diff --git a/.cvsignore b/.cvsignore deleted file mode 100644 index bc147887f..000000000 --- a/.cvsignore +++ /dev/null @@ -1,4 +0,0 @@ -__init__.pyc -build -setup.pyo -__init__.pyo diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3497785c5..3b6e4b960 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,12 +11,14 @@ Addresses issue # . **Checklist:** +The below is a list of things what will be considered when reviewing PRs. It is not prescriptive, and does not +imply that PRs which touch any of these will be rejected but gives a rough indication of where there is a potential +for hangups (i.e. factors which could turn a 5 min review into a half hour or longer and shunt it to the bottom +of the TODO list). -- [ ] Tested with numpy=1.14 -- [ ] Tested on python 2.7 and 3.6 -- [ ] Tested with wx=3.x and wx=4.x [if UI code] - [ ] Does the PR avoid variable renaming in existing code, whitespace changes, and other forms of tidying? [There is a place for code tidying, but it makes reviewing -much simpler if this is kept separate from functional changes] +much simpler if this is kept separate from functional changes. The auto-formatting performed by some editors is particulaly egregious and can lead to files with thousands +of non-functional changes with a few functional changes scattered amoungst them] If an enhancement (or non-trivial bugfix): diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index de22a4989..4d9a68e9b 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -25,12 +25,16 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.6' + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel sphinx mock numpy==1.14 numpydoc sphinx_rtd_theme - - name: Build + pip install setuptools wheel sphinx mock numpy numpydoc cython six sphinx_rtd_theme==0.5.2 + - name: Build extensions + run: | + cd PYME + python setup.py build_ext -i + - name: Build docs run: | cd docs ./gen_api_docs @@ -43,9 +47,9 @@ jobs: run: | cd docs curl -u "${DOCS_USERNAME}:${DOCS_PASSWORD}" -T doc_html.tar.bz2 https://python-microscopy.org:2078/ - - name: Artifact - uses: actions/upload-artifact@v1 - with: - name: docs_html.tar.bz2 - path: docs/doc_html.tar.bz2 + # - name: Artifact + # uses: actions/upload-artifact@v1 + # with: + # name: docs_html.tar.bz2 + # path: docs/doc_html.tar.bz2 diff --git a/.github/workflows/build_pip.yml b/.github/workflows/build_pip.yml index ed720a99f..8a4ee55f8 100644 --- a/.github/workflows/build_pip.yml +++ b/.github/workflows/build_pip.yml @@ -11,32 +11,40 @@ on: jobs: deploy_source: - runs-on: 'ubuntu-latest' - + runs-on: 'macos-latest' + steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python }} - - name: Install dependencies + python-version: '3.11' + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine cython numpy==1.14 pyyaml - - name: Build and publish + pip install build twine + - name: Build source distribution + run: | + python -m build --sdist + - name: Publish source distribution env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py sdist - python -m twine upload --skip-existing dist/* + twine upload --skip-existing dist/*.tar.gz deploy_wheel: needs: deploy_source #don't build wheels unless we successfully built source package. strategy: matrix: - os: [windows-latest, macos-latest] - python: ['3.6', '3.7'] #,2.7] - # 2.7 disabled for now due to missing VC9 + os: [windows-latest, macos-latest] #, ubuntu-latest] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] + arch: [x86_64, arm64] + exclude: + - os: windows-latest + arch: arm64 + - os: ubuntu-latest + arch: arm64 runs-on: ${{ matrix.os }} @@ -46,14 +54,21 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - - name: Install dependencies + - name: Install build dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine cython numpy==1.14 pyyaml - - name: Build and publish + pip install build twine + - name: Build wheel + shell: bash + run: | + if [[ "${{ matrix.os }}" == "macos-latest" ]]; then + arch -${{ matrix.arch }} python -m build --wheel + else + python -m build --wheel + fi + - name: Publish wheel env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - python setup.py bdist_wheel - twine upload --skip-existing dist/* + twine upload --skip-existing dist/*.whl diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..13d7f1528 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,74 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '32 4 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ['javascript', 'python' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + # - name: Autobuild + # uses: github/codeql-action/autobuild@v1 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + - if: matrix.language == 'cpp' + name: CPP build + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine cython numpy pyyaml + python setup.py build_ext + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/make_release.yml b/.github/workflows/make_release.yml index f40df3d08..b6310692f 100644 --- a/.github/workflows/make_release.yml +++ b/.github/workflows/make_release.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: '3.10' - name: update version run: | #python -m pip install --upgrade pip @@ -30,6 +30,7 @@ jobs: uses: ad-m/github-push-action@master with: github_token: ${{ secrets.GITHUB_TOKEN }} + force: true branch: ${{ github.ref }} - name: Create Release id: create_release @@ -39,4 +40,11 @@ jobs: with: tag_name: ${{ env.PYMEVERSION }} release_name: Release ${{ env.PYMEVERSION }} + # - name: Tigger package build + # run: | + # curl \ + # -X POST https://api.github.com/repos/python-microscopy/pyme-conda-recipes/actions/workflows/build_pyme_packages.yml/dispatches \ + # -H "Accept: application/vnd.github.v3+json" \ + # -u ${{ secrets.REC_REPO_TOKEN }}\ + # -d '{"ref":"ref"}' diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml new file mode 100644 index 000000000..197a4644d --- /dev/null +++ b/.github/workflows/pr_check.yml @@ -0,0 +1,130 @@ +name: CI Tests + +on: + pull_request: + branches: [master, main] + push: + branches: [master, main] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + python-version: ['3.11'] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + id: setup-python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: 'pyproject.toml' + + # Cache the entire pip site-packages directory + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + ${{ steps.setup-python.outputs.python-path }}/lib/python${{ matrix.python-version }}/site-packages + key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip-${{ matrix.python-version }}- + ${{ runner.os }}-pip- + + - name: Install system dependencies + run: | + sudo apt-get update -q + sudo apt-get install -y ninja-build + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + # Install the build-system.requires from pyproject.toml explicitly so + # that --no-build-isolation works (avoids a redundant isolated env). + pip install "meson-python" "meson" "cython" "numpy" + + - name: Install runtime dependencies (excluding wxpython) + run: | + # pymecompress must be built with --no-build-isolation to see the already-installed numpy. + pip install --no-build-isolation pymecompress + # Read remaining deps from pyproject.toml; skip wxpython (requires a display) + # and pymecompress. + python - <<'EOF' + import tomllib, subprocess, sys + with open('pyproject.toml', 'rb') as f: + deps = tomllib.load(f)['project']['dependencies'] + skip = {'wxpython', 'pymecompress'} + deps = [d for d in deps if not any(d.lower().startswith(s) for s in skip)] + subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + deps) + EOF + + - name: Build and install package + run: | + # --no-build-isolation reuses the build deps installed above. + # --no-deps avoids pip trying to install wxpython at this stage. + pip install --no-build-isolation --no-deps . + env: + # Disable Intel SVML to avoid undefined symbol issues + NPY_DISABLE_SVML: 1 + #CFLAGS: "-fno-vectorize" + + - name: Install test dependencies + run: | + pip install pytest pytest-cov + + - name: Run tests + run: | + pytest -v tests/ --cov=PYME --cov-report=xml --junit-xml=test-results-${{ matrix.python-version }}.xml + env: + GITHUB_CI: "1" + # Workaround for SVML symbols + #LD_PRELOAD: /usr/lib/x86_64-linux-gnu/libmvec.so.1 + + # Upload with unique name per matrix job + - name: Upload test results + uses: actions/upload-artifact@v4 + if: ${{ always() }} + with: + name: test-results-${{ matrix.python-version }} + path: test-results-${{ matrix.python-version }}.xml + retention-days: 30 + + # Only upload coverage from first matrix job + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: ${{ always() && strategy.job-index == 0 }} + with: + name: coverage + path: coverage.xml + retention-days: 30 + + # Merge all test results into single artifact + merge-test-results: + runs-on: ubuntu-latest + needs: test + if: always() + steps: + - name: Merge test results + uses: actions/upload-artifact/merge@v4 + with: + name: test-results + pattern: test-results-* + delete-merged: true + retention-days: 30 + + # - name: Upload coverage to Codecov + # # Only upload from one Python version to avoid duplicate reports + # if: matrix.python-version == '3.11' && !cancelled() + # uses: codecov/codecov-action@v4 + # with: + # files: coverage.xml + # fail_ci_if_error: false + # env: + # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/test_report.yml b/.github/workflows/test_report.yml new file mode 100644 index 000000000..dc22b5024 --- /dev/null +++ b/.github/workflows/test_report.yml @@ -0,0 +1,61 @@ +name: 'Test Report' +on: + workflow_run: + workflows: ['CI Tests'] # runs after CI workflow + types: + - completed +permissions: + contents: read + actions: read + checks: write + pull-requests: write +jobs: + report: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion != 'cancelled' + + steps: + - name: Download test results + uses: actions/download-artifact@v4 + with: + name: test-results + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish test results + uses: EnricoMi/publish-unit-test-result-action@v2 + if: always() + with: + files: '*.xml' + check_name: 'Python Test Results' + comment_mode: always + compare_to_earlier_commit: true + # This action shows full error messages and stack traces + report_individual_runs: true + deduplicate_classes_by_file_name: false + # Key additions for workflow_run trigger: + commit: ${{ github.event.workflow_run.head_sha }} + event_file: ${{ github.event_path }} + event_name: ${{ github.event.workflow_run.event }} + + - name: Download coverage artifact + if: always() + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: coverage + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Pytest coverage comment + if: always() + continue-on-error: true + uses: MishaKav/pytest-coverage-comment@v1 + with: + pytest-xml-coverage-path: ./coverage.xml + # Crucial: Pull the PR number from the workflow_run event + issue-number: ${{ github.event.workflow_run.pull_requests[0].number }} + # This enables the "coverage of code touched by PR" view + report-only-changed-files: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index fd528ac90..0f5b72251 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ build/* */_build/* */dist/* dist\* +dist/* *_hidden* *.so *.o @@ -23,6 +24,7 @@ Deconv/fftw_wisdom *.egg-info* docs/api/* docs/html/* +docs/warn.txt doc/api/* doc/html/* .idea @@ -52,3 +54,11 @@ tests/*.html fftw_wisdom.pkl *.css.map .vscode/ +pyme-bootstrap/package-lock.json +PYME/experimental/_octree.html +PYME/experimental/_treap.html +PYME/experimental/_triangle_mesh.html +PYME/experimental/func_octree.c +*.egg +*.whl +*.log \ No newline at end of file diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 000000000..c1cb049f9 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,53 @@ +### Instructions for building using pyproject.toml and meson + +Fairly rough notes so far, + +# Setup + +`pip install build` + + +# Building + +`python -m build` (will build both sdist and wheel) + +In most cases you might be better building just the wheel, ie + +`python -m build --wheel` + +# Specifying architecture on mac + +``` bash +export ARCHFLAGS="-arch x86_64" +python -m build +``` +alternatively / preferentially `-arch aarch64` + + + +# Validating that everything made it into the wheel + +- Unpack wheel +- e.g. +``` bash +diff -rq --exclude='*.pyc' --exclude='__pycache__' --exclude='*.so' --exclude='*.build' --exclude='.DS_Store' --exclude='*.h' --exclude='*.c' ./PYME ./dist/python_microscopy-25.6.5-cp313-cp313-macosx_10_16_x86_64/PYME +``` + +Obviously substituting the path to the un-packed wheel. It's a bit of a judgement call what should make it in (I exclude a bunch of the bitrot and super-experimental stuff by default), but if you've added something new and it doesn't make it through this should pick up on it. + + +## Editable install (replaces `python setup.py develop`) + +Ensure `build`, `meson` and `meson-python` are installed alongside standard install dependencies + +```bash +pip install --no-build-isolation --editable . +``` + +To recompile extension modules: + +```bash +pip install --no-deps --no-build-isolation --editable . +``` + +replacing the build subdir (e.g. `cp313`) with the one appropriate for your python version (i.e. the one that was generated with `pip install -e` above) \ No newline at end of file diff --git a/PYME/Acquire/.cvsignore b/PYME/Acquire/.cvsignore deleted file mode 100644 index fd4801039..000000000 --- a/PYME/Acquire/.cvsignore +++ /dev/null @@ -1,69 +0,0 @@ -prevviewer.pyo -chanedit.pyo -ExecTools.pyc -HDFSpoolFrame.pyo -timesequenceaquisator.pyc -Spooler.pyc -chanfr.pyo -setup.pyo -eventLog.pyo -chanpanel.pyc -prevviewer.pyc -splashScreen.pyo -pointScanner.pyc -timesequenceaquisator.pyo -seqdialog_.pyo -zScanner.pyc -smiApp.pyo -__init__.pyo -setup.pyc -simplesequenceaquisator.pyc -Spooler.pyo -chanfr.pyc -chanedit.pyc -__init__.pyc -MetaDataHandler.pyc -FrSpool.pyo -funcs.pyc -mytimer.pyc -eventLog.pyc -HDFSpoolFrame.pyc -coverslipGeometries.pyc -MetaDataHandler.pyo -mytimer.pyo -HDFSpooler.pyo -protocol.pyc -intsliders.pyc -sampleInformation.pyc -timeseqdialog.pyo -ccdCalibrator.pyo -ccdCalibrator.pyc -previewaquisator.pyo -PYMESettings.db -fastTiler.pyc -timeseqdialog.pyc -funcs.pyo -previewaquisator.pyc -smimainframe.pyc -QueueSpooler.pyc -intsliders.pyo -protocol.pyo -PYMEAquire.pyo -seqdialog.pyo -KDFSpooler.pyo -seqdialog.pyc -smimainframe.pyo -sarcSpacing.pyo -simplesequenceaquisator.pyo -QueueSpooler.pyo -psliders.pyc -noclosefr.pyc -sarcSpacing.pyc -ExecTools.pyo -stepDialog.pyo -chanpanel.pyo -HDFSpooler.pyc -psliders.pyo -splashScreen.pyc -noclosefr.pyo -stepDialog.pyc diff --git a/PYME/Acquire/ActionManager.py b/PYME/Acquire/ActionManager.py index 9d6ef32dd..5c9bef770 100644 --- a/PYME/Acquire/ActionManager.py +++ b/PYME/Acquire/ActionManager.py @@ -21,6 +21,19 @@ import logging logger = logging.getLogger(__name__) +from .actions import * + +class MutablePriorityQueue(Queue.PriorityQueue): + def __init__(self, *args, **kwargs): + Queue.PriorityQueue.__init__(self, *args,**kwargs) + + def remove(self, item): + with self.mutex: + if item in self.queue: + self.queue.remove(item) + #self.queue.sort() + self.not_full.notify() + class ActionManager(object): """This implements a queue for actions which should be called sequentially. @@ -44,13 +57,14 @@ def __init__(self, scope): Parameters ---------- - scope : PYME.Acquire.microscope.microscope object + scope : PYME.Acquire.microscope.Microscope object The microscope. The function object to call for an action should be accessible within the scope namespace, and will be resolved by calling eval('scope.functionName') """ - self.actionQueue = Queue.PriorityQueue() + self.actionQueue = MutablePriorityQueue() # queue for immediate execution + self.scheduledQueue = MutablePriorityQueue() # queue for scheduled execution self.scope = weakref.ref(scope) #this will be assigned to a callback to indicate if the last task has completed @@ -68,28 +82,32 @@ def __init__(self, scope): self._monitor.daemon = True self._monitor.start() + self._lock = threading.Lock() + def QueueAction(self, functionName, args, nice=10, timeout=1e6, - max_duration=np.finfo(float).max): - """Add an action to the queue + max_duration=np.finfo(float).max, execute_after=0): + """Add an action to the queue. Legacy version for string based actions. Most applications should use queue_actions() below instead Parameters ---------- functionName : string The name of a function relative to the microscope object. - e.g. to `call scope.spoolController.StartSpooling()`, you would use - a functionName of 'spoolController.StartSpooling'. + e.g. to `call scope.spoolController.start_spooling()`, you would use + a functionName of 'spoolController.start_spooling'. The function should either return `None` if the operation has already completed, or function which evaluates to True once the operation - has completed. See `scope.spoolController.StartSpooling()` for an + has completed. See `scope.spoolController.start_spooling()` for an example. args : dict a dictionary of arguments to pass the function nice : int (or float) The priority with which to execute the function. Functions with a - lower nice value execute first. + lower nice value execute first. Nice should have a value between 0 and 20. nice=20 is reserved by convention + for tidy-up tasks which should run + after all other tasks and put the microscope in a 'safe' state. timeout : float A timeout in seconds from the current time at which the action becomes irrelevant and should be ignored. @@ -100,9 +118,19 @@ def QueueAction(self, functionName, args, nice=10, timeout=1e6, though it has presumably already failed at that point. Intended as a safety feature for automated acquisitions, the check is every 3 s rather than fine-grained. + execute_after: float + A timestamp in system time before which the action should not be executed. + If this is before the current time, the action will be queued for immediate + execution, otherwise it will be placed in a queue of scheduled acquisitions + which is polled by the Tick() method and added to the execution queue when + the time is right. The default of 0 means that the action will be queued for + immediate execution. """ - curTime = time.time() + # make sure nice is in supported range. + assert ((nice >= 0) and (nice <= 20)) + + curTime = time.time() expiry = curTime + timeout #make sure our timestamps strictly increment @@ -111,8 +139,113 @@ def QueueAction(self, functionName, args, nice=10, timeout=1e6, #ensure FIFO behaviour for events with the same priority nice_ = nice + self._timestamp*1e-10 - self.actionQueue.put_nowait((nice_, functionName, args, expiry, - max_duration)) + if execute_after < curTime: + logger.debug('Queuing action %s for immediate execution (%s, %s)' % (functionName, curTime, execute_after)) + self.actionQueue.put_nowait((nice_, FunctionAction(functionName, args), expiry, max_duration)) + else: + logger.debug('Queuing action %s for delayed execution (%s, %s)' % (functionName, curTime, execute_after)) + self.scheduledQueue.put_nowait((execute_after, (nice_, FunctionAction(functionName, args), expiry, max_duration))) + + self.onQueueChange.send(self) + + def queue_actions(self, actions, nice=10, timeout=1e6, max_duration=np.finfo(float).max, execute_after=0): + ''' + Queue a number of actions for subsequent execution + + Parameters + ---------- + actions : list + A list of Action instances + nice : int (or float) + The priority with which to execute the function. Functions with a + lower nice value execute first. Nice should have a value between 0 and 20. + timeout : float + A timeout in seconds from the current time at which the action + becomes irrelevant and should be ignored. + max_duration : float + A generous estimate, in seconds, of how long the task might take, + after which the lasers will be automatically turned off and the + action queue paused. This will not interrupt the current task, + though it has presumably already failed at that point. Intended as a + safety feature for automated acquisitions, the check is every 3 s + rather than fine-grained. + execute_after: float + A timestamp in system time before which the action should not be executed. + If this is before the current time, the action will be queued for immediate + execution, otherwise it will be placed in a queue of scheduled acquisitions + which is polled by the Tick() method and added to the execution queue when + the time is right. The default of 0 means that the action will be queued for + immediate execution. + + Returns + ------- + + + Examples + -------- + + >>> my_actions = [UpdateState(state={'Camera.ROI' : [50, 50, 200, 200]}), + >>> SpoolSeries(maxFrames=500, stack=False), + >>> UpdateState(state={'Camera.ROI' : [100, 100, 250, 250]}).then(SpoolSeries(maxFrames=500, stack=False)), + >>> ] + >>> + >>>ActionManager.queue_actions(my_actions) + + Note that the first two tasks are independant - + + ''' + # make sure nice is in supported range. + assert((nice >= 0) and (nice <= 20)) + + with self._lock: + # lock to prevent 'nice' collisions when queueing from separate threads. + + for j, action in enumerate(actions): + curTime = time.time() + expiry = curTime + timeout + + #make sure our timestamps strictly increment + self._timestamp = max(curTime, self._timestamp + 1e-3) + + #ensure FIFO behaviour for events with the same priority + nice_ = nice + self._timestamp * 1e-10 + + if np.isscalar(execute_after): + after = execute_after + else: + after = execute_after[j] + + if after < curTime: + #logger.debug('Queuing action %s for immediate execution (%s, %s)' % (functionName, curTime, execute_after)) + self.actionQueue.put_nowait((nice_, action, expiry, max_duration)) + else: + #logger.debug('Queuing action %s for delayed execution (%s, %s)' % (functionName, curTime, execute_after)) + self.scheduledQueue.put_nowait((after, (nice_, action, expiry, max_duration))) + + self.onQueueChange.send(self) + + def remove_actions(self, actions): + """Remove a specific action from the queue + + Parameters + ---------- + action : Action + The action to remove + """ + + #TODO - do we need to lock here? + + for action in actions: + # try and remove from both queues + try: + self.actionQueue.remove(action) + except ValueError: + pass + try: + self.scheduledQueue.remove(action) + except ValueError: + pass + self.onQueueChange.send(self) @@ -125,10 +258,28 @@ def Tick(self, **kwargs): if self.paused: return + # queue scheduled actions which are now due + _n_queued = 0 + while (self.scheduledQueue.qsize() > 0) and ((self.scheduledQueue.queue[0][0]) < time.time()): + execute_after, action = self.scheduledQueue.get_nowait() + print(time.time(), execute_after, action) + self.actionQueue.put_nowait(action) + _n_queued += 1 + + if _n_queued > 0: + self.onQueueChange.send(self) # TODO - avoid sending this signal here and below?? + if (self.isLastTaskDone is None) or self.isLastTaskDone(): + try: + self.currentTask.finalise(self.scope()) + self.currentTask = None + self.isLastTaskDone = None # prevent us from continuing to poll the last task if it's done + except AttributeError: + pass + try: self.currentTask = self.actionQueue.get_nowait() - nice, functionName, args, expiry, max_duration = self.currentTask + nice, action, expiry, max_duration = self.currentTask self._cur_task_kill_time = time.time() + max_duration self.onQueueChange.send(self) except Queue.Empty: @@ -136,12 +287,12 @@ def Tick(self, **kwargs): return if expiry > time.time(): - print('%s, %s' % (self.currentTask, functionName)) - fcn = eval('.'.join(['self.scope()', functionName])) - self.isLastTaskDone = fcn(**args) + logger.debug('Executing action: %s, %s' % (self.currentTask, action)) + #fcn = eval('.'.join(['self.scope()', functionName])) + self.isLastTaskDone = action(self.scope()) else: past_expire = time.time() - expiry - logger.debug('task expired %f s ago, ignoring %s' % (past_expire, + logger.debug('Action expired %f s ago, ignoring %s' % (past_expire, self.currentTask)) def _monitor_defunct(self): @@ -177,6 +328,42 @@ def __init__(self, action_manager): action manager instance to wrap """ self.action_manager = action_manager + + @webframework.register_endpoint('/queue_actions', output_is_json=False) + def queue_actions(self, body, nice=10, timeout=1e6, max_duration=np.finfo(float).max, execute_after=0): + """ + Add a list of actions to the queue + + Parameters + ---------- + body - json formatted list of serialised actions (see example below) + nice + timeout + max_duration + + Returns + ------- + + + Examples + -------- + + Body json + ^^^^^^^^^ + + .. code-block:: json + + [{'UpdateState':{'foo':'bar', 'then': {'SpoolSeries' : {...}}}] + + """ + import json + actions = [action_from_dict(a) for a in json.loads(body)] + + self.action_manager.queue_actions(actions, nice=int(nice), + timeout=float(timeout), + max_duration=float(max_duration), + execute_after=execute_after) + @webframework.register_endpoint('/queue_action', output_is_json=False) def queue_action(self, body): @@ -189,13 +376,13 @@ def queue_action(self, body): json.dumps(dict) with the following keys: function_name : str The name of a function relative to the microscope object. - e.g. to `call scope.spoolController.StartSpooling()`, you - would use a functionName of 'spoolController.StartSpooling'. + e.g. to `call scope.spoolController.start_spooling()`, you + would use a functionName of 'spoolController.start_spooling'. The function should either return `None` if the operation has already completed, or function which evaluates to True once the operation has completed. See - `scope.spoolController.StartSpooling()` for an example. + `scope.spoolController.start_spooling()` for an example. args : dict, optional a dictionary of arguments to pass to `function_name` nice : int, optional @@ -217,9 +404,10 @@ def queue_action(self, body): nice = params.get('nice', 10.) timeout = params.get('timeout', 1e6) max_duration = params.get('max_duration', np.finfo(float).max) + execute_after = params.get('execute_after', 0) self.action_manager.QueueAction(function_name, args, nice, timeout, - max_duration) + max_duration, execute_after) class ActionManagerServer(webframework.APIHTTPServer, ActionManagerWebWrapper): diff --git a/PYME/Acquire/ExecTools.py b/PYME/Acquire/ExecTools.py index 241980ff9..77308ad8a 100755 --- a/PYME/Acquire/ExecTools.py +++ b/PYME/Acquire/ExecTools.py @@ -195,7 +195,7 @@ def run(self, parent, scope): Parameters ---------- parent : PYME.Acquire.acquiremainframe.PYMEMainFrame, wx.Frame - scope : PYME.Acquire.microscope.microscope + scope : PYME.Acquire.microscope.Microscope """ global defGlobals diff --git a/PYME/Acquire/HDFSpooler.py b/PYME/Acquire/HDFSpooler.py deleted file mode 100755 index d16dc8542..000000000 --- a/PYME/Acquire/HDFSpooler.py +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/python - -################## -# HDFSpooler.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import datetime -import tables -from PYME.IO import MetaDataHandler - - -import time - -#from PYME.Acquire import eventLog -import PYME.Acquire.Spooler as sp -#from PYME.Acquire import protocol as p - -from PYME.IO.FileUtils import fileID - -class SpoolEvent(tables.IsDescription): - """Pytables description for Events table in spooled dataset""" - EventName = tables.StringCol(32) - Time = tables.Time64Col() - EventDescr = tables.StringCol(256) - -class EventLogger: - """Event logging backend for hdf/pytables data storage - - Parameters - ---------- - spool : instance of HDFSpooler.Spooler - The spooler to ascociate this logger with - - hdf5File : pytables hdf file - The open HDF5 file to write to - """ - def __init__(self, spool, hdf5File): - """Create a new Events table. - - - """ - self.spooler = spool - #self.scope = scope - self.hdf5File = hdf5File - - self.evts = self.hdf5File.create_table(hdf5File.root, 'Events', SpoolEvent) - - def logEvent(self, eventName, eventDescr = '', timestamp=None): - """Log an event. - - Parameters - ---------- - eventName : string - short event name - < 32 chars and should be shared by events of the - same type. - eventDescr : string - description of the event - additional, even specific information - packaged as a string (<255 chars). This is commonly used to store - parameters - e.g. z positions, and should be both human readable and - easily parsed. - - - In addition to the name and description, timing information is recorded - for each event. - """ - if eventName == 'StartAq': - eventDescr = '%d' % self.spooler.imNum - - ev = self.evts.row - - ev['EventName'] = eventName - ev['EventDescr'] = eventDescr - - if timestamp is None: - ev['Time'] = sp.timeFcn() - else: - ev['Time'] = timestamp - - ev.append() - self.evts.flush() - -class Spooler(sp.Spooler): - """Responsible for the mechanics of spooling to a pytables/hdf file. - """ - def __init__(self, filename, frameSource, frameShape, complevel=6, complib='zlib', **kwargs): - self.h5File = tables.open_file(filename, 'w') - - filt = tables.Filters(complevel, complib, shuffle=True) - - self.imageData = self.h5File.create_earray(self.h5File.root, 'ImageData', tables.UInt16Atom(), (0,frameShape[0],frameShape[1]), filters=filt) - self.md = MetaDataHandler.HDFMDHandler(self.h5File) - self.evtLogger = EventLogger(self, self.h5File) - - sp.Spooler.__init__(self, filename, frameSource, **kwargs) - - def StopSpool(self): - """Stop spooling and close file""" - sp.Spooler.StopSpool(self) - - self.h5File.flush() - self.h5File.close() - - def OnFrame(self, sender, frameData, **kwargs): - """Called on each frame""" - - if not self.watchingFrames: - # drop frames if we've already stopped spooling. TODO - do we also need to check if disconnect works? - return - - #print 'f' - if frameData.shape[0] == 1: - self.imageData.append(frameData) - else: - self.imageData.append(frameData.reshape(1,frameData.shape[0],frameData.shape[1])) - self.h5File.flush() - if self.imNum == 0: #first frame - self.md.setEntry('imageID', fileID.genFrameID(self.imageData[0,:,:])) - - sp.Spooler.OnFrame(self) - - def __del__(self): - if self.spoolOn: - self.StopSpool() diff --git a/PYME/Acquire/HTTPHDFSpooler.py b/PYME/Acquire/HTTPHDFSpooler.py deleted file mode 100644 index 9787607e0..000000000 --- a/PYME/Acquire/HTTPHDFSpooler.py +++ /dev/null @@ -1,229 +0,0 @@ -#!/usr/bin/python - -################## -# QueueSpooler.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -from __future__ import print_function -import tables -from PYME.IO import MetaDataHandler - -#import Pyro.core -import os -import time - -import PYME.Acquire.Spooler as sp -#from PYME.Acquire import protocol as p -from PYME.IO.FileUtils import fileID, nameUtils -#from PYME.IO.FileUtils.nameUtils import getRelFilename - - -try: - # noinspection PyCompatibility - import httplib -except ImportError: - #py3 - import http.client as httplib - -try: - # noinspection PyCompatibility - import cPickle as pickle -except ImportError: - #py3 - import pickle - -import threading - -try: - # noinspection PyCompatibility - import Queue -except ImportError: - #py3 - import queue as Queue - -import requests - -#rom PYME.Acquire import eventLog - -class SpoolEvent(tables.IsDescription): - EventName = tables.StringCol(32) - Time = tables.Time64Col() - EventDescr = tables.StringCol(256) - -class EventLogger: - def __init__(self, spool):#), scope): - self.spooler = spool - #self.scope = scope - - def logEvent(self, eventName, eventDescr = '', timestamp = None): - if eventName == 'StartAq': - eventDescr = '%d' % self.spooler.imNum - - if timestamp is None: - timestamp = sp.timeFcn() - self.spooler._post('NEWEVENT', (eventName, eventDescr, timestamp)) - -class HttpSpoolMDHandler(MetaDataHandler.MDHandlerBase): - def __init__(self, spooler, mdToCopy=None): - self.spooler = spooler - self.cache = {} - - if not mdToCopy is None: - self.copyEntriesFrom(mdToCopy) - - def setEntry(self,entryName, value): - self.spooler._post('METADATAENTRY', (entryName, value)) - - def getEntry(self,entryName): - return self.cache[entryName] - - def getEntryNames(self): - return self.cache.keys() - - def copyEntriesFrom(self, mdToCopy): - self.spooler._post('METADATA', MetaDataHandler.NestedClassMDHandler(mdToCopy)) - -SERVERNAME='127.0.0.1:8080' - -class Spooler(sp.Spooler): - def __init__(self, filename, frameSource, frameShape, **kwargs): - # if 'PYME_TASKQUEUENAME' in os.environ.keys(): - # taskQueueName = os.environ['PYME_TASKQUEUENAME'] - # else: - # taskQueueName = 'taskQueue' - #from PYME.misc.computerName import GetComputerName - #compName = GetComputerName() - - #taskQueueName = 'TaskQueues.%s' % compName - - #self.tq = Pyro.core.getProxyForURI('PYRONAME://' + taskQueueName) - #self.tq._setOneway(['postTask', 'postTasks', 'addQueueEvents', 'setQueueMetaData', 'logQueueEvent']) - filename = filename[len(nameUtils.datadir):] - - - self.seriesName = '/'.join(filename.split(os.path.sep)) - #print filename, self.seriesName - self.buffer = [] - self.buflen = 30 - - - - self.postQueue = Queue.Queue() - self.dPoll = True - - self.pollThread = threading.Thread(target=self._queuePoll) - self.pollThread.start() - - #self.tq.createQueue('HDFTaskQueue',self.seriesName, filename, frameSize = (scope.cam.GetPicWidth(), scope.cam.GetPicHeight()), complevel=complevel, complib=complib) - - self.md = HttpSpoolMDHandler(self) - self.evtLogger = EventLogger(self) - - - sp.Spooler.__init__(self, filename, frameSource, **kwargs) - - def __queuePoll(self): - self.conn = httplib.HTTPConnection(SERVERNAME, timeout=5) - while self.dPoll: - ur, data = self.postQueue.get() - #print(repr(ur)) - #try: - self.conn.request('POST', ur.encode(), pickle.dumps(data, 2), {"Connection":"keep-alive"}) - - resp = self.conn.getresponse() - #print resp.status, resp.reason - #except UnicodeDecodeError: - # print self.conn._buffer - time.sleep(.1) - - - def _queuePoll(self): - #self.conn = - while self.dPoll: - ur, data = self.postQueue.get() - #print repr(ur) - #conn = httplib.HTTPConnection(SERVERNAME, timeout=15) - #print 'hc' - #conn.request('POST', ur.encode(), pickle.dumps(data, 2))#, {"Connection":"keep-alive"}) - #print 'rq' - #resp = conn.getresponse() - #print 'rp' - #conn.close() - - r = requests.post('http://' + SERVERNAME + ur.encode(), pickle.dumps(data, 2)) - - #print r.status_code - - #print resp.status, resp.reason - #except UnicodeDecodeError: - # print self.conn._buffer - time.sleep(.1) - - def ___queuePoll(self): - #self.conn = - while self.dPoll: - ur, data = self.postQueue.get() - #print repr(ur) - conn = httplib.HTTPConnection(SERVERNAME, timeout=15) - #print 'hc' - conn.request('POST', ur.encode(), pickle.dumps(data, 2))#, {"Connection":"keep-alive"}) - #print 'rq' - resp = conn.getresponse() - #print 'rp' - #conn.close() - - #print resp.status, resp.reason - #except UnicodeDecodeError: - # print self.conn._buffer - time.sleep(.1) - - def _post(self, ursufix, data): - self.postQueue.put(('%s/%s' % (self.seriesName, ursufix), data)) - - def getURL(self): - return 'http://' + SERVERNAME + self.seriesName - - def StopSpool(self): - self.dPoll = False - sp.Spooler.StopSpool(self) - - - def OnFrame(self, sender, frameData, **kwargs): - self.buffer.append(frameData.reshape(1,frameData.shape[0],frameData.shape[1]).copy()) - - if self.imNum == 0: #first frame - self.md.setEntry('imageID', fileID.genFrameID(self.buffer[-1].squeeze())) - #pass - - if len(self.buffer) >= self.buflen: - self.FlushBuffer() - - sp.Spooler.OnFrame(self) - - def FlushBuffer(self): - t1 = time.time() - self._post('NEWFRAMES', self.buffer) - #print time.time() -t1 - self.buffer = [] - - - - - diff --git a/PYME/Acquire/HTTPSpooler.py b/PYME/Acquire/HTTPSpooler.py deleted file mode 100644 index be8a76db1..000000000 --- a/PYME/Acquire/HTTPSpooler.py +++ /dev/null @@ -1,307 +0,0 @@ -#!/usr/bin/python - -################## -# QueueSpooler.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import tables -from PYME.IO import MetaDataHandler - -#import Pyro.core -import os -import time - -import PYME.Acquire.Spooler as sp -#from PYME.Acquire import protocol as p -#from PYME.IO.FileUtils import fileID, nameUtils -#from PYME.ParallelTasks.relativeFiles import getRelFilename - -import threading - -try: - # noinspection PyCompatibility - import Queue -except ImportError: - #py3 - import queue as Queue - -from PYME.IO import clusterIO -from PYME.IO import PZFFormat - -import numpy as np -import random -from PYME import config - -import json - -import logging -logger = logging.getLogger(__name__) - -class EventLogger: - def __init__(self, spool):#, scope): - self.spooler = spool - #self.scope = scope - - self._events = [] - self._event_lock = threading.Lock() - - def logEvent(self, eventName, eventDescr = '', timestamp=None): - if eventName == 'StartAq' and eventDescr == '': - eventDescr = '%d' % self.spooler.imNum - - if timestamp is None: - timestamp = sp.timeFcn() - - with self._event_lock: - self._events.append((eventName, eventDescr, timestamp)) - - def to_JSON(self): - return json.dumps(self._events) - - -def genSequenceID(filename=''): - return int(time.time()) & random.randint(0, 2**31) << 31 - - - - -def getReducedFilename(filename): - #rname = filename[len(nameUtils.datadir):] - - sname = '/'.join(filename.split(os.path.sep)) - if sname.startswith('/'): - sname = sname[1:] - - return sname - -def exists(seriesName): - return clusterIO.exists(getReducedFilename(seriesName)) - -#Push data to cluster from multiple threads simultaeneously to hide IO latency -#of each individual node. Not sure what the best number is here - currenty set -#a "safe" maximum number of nodes that could access data -NUM_POLL_THREADS = 10 -QUEUE_MAX_SIZE = 200 # ~10k frames - -defaultCompSettings = { - 'compression' : PZFFormat.DATA_COMP_HUFFCODE, - 'quantization' : PZFFormat.DATA_QUANT_NONE, - 'quantizationOffset' : -1e6, # set to an unreasonable value so that we raise an error if default offset is used - 'quantizationScale' : 1.0 -} - -class Spooler(sp.Spooler): - def __init__(self, filename, frameSource, frameShape, **kwargs): - - #filename = filename[len(nameUtils.datadir):] - - #filename, - self.seriesName = getReducedFilename(filename) - - #self.seriesName = '/'.join(filename.split(os.path.sep)) - #if self.seriesName.startswith('/'): - # self.seriesName = self.seriesName[1:] - #print filename, self.seriesName - self._aggregate_h5 = kwargs.get('aggregate_h5', False) - - self.clusterFilter = kwargs.get('serverfilter', - config.get('dataserver-filter', '')) - self._buffer = [] - - self.buflen = config.get('httpspooler-chunksize', 50) - - self._postQueue = Queue.Queue(QUEUE_MAX_SIZE) - self._dPoll = True - self._stopping = False - self._lock = threading.Lock() - - self._last_thread_exception = None - - self._numThreadsProcessing = 0 - - self._pollThreads = [] - for i in range(NUM_POLL_THREADS): - pt = threading.Thread(target=self._queuePoll) - pt.daemon = False - pt.start() - self._pollThreads.append(pt) - - self.md = MetaDataHandler.NestedClassMDHandler() - self.evtLogger = EventLogger(self) - - self.sequenceID = genSequenceID() - self.md['imageID'] = self.sequenceID - - sp.Spooler.__init__(self, filename, frameSource, **kwargs) - - self._lastFrameTime = 1e12 - - self.compSettings = {} - self.compSettings.update(defaultCompSettings) - try: - self.compSettings.update(kwargs['compressionSettings']) - except KeyError: - pass - - if not self.compSettings['quantization'] == PZFFormat.DATA_QUANT_NONE: - # do some sanity checks on our quantization parameters - # note that these conversions will throw a ValueError if the settings are not numeric - offset = float(self.compSettings['quantizationOffset']) - scale = float(self.compSettings['quantizationScale']) - - # these are potentially a bit too permissive, but should catch an offset which has been left at the - # default value - assert(offset >= 0) - assert(scale >=.001) - assert(scale <= 100) - - def _queuePoll(self): - while self._dPoll: - try: - data = self._postQueue.get_nowait() - - with self._lock: - self._numThreadsProcessing += 1 - - try: - files = [] - for imNum, frame in data: - if self._aggregate_h5: - fn = '/'.join(['__aggregate_h5', self.seriesName, 'frame%05d.pzf' % imNum]) - else: - fn = '/'.join([self.seriesName, 'frame%05d.pzf' % imNum]) - - pzf = PZFFormat.dumps(frame, sequenceID=self.sequenceID, frameNum = imNum, **self.compSettings) - - files.append((fn, pzf)) - - if len(files) > 0: - clusterIO.put_files(files, serverfilter=self.clusterFilter) - - except Exception as e: - self._last_thread_exception = e - logging.exception('Exception whilst putting files') - raise - finally: - with self._lock: - self._numThreadsProcessing -= 1 - - time.sleep(.01) - #print 't', len(data) - except Queue.Empty: - if self._stopping: - self._dPoll = False - else: - time.sleep(.01) - - def finished(self): - if not self._last_thread_exception is None: - #raise an exception here, in the calling thread - logging.error('An exception occurred in one of the spooling threads') - raise RuntimeError('An exception occurred in one of the spooling threads') - else: - return self._postQueue.empty() and (self._numThreadsProcessing == 0) - - - def getURL(self): - #print CLUSTERID, self.seriesName - return 'PYME-CLUSTER://%s/%s' % (self.clusterFilter, self.seriesName) - - def StartSpool(self): - sp.Spooler.StartSpool(self) - - logger.debug('Starting spooling: %s' %self.seriesName) - - if self._aggregate_h5: - #NOTE: allow a longer timeout than normal here as __aggregate with metadata waits for a lock on the server side before - # actually adding (and is therefore susceptible to longer latencies than most operations). FIXME - remove server side lock. - clusterIO.put_file('__aggregate_h5/' + self.seriesName + '/metadata.json', self.md.to_JSON().encode(), serverfilter=self.clusterFilter, timeout=3) - else: - clusterIO.put_file(self.seriesName + '/metadata.json', self.md.to_JSON().encode(), serverfilter=self.clusterFilter) - - def StopSpool(self): - sp.Spooler.StopSpool(self) - - # wait until our input queue is empty rather than immediately stopping saving. - self._stopping=True - logger.debug('Stopping spooling %s' % self.seriesName) - - - #join our polling threads - if config.get('httpspooler-jointhreads', True): - # Allow this to be switched off in a config option for maximum performance on High Throughput system. - # Joining threads is the recommended and safest behaviour, but forces spooling of current series to complete - # before next series starts, so could have negative performance implications. - # The alternative - letting spooling continue during the acquisition of the next series - has the potential - # to result in runaway memory and thread usage when things go pear shaped (i.e. spooling is not fast enough) - # TODO - is there actually a performance impact that justifies this config option, or is it purely theoretical - for pt in self._pollThreads: - pt.join() - - # remove our reference to the threads which hold back-references preventing garbage collection - del(self._pollThreads) - - # save events and final metadata - # TODO - use a binary format for saving events - they can be quite - # numerous, and can trip the standard 1 s clusterIO.put_file timeout. - # Use long timeouts as a temporary hack because failing these can ruin - # a dataset - if self._aggregate_h5: - clusterIO.put_file('__aggregate_h5/' + self.seriesName + '/final_metadata.json', - self.md.to_JSON().encode(), self.clusterFilter) - clusterIO.put_file('__aggregate_h5/' + self.seriesName + '/events.json', - self.evtLogger.to_JSON().encode(), - self.clusterFilter, timeout=10) - else: - clusterIO.put_file(self.seriesName + '/final_metadata.json', - self.md.to_JSON().encode(), self.clusterFilter) - clusterIO.put_file(self.seriesName + '/events.json', - self.evtLogger.to_JSON().encode(), - self.clusterFilter, timeout=10) - - - def OnFrame(self, sender, frameData, **kwargs): - # NOTE: copy is now performed in frameWrangler, so we don't need to worry about it here - if frameData.shape[0] == 1: - self._buffer.append((self.imNum, frameData)) - else: - self._buffer.append((self.imNum, frameData.reshape(1, frameData.shape[0], frameData.shape[1]))) - - #print len(self.buffer) - t = time.time() - - #purge buffer if more than self.buflen frames have been added, or more than 1 second elapsed - if (len(self._buffer) >= self.buflen) or ((t - self._lastFrameTime) > 1): - self.FlushBuffer() - self._lastFrameTime = t - - sp.Spooler.OnFrame(self) - - def cleanup(self): - self._dPoll = False - - def FlushBuffer(self): - self._postQueue.put(self._buffer) - self._buffer = [] - - - - - diff --git a/PYME/Acquire/Hardware/.cvsignore b/PYME/Acquire/Hardware/.cvsignore deleted file mode 100644 index f8ba1c41c..000000000 --- a/PYME/Acquire/Hardware/.cvsignore +++ /dev/null @@ -1,37 +0,0 @@ -focusKeys.pyo -NikonTE2000.pyc -fakeShutters.pyo -ccdCalibrator.pyo -setup.pyo -FilterWheel.pyo -ccdCalibrator.pyc -ccdAdjPanel.pyc -ccdAdjPanelC.pyo -FocCorrR.pyo -LaserControlFrame.pyo -FilterWheel.pyc -lasers.pyo -__init__.pyo -fakeShutters.pyc -setup.pyc -LaserControlFrame.pyc -NikonTE2000.pyo -frZStage.pyo -ccdAdjPanel.pyo -FocCorr.pyo -__init__.pyc -EMCCDTheory.pyc -FrFilter.pyo -lasers.pyc -fw102.pyo -frZStage.pyc -nikonZStage.pyo -ccdAdjPanelF.pyo -thorlabsPiezo.pyo -splitter.pyo -focusKeys.pyc -EMCCDTheory.pyo -FocCorrR.pyc -fw102.pyc -thorlabsPiezo.pyc -splitter.pyc diff --git a/PYME/Acquire/Hardware/AAOptoelectronics/MDS.py b/PYME/Acquire/Hardware/AAOptoelectronics/MDS.py index 73104deac..6a61278f0 100644 --- a/PYME/Acquire/Hardware/AAOptoelectronics/MDS.py +++ b/PYME/Acquire/Hardware/AAOptoelectronics/MDS.py @@ -37,7 +37,7 @@ class AAOptoMDS(AOTF): because we connect to it through an external USB hub. As a result we keep the serial ports open rather than using context managers. """ - def __init__(self, calibrations, com_port='COM6', name='AAOptoMDS', n_chans=8, serial_timeout=1): + def __init__(self, calibrations, com_port='COM6', name='AAOptoMDS', n_chans=8, serial_timeout=1, baud_rate=57600): """ Parameters ---------- @@ -58,7 +58,7 @@ def __init__(self, calibrations, com_port='COM6', name='AAOptoMDS', n_chans=8, s # initialize serial self.timeout = serial_timeout # initialize and configure the serial port without opening it - self.com_port = serial.Serial(com_port, timeout=serial_timeout) + self.com_port = serial.Serial(com_port, baud_rate, timeout=serial_timeout) self.lock = threading.Lock() self.is_on = True # set to internal control mode, grab a couple extra lines to give the unit time to write before clearing buffer diff --git a/tests/PYME/__init__.py b/PYME/Acquire/Hardware/ARCoptix/__init__.py similarity index 100% rename from tests/PYME/__init__.py rename to PYME/Acquire/Hardware/ARCoptix/__init__.py diff --git a/PYME/Acquire/Hardware/ARCoptix/lcdriver.py b/PYME/Acquire/Hardware/ARCoptix/lcdriver.py new file mode 100644 index 000000000..15cd508ba --- /dev/null +++ b/PYME/Acquire/Hardware/ARCoptix/lcdriver.py @@ -0,0 +1,149 @@ +# -*- coding: utf-8 -*- + +""" +Controls for the ARCoptix liquid crystal driver. + +Requirements: + - Download and install the LC driver from http://www.arcoptix.com/pdf/LC_Driver_1_2.zip. + This should install to C:\Program Files\ARCoptix\ARCoptix LC Driver 1.2 + - pip install pythonnet msl-loadlib + +This uses inter-process communication to access a 32-bit .NET DLL from 64-bit python. +Pre-flight to create the 32-bit server: + - Create a 32-bit conda environment + - set CONDA_FORCE_32BIT=1 + - conda create -n py37_32 python=3.7.9 # change this to the python used in your pyme install + - set CONDA_FORCE_32BIT=1 + - activate py37_32 + - pip install msl-loadlib pyinstaller comtypes pythonnet numpy # make sure the versions match the versions in your 64-bit environment + # numpy is optional, but if you don't use it make sure you cast everything + # to float before passing to the server + - python + - from msl.loadlib import freeze_server32 + - freeze_server32.main() + ... PyInstaller logging messages ... + Server saved to: ... + - Copy the generated server32-windows.exe file to + miniconda3\envs\<64-bit pyme environment>\lib\site-packages\msl\loadlib + - Copy C:\Program Files\ARCoptix\ARCoptix LC Driver 1.2\CyUSB.dll to + miniconda3\envs\\lib\site-packages\msl\loadlib. + NOTE: We shouldn't need to do this, but setting os.environ['PATH'] doesn't + update the path for server32-windows.exe. Ideally we'd find a way to do this + and add C:\Program Files\ARCoptix\ARCoptix LC Driver 1.2 to server32-windows.exe's + path. + - set CONDA_FORCE_32BIT= + - activate + +Created on Fri January 08 2021 + +@author: zacsimile +""" + +import os +import platform +from msl.loadlib import Client64 + +sys = platform.system() +if sys != 'Windows': + raise Exception("Operating system is not supported.") + +class LCDriver(Client64): + def __init__(self): + """ + Communicates with 32-bit LCDriver.dll library via lcserver32.py. + """ + Client64.__init__(self, module32='lcserver32', + append_sys_path=os.path.dirname(__file__)) + + def get_class_names(self): + """ Returns the class names in the library. """ + return self.request32('get_class_names') + + def get_number_of_devices_connected(self): + """ + Returns the number of LCDrivers connected to the computer. device_number + in other functions is in the range 0 to (number of LCDrivers - 1). + + Returns + ------- + int + the number of LC drivers connected to the computer + """ + return self.request32('get_number_of_devices_connected') + + def get_serial_number(self, device_number=0): + """ + Gets the serial number of the active device. + + Parameters + ---------- + device_number : int + Index of the LCDriver + + Returns + ------- + string + Serial number of LCDriver + """ + return self.request32('get_serial_number', device_number) + + def get_max_voltage(self): + """ + Gets the maximal possible output value for the LC driver. + + Returns + ------- + float + Max voltage + """ + return self.request32('get_max_voltage') + + def set_dac_voltage(self, V, ch_number, device_number=0): + """ + Sets the output voltage in volts. + + Parameters + ---------- + V : float + output voltage + ch_number : int + channel number 0,1,2,3 or 4 + device_number : int + Index of the LCDriver + + Returns + ------- + bool + True if command is sent successfully + """ + return self.request32('set_dac_voltage', float(V), int(ch_number), int(device_number)) + + def set_triggers(self, out0external, out1external, out2external, out3external, + device_number=0): + """ + Set the external trigger active for the outputs and for the device number device_number. + + Only possible to use if the trigger option is available on your LCDDriver! + + Parameters + ---------- + out0external : bool + channel 0 external trigger + out1external : bool + channel 1 external trigger + out2external : bool + channel 2 external trigger + out3external : bool + channel 3 external trigger + + Returns + ------- + bool + True if command is sent successfully + """ + return self.request32('set_triggers', + out0external, + out1external, + out2external, + out3external, + device_number) diff --git a/PYME/Acquire/Hardware/ARCoptix/lcserver32.py b/PYME/Acquire/Hardware/ARCoptix/lcserver32.py new file mode 100644 index 000000000..8a8d8a676 --- /dev/null +++ b/PYME/Acquire/Hardware/ARCoptix/lcserver32.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- + +# Server class for LC Driver + +from msl.loadlib import Server32 +import clr # need this to import types from system +from System import Double, Byte, Boolean + +class LCServer(Server32): + def __init__(self, host, port, quiet, **kwargs): + """ + A wrapper around 32-bit LCDriver.dll library. + + Parameters + ---------- + host : str + The IP address of the server. + port : int + The port to open on the server. + quiet : bool + Whether to hide :data:`sys.stdout` messages from the server. + """ + Server32.__init__(self, 'C:\Program Files\ARCoptix\ARCoptix LC Driver 1.2\LCDriver.dll', + 'net', host, port, quiet) + # Need True flag to always open as if we might connect multiple LC driver devices + self.lcdriver = self.lib.ARCoptix.LCdriver.LCdriver(True) + + def get_class_names(self): + """ Returns the class names in the library. """ + return ';'.join(str(name) for name in self.assembly.GetTypes()).split(';') + + def get_number_of_devices_connected(self): + """ + Returns the number of LCDrivers connected to the computer. device_number + in other functions is in the range 0 to (number of LCDrivers - 1). + + Returns + ------- + int + the number of LC drivers connected to the computer + """ + return self.lcdriver.GetNumberOfDevicesConnected() + + def get_serial_number(self, device_number=0): + """ + Gets the serial number of the active device. + + Parameters + ---------- + device_number : int + Index of the LCDriver + + Returns + ------- + string + Serial number of LCDriver + """ + return self.lcdriver.GetSerialNumber(device_number) + + def get_max_voltage(self): + """ + Gets the maximal possible output value for the LC driver. + + Returns + ------- + float + Max voltage + """ + return self.lcdriver.GetMaxVoltage() + + def set_dac_voltage(self, V, ch_number, device_number=0): + """ + Sets the output voltage in volts. + + Parameters + ---------- + V : float + output voltage + ch_number : int + channel number 0,1,2,3 or 4 + device_number : int + Index of the LCDriver + + Returns + ------- + bool + True if command is sent successfully + """ + max_volts = self.get_max_voltage() + if V > max_volts: + V = max_volts + elif V < 0.0: + V = 0.0 + return self.lcdriver.SetDACVoltage(Double(V), Byte(ch_number), device_number) + + def set_triggers(self, out0external, out1external, out2external, out3external, + device_number=0): + """ + Set the external trigger active for the outputs and for the device number device_number. + + Only possible to use if the trigger option is available on your LCDDriver! + + Parameters + ---------- + out0external : bool + channel 0 external trigger + out1external : bool + channel 1 external trigger + out2external : bool + channel 2 external trigger + out3external : bool + channel 3 external trigger + + Returns + ------- + bool + True if command is sent successfully + """ + return self.lcdriver.SetTriggers(Boolean(out0external), + Boolean(out1external), + Boolean(out2external), + Boolean(out3external), + device_number) \ No newline at end of file diff --git a/PYME/Acquire/Hardware/AndorIXon/.cvsignore b/PYME/Acquire/Hardware/AndorIXon/.cvsignore deleted file mode 100644 index 091955bb2..000000000 --- a/PYME/Acquire/Hardware/AndorIXon/.cvsignore +++ /dev/null @@ -1,8 +0,0 @@ -__init__.pyc -AndorCam.pyc -AndorIXon.pyo -__init__.pyo -AndorIXon.pyc -AndorControlFrame.pyc -AndorControlFrame.pyo -AndorCam.pyo diff --git a/PYME/Acquire/Hardware/AndorIXon/AndorCam.py b/PYME/Acquire/Hardware/AndorIXon/AndorCam.py index 2478b7f1f..c4e2bd598 100755 --- a/PYME/Acquire/Hardware/AndorIXon/AndorCam.py +++ b/PYME/Acquire/Hardware/AndorIXon/AndorCam.py @@ -35,7 +35,13 @@ if arch == '32bit': _stdcall_libraries['ATMCD32D'] = WinDLL('ATMCD32D') else: - _stdcall_libraries['ATMCD32D'] = WinDLL('atmcd64d') + try: + _stdcall_libraries['ATMCD32D'] = WinDLL('atmcd64d') + except OSError: + # see https://stackoverflow.com/questions/59330863/cant-import-dll-module-in-python + # winmode=0 enforces windows default dll search mechanism including searching the path set + # necessary since python 3.8.x + _stdcall_libraries['ATMCD32D'] = WinDLL('atmcd64d',winmode=0) from ctypes.wintypes import ULONG, DWORD, BOOL, BYTE, WORD, UINT, HANDLE, HWND else: _stdcall_libraries['ATMCD32D'] = CDLL('libandor.so') diff --git a/PYME/Acquire/Hardware/AndorIXon/AndorControlFrame.py b/PYME/Acquire/Hardware/AndorIXon/AndorControlFrame.py index 133c3ca46..d4ff25c6a 100755 --- a/PYME/Acquire/Hardware/AndorIXon/AndorControlFrame.py +++ b/PYME/Acquire/Hardware/AndorIXon/AndorControlFrame.py @@ -48,7 +48,7 @@ def create(parent): wxID_ANDORFRAMESTATICTEXT3, wxID_ANDORFRAMESTATICTEXT4, wxID_ANDORFRAMESTATICTEXT5, wxID_ANDORFRAMESTATICTEXT6, wxID_ANDORFRAMETCCDTEMP, wxID_ANDORFRAMETEMGAIN, -] = [wx.NewId() for _init_ctrls in range(24)] +] = [wx.NewIdRef() for _init_ctrls in range(24)] from PYME.ui import manualFoldPanel as afp diff --git a/PYME/Acquire/Hardware/AndorIXon/AndorIXon.py b/PYME/Acquire/Hardware/AndorIXon/AndorIXon.py index 94f1ab1e1..e25484392 100755 --- a/PYME/Acquire/Hardware/AndorIXon/AndorIXon.py +++ b/PYME/Acquire/Hardware/AndorIXon/AndorIXon.py @@ -27,6 +27,7 @@ import sys from PYME.IO import MetaDataHandler from PYME.Acquire.Hardware import ccdCalibrator +from PYME.Acquire.Hardware import camera_noise #import example #import scipy @@ -37,54 +38,19 @@ #import threading -noiseProperties = { -1823 : { - 'ReadNoise' : 109.8, - 'ElectronsPerCount' : 27.32, - 'NGainStages' : 536, - 'ADOffset' : 971, - 'DefaultEMGain' : 150, - 'SaturationThreshold' : (2**14 -1) - }, -5414 : { - 'ReadNoise' : 61.33, - 'ElectronsPerCount' : 25.24, - 'NGainStages' : 536, - 'ADOffset' : 413, - 'DefaultEMGain' : 90, - 'SaturationThreshold' : (2**14 -1) - }, -7863 : { #Gain setting of 3 - 'ReadNoise' : 88.1, - 'ElectronsPerCount' : 4.99, - 'NGainStages' : 536, - 'ADOffset' : 203, - 'DefaultEMGain' : 90, - 'SaturationThreshold' : 5.4e4#(2**16 -1) - }, -7546 : { - # preamp: currently using most sensitive setting (default according to docs) - # if I understand the code correctly the fastest Horizontal Shift Speed will be selected - # which should be 17 MHz for this camera; therefore using 17 MHz data - 'ReadNoise' : 85.23, - 'ElectronsPerCount' : 4.82, - 'NGainStages' : 536, # relevant? - 'ADOffset' : 150, # from test measurement at EMGain 85 (realgain ~30) - 'DefaultEMGain' : 85, # we start carefully and can bumb this later to be in the vicinity of 30 - 'SaturationThreshold' : (2**16 -1) # this cam has 16 bit data - }, -} - -preamp_gains = { - 1823 : 0, - 5414 : 0, - 7863 : 2, -} - from PYME.Acquire.Hardware.Camera import Camera class iXonCamera(Camera): + """ Andor iXon camera class + + Note that the camera will initialize in the default EM gain mode (0) + with EMGain of 0, and the shutter open. + """ numpy_frames=False + @property + def _gain_mode(self): + return 'Preamp Gain %d' % self.preampGain + #define a couple of acquisition modes #MODE_CONTINUOUS = 5 @@ -130,7 +96,7 @@ def __init__(self, boardNum=0): else: self.initialised = True - self.noiseProps = noiseProperties[self.GetSerialNumber()] + #self.noiseProps = noiseProperties[self.GetSerialNumber()] #get the CCD size ccdWidth = ac.GetDetector.argtypes[0]._type_() @@ -206,7 +172,8 @@ def __init__(self, boardNum=0): #set the preamp gain if we have data for our camera, otherwise default to highest # NOTE: this is important as other software may have left it in an undefined state - self.preampGain = preamp_gains.get(self.GetSerialNumber(), 2) #gain of "3" + # NOTE: We cheat a bit here and store this in noise_properties + self.preampGain = camera_noise.noise_properties.get(self.GetSerialNumber(), {}).get('default_preamp_gain',2) ret = ac.SetPreAmpGain(self.preampGain) if not ret == ac.DRV_SUCCESS: raise RuntimeError('Error setting Preamp gain: %s' % ac.errorCodes[ret]) @@ -222,9 +189,7 @@ def __init__(self, boardNum=0): if not ret == ac.DRV_SUCCESS: raise RuntimeError('Error setting image size: %s' % ac.errorCodes[ret]) - ret = ac.SetEMGainMode(0) - if not ret == ac.DRV_SUCCESS: - raise RuntimeError('Error setting EM Gain Mode: %s' % ac.errorCodes[ret]) + self.SetEMGainMode(0) # use the default self.EMGain = 0 #start with zero EM gain ret = ac.SetEMCCDGain(self.EMGain) @@ -247,7 +212,37 @@ def __init__(self, boardNum=0): print(('Error setting shutter: %s' % ac.errorCodes[ret])) + def SetEMGainMode(self, mode=0): + """Change the calibration mode used for EMGain values + + See Andor manual / SDK for more information. + + NOTE: Use anything other than mode 0 (default) with caution as mode 0 is assumed + by the gain calibration and reporting functionality in PYMEAcquire. This code replaces + and removes the need for the more advanced modes in the SDK, which are not available for all cameras. + If you do use anything other than mode 0, ensure that gain is set at startup (immediately after creating the + camera object) and does not change between sessions. If changing the gain mode after + you have already calibrated the camera, ensure that you run a re-calibration after + changing the gain mode. + + Parameters + ---------- + mode : int, optional + 0: The EM Gain is controlled by settings in the range 0-255. Default mode + 1: The EM Gain is controlled by settings in the range 0-4095 + 2: Linear mode + 3: Real EM gain + + Raises + ------ + RuntimeError + if the mode cannot be set + + """ + ret = ac.SetEMGainMode(mode) + if not ret == ac.DRV_SUCCESS: + raise RuntimeError('Error setting EM Gain Mode: %s' % ac.errorCodes[ret]) def _InitSpeedInfo(self): #temporary vars for function returns @@ -461,7 +456,7 @@ def StartExposure(self): self._GetAcqTimings() self._GetBufferSize() - eventLog.logEvent('StartAq', '') + self._log_exposure_start() ret = ac.StartAcquisition() if not ret == ac.DRV_SUCCESS: raise RuntimeError('Error starting acquisition: %s' % ac.errorCodes[ret]) @@ -690,9 +685,9 @@ def GetHeadModel(self): ac.GetHeadModel(hm) return hm.value.decode() - @property - def noise_properties(self): - return self.noiseProps + #@property + #def noise_properties(self): + # return self.noiseProps def GenStartMetadata(self, mdh): if self.active: #we are active -> write metadata @@ -720,7 +715,7 @@ def GenStartMetadata(self, mdh): #these should really be read from a configuration file #hard code them here until I get around to it #current values are at 10Mhz using e.m. amplifier - np = noiseProperties[self.GetSerialNumber()] + np = self.noise_properties mdh.setEntry('Camera.ReadNoise', np['ReadNoise']) mdh.setEntry('Camera.NoiseFactor', 1.41) mdh.setEntry('Camera.ElectronsPerCount', np['ElectronsPerCount']) @@ -729,10 +724,11 @@ def GenStartMetadata(self, mdh): if not realEMGain is None: mdh.setEntry('Camera.TrueEMGain', realEMGain) - def __getattr__(self, name): - if name in list(self.noiseProps.keys()): - return self.noiseProps[name] - else: raise AttributeError(name) # <<< DON'T FORGET THIS LINE !! + # we should not need this + #def __getattr__(self, name): + # if name in list(self.noiseProps.keys()): + # return self.noiseProps[name] + # else: raise AttributeError(name) # <<< DON'T FORGET THIS LINE !! def __del__(self): diff --git a/PYME/Acquire/Hardware/AndorNeo/AndorNeo.py b/PYME/Acquire/Hardware/AndorNeo/AndorNeo.py index 370306cb4..af127b8fe 100644 --- a/PYME/Acquire/Hardware/AndorNeo/AndorNeo.py +++ b/PYME/Acquire/Hardware/AndorNeo/AndorNeo.py @@ -454,13 +454,13 @@ def SetCOC(*args): pass def StartExposure(self): - #make sure no acquisiton is running + #make sure no acquisition is running self.StopAq() self._temp = self.SensorTemperature.getValue() self._frameRate = self.FrameRate.getValue() self.tKin = 1.0 / self._frameRate - eventLog.logEvent('StartAq', '') + self._log_exposure_start() self._flush() self.InitBuffers() self.AcquisitionStart() diff --git a/PYME/Acquire/Hardware/AndorNeo/AndorNeoControlFrame.py b/PYME/Acquire/Hardware/AndorNeo/AndorNeoControlFrame.py index ee071cb63..257485cf7 100644 --- a/PYME/Acquire/Hardware/AndorNeo/AndorNeoControlFrame.py +++ b/PYME/Acquire/Hardware/AndorNeo/AndorNeoControlFrame.py @@ -48,7 +48,7 @@ def create(parent): wxID_ANDORFRAMESTATICTEXT3, wxID_ANDORFRAMESTATICTEXT4, wxID_ANDORFRAMESTATICTEXT5, wxID_ANDORFRAMESTATICTEXT6, wxID_ANDORFRAMETCCDTEMP, wxID_ANDORFRAMETEMGAIN, -] = [wx.NewId() for _init_ctrls in range(24)] +] = [wx.NewIdRef() for _init_ctrls in range(24)] diff --git a/PYME/Acquire/Hardware/AndorNeo/AndorZyla.py b/PYME/Acquire/Hardware/AndorNeo/AndorZyla.py index db403571d..0610d08d5 100644 --- a/PYME/Acquire/Hardware/AndorNeo/AndorZyla.py +++ b/PYME/Acquire/Hardware/AndorNeo/AndorZyla.py @@ -46,97 +46,26 @@ class AndorBase(SDK3Camera, CameraMapMixin): numpy_frames=1 + supports_software_trigger = True #MODE_CONTINUOUS = 1 #MODE_SINGLE_SHOT = 0 - PixelEncodingForGain = {'12-bit (low noise)': 'Mono12', - '12-bit (high well capacity)': 'Mono12', - '16-bit (low noise & high well capacity)' : 'Mono16' - } - - _noise_properties = { - 'VSC-00954': { - '12-bit (low noise)': { - 'ReadNoise' : 1.1, - 'ElectronsPerCount' : 0.28, - 'ADOffset' : 100, # check mean (or median) offset - 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit - }, - '12-bit (high well capacity)': { - 'ReadNoise' : 5.96, - 'ElectronsPerCount' : 6.97, - 'ADOffset' : 100, - 'SaturationThreshold' : 2**11-1#(2**16 -1) - }, - '16-bit (low noise & high well capacity)': { - 'ReadNoise' : 1.33, - 'ElectronsPerCount' : 0.5, - 'ADOffset' : 100, - 'SaturationThreshold' : (2**16 -1) - }}, - 'VSC-02858': { - '12-bit (low noise)': { - 'ReadNoise' : 1.19, - 'ElectronsPerCount' : 0.3, - 'ADOffset' : 100, # check mean (or median) offset - 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit - }, - '12-bit (high well capacity)': { - 'ReadNoise' : 6.18, - 'ElectronsPerCount' : 7.2, - 'ADOffset' : 100, - 'SaturationThreshold' : 2**11-1#(2**16 -1) - }, - '16-bit (low noise & high well capacity)': { - 'ReadNoise' : 1.42, - 'ElectronsPerCount' : 0.5, - 'ADOffset' : 100, - 'SaturationThreshold' : (2**16 -1) - }}, - 'VSC-02698': { - '12-bit (low noise)': { - 'ReadNoise' : 1.16, - 'ElectronsPerCount' : 0.26, - 'ADOffset' : 100, # check mean (or median) offset - 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit - }, - '12-bit (high well capacity)': { - 'ReadNoise' : 6.64, - 'ElectronsPerCount' : 7.38, - 'ADOffset' : 100, - 'SaturationThreshold' : 2**11-1#(2**16 -1) - }, - '16-bit (low noise & high well capacity)': { - 'ReadNoise' : 1.36, - 'ElectronsPerCount' : 0.49, - 'ADOffset' : 100, - 'SaturationThreshold' : (2**16 -1) - }}} - + ZylaPixelEncodingForGain = {'12-bit (low noise)': 'Mono12', + '12-bit (high well capacity)': 'Mono12', + '16-bit (low noise & high well capacity)' : 'Mono16' + } + @property - def noise_properties(self): - """return the noise properties for a the given camera - - TODO: make this look in config, rather than storing noise properties here - """ - try: - return self._noise_properties[self.GetSerialNumber()][self.GetSimpleGainMode()] - except KeyError: - logger.warn('camera specific noise props not found - using default noise props') - return {'ReadNoise' : 1.1, - 'ElectronsPerCount' : 0.28, - 'ADOffset' : 100, # check mean (or median) offset - 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit, - } - - + def _gain_mode(self): + return self.GetSimpleGainMode() + # this class is compatible with the ATEnum object properties that are used in ZylaControlPanel # we use it as a higher level alternative to setting gainmode and encoding directly class SimpleGainEnum(object): def __init__(self, cam): self.cam = cam - self.gainmodes = cam.PixelEncodingForGain.keys() - self.propertyName = 'SimpleGainModes' + self.gainmodes = cam.SimplePreAmpGainControl.getAvailableValues() + self.propertyName = 'SimpleGainMode' def getAvailableValues(self): return self.gainmodes @@ -148,8 +77,10 @@ def getString(self): return self.cam.GetSimpleGainMode() + def __init__(self, camNum): #define properties + self.CameraAcquiring = ATBool() self.SensorCooling = ATBool() @@ -226,13 +157,36 @@ def __init__(self, camNum): def Init(self): SDK3Camera.Init(self) - #set some intial parameters - #self.setNoisePropertiesByCam(self.GetSerialNumber()) + # figure out preamp gain modes for this camera type + if not self.CameraModel.getValue().startswith('SIM'): + # Special case for Sona cams + self.PixelEncodingForGain = {} + for mode in self.SimplePreAmpGainControl.getAvailableValues(): + if mode.startswith('12'): + self.PixelEncodingForGain[mode] = 'Mono12' + elif mode.startswith('16'): + self.PixelEncodingForGain[mode] = 'Mono16' + else: + raise RuntimeError('PixelEncodingForGain mode "%s" unknown bit depth (neither 12 nor 16 bit)' % (mode)) + else: + # Assume Zyla + self.PixelEncodingForGain = self.ZylaPixelEncodingForGain + + + # this instance is compatible with use in Zylacontrolpanel + # note we make this only once the camera has been initialised and PixelEncodingForGain been made + self.SimpleGainEnumInstance = self.SimpleGainEnum(self) + + self.FrameCount.setValue(1) self.CycleMode.setString(u'Continuous') #need this to get full frame rate - self.Overlap.setValue(True) + try: + self.Overlap.setValue(True) + except: + logger.info("error setting overlap mode") + pass # we use a try block as this will allow us to use the SDK software cams for simple testing try: @@ -258,12 +212,21 @@ def Init(self): self.TriggerMode.setString('Internal') - self.SensorCooling.setValue(True) - #self.TemperatureControl.setString('-30.00') + try: + self.SensorCooling.setValue(True) + except: + logger.info("error setting cooling mode") + pass + + try: + TCModes = self.TemperatureControl.getAvailableValues() + self.TemperatureControl.setString(TCModes[0]) + except: + pass + #self.PixelReadoutRate.setIndex(1) # test if we have only fixed ROIs self._fixed_ROIs = not self.FullAOIControl.isImplemented() or not self.FullAOIControl.getValue() - #self.noiseProps = self.baseNoiseProps[self.GetSimpleGainMode()] self.SetIntegTime(.100) @@ -391,9 +354,9 @@ def _pollBuffer(self): buf = self.queuedBuffers.get() self.nQueued -= 1 if not buf.ctypes.data == ctypes.addressof(pData.contents): - print((ctypes.addressof(pData.contents), buf.ctypes.data)) + #print((ctypes.addressof(pData.contents), buf.ctypes.data)) #self.camLock.release() - raise RuntimeError('Returned buffer not equal to expected buffer') + raise RuntimeError('Returned buffer not equal to expected buffer (%s)' % str((ctypes.addressof(pData.contents), buf.ctypes.data))) #print 'Returned buffer not equal to expected buffer' self.fullBuffers.put(buf) @@ -487,6 +450,7 @@ def GetAcquisitionMode(self): def FireSoftwareTrigger(self): self.SoftwareTrigger() + self._log_exposure_start() @property def contMode(self): @@ -590,7 +554,7 @@ def Shutdown(self): def StartExposure(self): - #make sure no acquisiton is running + #make sure no acquisition is running self.StopAq() self._temp = self.SensorTemperature.getValue() self._frameRate = self.FrameRate.getValue() @@ -601,7 +565,7 @@ def StartExposure(self): self.hardware_overflowed = False self._n_timeouts = 0 #logger.debug('StartAq') - eventLog.logEvent('StartAq', '') + self._log_exposure_start() self._flush() self.InitBuffers() self.AcquisitionStart() @@ -653,8 +617,13 @@ def GenStartMetadata(self, mdh): mdh.setEntry('Camera.ROIOriginY', y1) mdh.setEntry('Camera.ROIWidth', x2 - x1) mdh.setEntry('Camera.ROIHeight', y2 - y1) - #mdh.setEntry('Camera.StartCCDTemp', self.GetCCDTemp()) + #mdh.setEntry('Camera.StartCCDTemp', self.GetCCDTemp()) + # these make sense with the Sona choices but should also be ok for the Zyla + mdh.setEntry('Camera.TemperatureControl', self.TemperatureControl.getString()) + mdh.setEntry('Camera.TemperatureStatus', self.TemperatureStatus.getString()) + mdh.setEntry('Camera.SensorTemperature', self.SensorTemperature.getValue()) + # pick up noise settings for gain mode np = self.noise_properties mdh.setEntry('Camera.ReadNoise', np['ReadNoise']) @@ -714,6 +683,13 @@ def GetFPS(self): #return self.FrameRate.getValue() return self._frameRate + def TemperatureStatusText(self): + # TODO - rename (potentially when considering PEP8ing / genral camera interface refactor) + # TODO - uniform interface for device status text across devices - should be more generic than temperature + return "Zyla target T %s - %s" % (self.TemperatureControl.getString(), + self.TemperatureStatus.getString()) + + def __del__(self): self.Shutdown() #self.compT.kill = True @@ -737,7 +713,7 @@ def __init__(self, camNum): self.TemperatureControl = ATEnum() self.TemperatureStatus = ATEnum() self.SimplePreAmpGainControl = ATEnum() - self.SimpleGainEnumInstance = self.SimpleGainEnum(self) # this instance is compatible with use in Zylacontrolpanel + self.BitDepth = ATEnum() self.ActualExposureTime = ATFloat() @@ -757,6 +733,7 @@ def __init__(self, camNum): self.FirmwareVersion = ATString() AndorBase.__init__(self,camNum) + class AndorSim(AndorBase): def __init__(self, camNum): diff --git a/PYME/Acquire/Hardware/AndorNeo/SDK3.py b/PYME/Acquire/Hardware/AndorNeo/SDK3.py index a07dd7c00..18036d771 100644 --- a/PYME/Acquire/Hardware/AndorNeo/SDK3.py +++ b/PYME/Acquire/Hardware/AndorNeo/SDK3.py @@ -43,8 +43,15 @@ _stdcall_libraries['ATCORE'] = ctypes.WinDLL('atcore') _stdcall_libraries['ATUTIL'] = ctypes.WinDLL('atutility') else: - _stdcall_libraries['ATCORE'] = ctypes.WinDLL('atcore') - _stdcall_libraries['ATUTIL'] = ctypes.WinDLL('atutility') + try: + _stdcall_libraries['ATCORE'] = ctypes.WinDLL('atcore') + _stdcall_libraries['ATUTIL'] = ctypes.WinDLL('atutility') + except OSError: + # see https://stackoverflow.com/questions/59330863/cant-import-dll-module-in-python + # winmode=0 enforces windows default dll search mechanism including searching the path set + # necessary since python 3.8.x + _stdcall_libraries['ATCORE'] = ctypes.WinDLL('atcore',winmode=0) + _stdcall_libraries['ATUTIL'] = ctypes.WinDLL('atutility',winmode=0) CALLBACKTYPE = ctypes.WINFUNCTYPE(c_int, AT_H, POINTER(AT_WC), c_void_p) else: _stdcall_libraries['ATCORE'] = ctypes.CDLL('atcore.so') @@ -297,4 +304,4 @@ def dllFunc(name, args = [], argnames = [], lib='ATCORE'): dllFunc('AT_ConvertBufferUsingMetadata', [POINTER(AT_U8), POINTER(AT_U8), AT_64, STRING], lib='ATUTIL') #Initialize the utility library -InitialiseUtilityLibrary() \ No newline at end of file +InitialiseUtilityLibrary() diff --git a/PYME/Acquire/Hardware/AndorNeo/SDK3Cam.py b/PYME/Acquire/Hardware/AndorNeo/SDK3Cam.py index cc5cd9640..930819633 100644 --- a/PYME/Acquire/Hardware/AndorNeo/SDK3Cam.py +++ b/PYME/Acquire/Hardware/AndorNeo/SDK3Cam.py @@ -149,7 +149,7 @@ def __init__(self, camNum): def Init(self): - print('Foo') + #print('Foo') self.handle = SDK3.Open(self.camNum) self.connectProperties() diff --git a/PYME/Acquire/Hardware/AndorNeo/ZylaControlPanel.py b/PYME/Acquire/Hardware/AndorNeo/ZylaControlPanel.py index 5e13e8dd3..19687e5a9 100644 --- a/PYME/Acquire/Hardware/AndorNeo/ZylaControlPanel.py +++ b/PYME/Acquire/Hardware/AndorNeo/ZylaControlPanel.py @@ -8,14 +8,26 @@ import wx class EnumControl(wx.Panel): - def __init__(self, parent, target): + def __init__(self, parent, target, display_name=None): wx.Panel.__init__(self, parent) self.scope = parent.scope self.parent = parent self.target = target + # this keyword allows us to override unwieldy property Names + if display_name is None: + self.display_name = target.propertyName + else: + self.display_name = display_name + + # TODO - do we really need to support read only enums???? + try: + self._read_only = target.isReadOnly() + except AttributeError: + self._read_only = False + hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, target.propertyName), 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + hsizer.Add(wx.StaticText(self, -1, self.display_name), 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) self.cChoice = wx.Choice(self, -1, size = [100,-1]) self.cChoice.Bind(wx.EVT_CHOICE, self.onChange) @@ -26,10 +38,11 @@ def __init__(self, parent, target): self.SetSizerAndFit(hsizer) def onChange(self, event=None): - self.scope.frameWrangler.stop() - self.target.setString(self.cChoice.GetStringSelection()) - self.scope.frameWrangler.start() - self.parent.update() + if not self._read_only: + self.scope.frameWrangler.stop() + self.target.setString(self.cChoice.GetStringSelection()) + self.scope.frameWrangler.start() + self.parent.update() def update(self): choices = list(self.target.getAvailableValues()) @@ -73,16 +86,20 @@ def __init__(self, parent, cam, scope): self.cam = cam self.scope = scope - self.ctrls = [EnumControl(self, cam.SimpleGainEnumInstance), + self.ctrls = [EnumControl(self, cam.SimpleGainEnumInstance), # use enum class as it also sets pixel encoding EnumControl(self, cam.PixelReadoutRate), BoolControl(self, cam.SpuriousNoiseFilter), BoolControl(self, cam.StaticBlemishCorrection), EnumControl(self, cam.CycleMode),] - + + + if len(cam.TemperatureControl.getAvailableValues()) > 1: # we only add this if there is a real choice + self.ctrls.append(EnumControl(self, cam.TemperatureControl)) + self._init_ctrls() def update(self): for c in self.ctrls: c.update() - + diff --git a/PYME/Acquire/Hardware/Camera.py b/PYME/Acquire/Hardware/Camera.py index bf3ad9ecb..049a49ce7 100644 --- a/PYME/Acquire/Hardware/Camera.py +++ b/PYME/Acquire/Hardware/Camera.py @@ -29,6 +29,12 @@ from threading import Lock from PYME.IO import MetaDataHandler +from PYME.Acquire import eventLog + +from PYME import config +from . import camera_noise +import warnings +from PYME.contrib import dispatch import numpy as np import logging logger = logging.getLogger(__name__) @@ -54,7 +60,14 @@ def check_mapexists(mdh, type='dark', fill=True): local_path = os.path.join(nameUtils.getCalibrationDir(mdh['Camera.SerialNumber']), mapfn) cluster_path = 'CALIBRATION/%s/%s' % (mdh['Camera.SerialNumber'], mapfn) - if clusterIO.exists(cluster_path): + try: + cluster_map_exists = clusterIO.exists(cluster_path) + except OSError: + # The cluster might not be running, if the cluster is not running, we clusterIO.exists throws an error. + # catch this to allow offline spooling (see issue #1141) + cluster_map_exists = False + + if cluster_map_exists: c_path = 'PYME-CLUSTER://%s/%s' % (clusterIO.local_serverfilter, cluster_path) if fill: mdh[id] = c_path @@ -98,12 +111,22 @@ def fill_camera_map_metadata(self, mdh): class Camera(object): # Frame format - PYME previously supported frames in a custom format, but numpy_frames should always be true for current code numpy_frames = 1 #Frames are delivered as numpy arrays. + + # what data type is returned by the camera? For full support it is recommended to use + # a format supported in `PYME.IO.PZFFormat` + dtype = np.dtype('uint16') # uint16 is the default for PYME, and historically the only supported dtype of PYMEAcquire # Acquisition modes MODE_SINGLE_SHOT = 0 MODE_CONTINUOUS = 1 MODE_SOFTWARE_TRIGGER = 2 MODE_HARDWARE_TRIGGER = 3 + MODE_HARDWARE_START_TRIGGER = 4 + + # Does this camera support a software trigger? + # Should be overridden in derived classes if the camera supports software triggering, + # and the derived class should also implement the FireSoftwareTrigger method + supports_software_trigger = False def __init__(self, *args, **kwargs): @@ -127,10 +150,16 @@ def __init__(self, *args, **kwargs): self.active = True # Should the camera write its metadata? + self._saturation_threshold = (2**16) - 1 # default saturation threshold, if not provided in noise_properties + # Register as a provider of metadata (record camera settings) # this is important so that the camera settings get recorded MetaDataHandler.provideStartMetadata.append(self.GenStartMetadata) + # create flag for idle state, allowing different behavior between spooling + self._idle = False + self.on_idle_change = dispatch.Signal(['idle']) + def Init(self): """ Optional intialization function. Also called from the init script. @@ -153,6 +182,10 @@ def ExpReady(self): ------- exposureReady : bool True if there are frames waiting + + Notes + ----- + For cameras with an "Idle" mode, this should return False when in idle mode. """ @@ -588,25 +621,58 @@ def GetAcquisitionMode(self): Returns ------- int - One of self.MODE_CONTINUOUS, self.MODE_SINGLE_SHOT + One of: + MODE_CONTINUOUS + camera is free-running using internal timing + MODE_SINGLE_SHOT + captures single frame on acquisition start + MODE_SOFTWARE_TRIGGER + captures a single frame every time a software trigger is + sent. See `FireSoftwareTrigger` + MODE_HARDWARE_TRIGGER + captures a single frame every time an external hardware + trigger is received by the camera + MODE_HARDWARE_START_TRIGGER + camera waits for an external hardware trigger and enters + free-running internal timing mode once received See Also -------- SetAcquisitionMode + + Notes + ----- + Calling code should compare against the constants defined on the Camera class, rather than their integer values + """ raise NotImplementedError('Should be implemented in derived class.') def SetAcquisitionMode(self, mode): """ - Set the readout mode of the Camera object. PYME currently supports two - modes: single shot, where the camera takes one image, and then a new + Set the readout mode of the Camera object. PYME currently supports + several modes; different options can be useful for timing. The primary + modes are single shot, where the camera takes one image, and then a new exposure has to be manually triggered, or continuous / free running, - where the camera runs as fast as it can until we tell it to stop. + where the camera runs as fast as it can until we tell it to stop. Not all cameras support all modes. All cameras should support `MODE_CONTINUOUS` and `MODE_SINGLE_SHOT`, but care should be taken when using the other modes (i.e. usage probably need to be restricted to code which knows what type of camera it's dealing with). TODO - add a mechanism for cameras to report which modes they support. + Parameters ---------- mode : int - One of self.MODE_CONTINUOUS, self.MODE_SINGLE_SHOT + One of: + MODE_CONTINUOUS + camera is free-running using internal timing + MODE_SINGLE_SHOT + captures single frame on acquisition start + MODE_SOFTWARE_TRIGGER + captures a single frame every time a software trigger is + sent. See `FireSoftwareTrigger` + MODE_HARDWARE_TRIGGER + captures a single frame every time an external hardware + trigger is received by the camera + MODE_HARDWARE_START_TRIGGER + camera waits for an external hardware trigger and enters + free-running internal timing mode once received Returns ------- @@ -615,6 +681,10 @@ def SetAcquisitionMode(self, mode): See Also -------- GetAcquisitionMode + + Notes + ----- + Calling code should use the constants defined on the Camera class, rather than their integer values """ raise NotImplementedError('Should be implemented in derived class.') @@ -638,16 +708,26 @@ def SetActive(self, active=True): self.active = bool(active) + def _log_exposure_start(self): + """ + Log a StartAq event to the event log to facilitate timestamping of acquisition frames. + This should be called in the StartExposure and FireSoftwareTrigger methods of derived camera classes. + """ + eventLog.logEvent('StartAq', '') + def StartExposure(self): """ Starts an acquisition. + Must call _log_exposure_start to log the start of the acquisition. + Returns ------- int Success (0) or failure (-1) of initialization. """ raise NotImplementedError('Implemented in derived class.') + def StopAq(self): """ @@ -682,30 +762,44 @@ def GetBufferSize(self): """ raise NotImplementedError("Implemented in derived class.") + + @property + def _gain_mode(self): + """ + Should return a representation of the camera gain and/or preamp mode that can be used as a dictionary key. + Should be overriden in derived classes if they have any setable preamp gain modes to speak of. + This is used to look up noise properties by preamp setting, see noise_properties below. + """ + return 'fixed' + @property def noise_properties(self): """ + Return the noise properties for the given camera. This is a dictionary with the following entries - Returns - ------- + 'ReadNoise' : camera read noise as a standard deviation in units of photoelectrons (e-) + 'ElectronsPerCount' : AD conversion factor - how many electrons per ADU + 'NoiseFactor' : excess (multiplicative) noise factor 1.44 for EMCCD, 1 for standard CCD/sCMOS. See + doi: 10.1109/TED.2003.813462 + 'SaturationThreshold' : the full well capacity (in ADU) - a dictionary with the following entries: + and optionally - 'ReadNoise' : camera read noise as a standard deviation in units of photoelectrons (e-) - 'ElectronsPerCount' : AD conversion factor - how many electrons per ADU - 'NoiseFactor' : excess (multiplicative) noise factor 1.44 for EMCCD, 1 for standard CCD/sCMOS. See - doi: 10.1109/TED.2003.813462 + 'ADOffset' : the dark level (in ADU) + 'DefaultEMGain' : a sensible EM gain setting to use for localization recording - and optionally - 'ADOffset' : the dark level (in ADU) - 'DefaultEMGain' : a sensible EM gain setting to use for localization recording - 'SaturationThreshold' : the full well capacity (in ADU) - """ - - raise AttributeError('Implement in derived class') - + These are sourced from config files, referenced by camera serial number and gain mode. + See :py:mod:`PYME.Acquire.Hardware.camera_noise` for details. There can be camera to camera to camera variations in + the valid values and meanings of `self._gain_mode`. + """ + try: + return camera_noise.noise_properties[self.GetSerialNumber()]['noise_properties'][self._gain_mode] + except KeyError: # last resort is a runtime error - we can debate what the best solution is + # we could also look for a 'default' entry in the properties + raise RuntimeError('Camera specific noise props not found for serial no "%s" and preamp mode "%s". See PYME.Acquire.Hardware.camera_noise for info on how to setup your camera' + % (self.GetSerialNumber(),self._gain_mode)) def GetStatus(self): """ @@ -757,8 +851,14 @@ def GenStartMetadata(self, mdh): self.GetStatus() # Personal identification - mdh.setEntry('Camera.Name', self.GetName()) - mdh.setEntry('Camera.Model', self.GetHeadModel()) + try: + mdh.setEntry('Camera.Name', self.GetName()) + mdh.setEntry('Camera.Model', self.GetHeadModel()) + except NotImplementedError: + # cameras are not required to provide GetName(), GetHeadModel() + # and these metadata entries should not be depended on (might change). + pass + mdh.setEntry('Camera.SerialNumber', self.GetSerialNumber()) # Time @@ -787,7 +887,57 @@ def GenStartMetadata(self, mdh): mdh.setEntry('Camera.ROIOriginY', y1) mdh.setEntry('Camera.ROIWidth', x2 - x1) mdh.setEntry('Camera.ROIHeight', y2 - y1) - + + @property + def SaturationThreshold(self): + """ + Returns + ------- + int + the full well capacity (in ADU), typically 2^bitdepth - 1 + """ + try: + return self.noise_properties['SaturationThreshold'] + except (KeyError, RuntimeError): + return self._saturation_threshold + + def SetIdle(self, idle=True): + """ + Set the camera to idle mode. This allows cameras to support different behavior during + spooling vs while waiting to spool. + + Most cameras probably can (and probably should) ignore idle mode, as keeping the camera running between + acquisitions will lead to the most consistent temperature and noise behaviour. For cameras which either + have a *really* long exposure or involve hardware motion in constructing a frame (e.g. confocal scanners) + implementing an idle mode could be useful. + + Parameters + ---------- + idle : bool + True to set idle, False to exit idle + + Returns + ------- + None + """ + self._idle = bool(idle) + self.on_idle_change.send(self, idle=self._idle) + + def GetIdle(self): + """ + Get whether the camera is in idle mode. + + Returns + ------- + bool + True if in idle mode + """ + try: + return self._idle + except AttributeError: + logger.warning("Camera idle flag was not initialized - Did custom camera class call super().__init__() ?") + self._idle = False + return self._idle def Shutdown(self): """Shutdown and clean up the camera""" @@ -836,7 +986,7 @@ def StopLifePreview(*args): raise DeprecationWarning("Deprecated.") -# FIXME - move out of this file +# DEPRECATED, TODO - schedule for removal class MultiviewCameraMixin(object): def __init__(self, multiview_info, default_roi, camera_class): """ @@ -875,6 +1025,8 @@ def __init__(self, whatever_cool_cam_needs, multiview_info) For now, the 0th multiview ROI should be the upper-left most multiview ROI, in order to properly spoof the position to match up with the stage. See PYME.IO.MetaDataHandler.get_camera_roi_origin. """ + warnings.warn(DeprecationWarning('Use PYME.Acquire.Hardware.multiview.MultiviewWrapper instead')) + logger.warn('MultiviewMixin is deprecated - use PYME.Acquire.Hardware.multiview.MultiviewWrapper instead') self.camera_class = camera_class self.multiview_info = multiview_info self._channel_color = multiview_info['Multiview.ChannelColor'] @@ -1094,3 +1246,4 @@ def register_state_handlers(self, state_manager): lambda : [self.size_x, self.size_y], lambda p : self.ChangeMultiviewROISize(p[0], p[1]), True) + diff --git a/PYME/Acquire/Hardware/Coherent/OBIS.py b/PYME/Acquire/Hardware/Coherent/OBIS.py index b9bbcb613..801befc76 100644 --- a/PYME/Acquire/Hardware/Coherent/OBIS.py +++ b/PYME/Acquire/Hardware/Coherent/OBIS.py @@ -33,8 +33,9 @@ class CoherentOBISLaser(Laser): - power_controllable = True - def __init__(self, serial_port='COM8', turn_on=False, name='OBIS', init_power=5, **kwargs): + powerControlable = True + + def __init__(self, serial_port='COM8', turn_on=False, name='OBIS', init_power=5, reply_ok=False, **kwargs): """ Parameters @@ -47,10 +48,15 @@ def __init__(self, serial_port='COM8', turn_on=False, name='OBIS', init_power=5, Name of the laser init_power: float In units of mW + reply_ok: bool + Whether this laser is configured to reply 'OK' after each message or not. + Note this has been observed when communicating throughthe OBIS power supply + rather than directly with the head unit. kwargs """ self.serial_port = serial.Serial(serial_port, timeout=.1) self.lock = threading.Lock() + self._reply_OK = int(reply_ok) @@ -94,7 +100,7 @@ def query(self, command, lines_expected=1): with self.lock: self.serial_port.reset_input_buffer() self.serial_port.write(command) - reply = [self.serial_port.readline() for line in range(lines_expected)] + reply = [self.serial_port.readline() for line in range(lines_expected + self._reply_OK)] self.serial_port.reset_input_buffer() return reply diff --git a/PYME/Acquire/Hardware/Coherent/Sapphire.py b/PYME/Acquire/Hardware/Coherent/Sapphire.py new file mode 100644 index 000000000..ef11b2a55 --- /dev/null +++ b/PYME/Acquire/Hardware/Coherent/Sapphire.py @@ -0,0 +1,162 @@ +#!/usr/bin/python + +############### +# CoherentOBISLaser.py +# +# Copyright David Baddeley, 2012 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################ +import serial +import time +import threading +from PYME.Acquire.Hardware.lasers import Laser + +# Notes: +# Interlock defeat must be plugged into the analog interface +# Oddly, the CONTROL switch on the back panel must be set to +# LOCAL rather than REMOTE to use USB commands to do anything +# more than query states.\ +# Laser uses a virtual COM port, if device manager does not +# show laser as virtual COM port, download virtual COM port +# driver from FTDI, laser should show up as USB Serial Converter +# in device manager, check Load VC box in properties, Laser +# should now show up as virtual COM port + + +class CoherentSapphireLaser(Laser): + power_controllable = True + powerControlable = power_controllable + units='mW' + def __init__(self, serial_port='COM7', turn_on=False, name='Sapphire', init_power=5, **kwargs): + """ + + Parameters + ---------- + serial_port: str + serial port + turn_on: bool + Whether or not to turn on the laser on instantiating the class + name: str + Name of the laser + init_power: float + In units of mW + kwargs + """ + self.serial_port = serial.Serial(serial_port, baudrate=19200, timeout=.1) + self.lock = threading.Lock() + with self.lock: + self.serial_port.reset_input_buffer() + self.serial_port.write(b'?E\r\n') + echo_ret = self.serial_port.readlines() + self.serial_port.reset_input_buffer() + if any([b'Sapphire' in s for s in echo_ret]): + self._sapphire_prompt = 1 + else: + self._sapphire_prompt = 0 + + if any([b'?E' in s for s in echo_ret]): + self._echo = 1 + else: + self._echo = 0 + + self.SetPower(init_power) + self.MIN_POWER = 1e3 * float(self.query(b'?MINLP\r\n')[0]) + self.MAX_POWER = 1e3 * float(self.query(b'?MAXLP\r\n')[0]) + self.is_on = False + + # self.query(b'SYST:COMM:HAND OFF\r\n', lines_expected=0) + + Laser.__init__(self, name, turn_on, **kwargs) + + + + def query(self, command): + """ + Send serial command and return a set number of reply lines from the device before clearing the device outputs + + Parameters + ---------- + command: bytes + Command to send to the device. Must be complete, e.g. b'command\r\n' + + Returns + ------- + reply: list + list of lines retrieved from the device. Blank lines are possible + + Notes + ----- + serial.Serial.readlines method was not used because our device requires a wait until each line is read before + it writes the next line. + """ + with self.lock: + self.serial_port.reset_input_buffer() + self.serial_port.write(command) + reply = self.serial_port.readlines() + self.serial_port.reset_input_buffer() + if self._echo: + reply.pop(0) + if self._sapphire_prompt: + reply.pop(-1) + return reply + + + def IsOn(self): + # Would be nice to check, but there is a performance hit (this gets tracked as a scope state to update the GUI) + return self.is_on + + def TurnOn(self): + self.query(b'L=1\r\n') + self.is_on = True + + def TurnOff(self): + self.query(b'L=0\r\n') + # FIXME - would be nice to check this worked + self.is_on = False + + def check_set_power(self): + power = float(self.query(b'?SP\r\n')[0]) + return power + + def check_power_output(self): + power = float(self.query(b'?P\r\n')[0]) + return power + + def SetPower(self, power): + # set power + self.query(b'P=%f\r\n' % power) + # log actual set power + self.power = self.check_set_power() + + def GetMAX(self): + + return float(self.query(b'?MAXLP\r\n')[0]) + + def GetMIN(self): + + return float(self.query(b'?MINLP\r\n')[0]) + + def GetPower(self): + return self.power + + def Close(self): + print('Shutting down %s' % self.name) + self.TurnOff() + time.sleep(.1) + + def __del__(self): + self.Close() diff --git a/PYME/Acquire/Hardware/DMDGui.py b/PYME/Acquire/Hardware/DMDGui.py index 226bec581..5c95e855b 100644 --- a/PYME/Acquire/Hardware/DMDGui.py +++ b/PYME/Acquire/Hardware/DMDGui.py @@ -8,6 +8,9 @@ from PIL import Image import numpy as np +import logging +logger = logging.getLogger(__name__) + display_mode = [ 'DISP_MODE_IMAGE', #/* Static Image */ 'DISP_MODE_TEST_PTN', #/* Internal Test pattern */ @@ -64,7 +67,7 @@ def __init__(self, parent, scope, **kwargs): self.SetSizerAndFit(vsizer) def OnCDMD(self, event): - print("Set display mode to: %s" % self.cDMD.GetStringSelection()) + logger.debug("Set display mode to: %s" % self.cDMD.GetStringSelection()) def OnBSetmodeButton(self, event): if self.cDMD.GetStringSelection() == 'Image Sequence': @@ -104,7 +107,7 @@ def __init__(self, parent, lightcrafter, winid=-1): self.SetSizerAndFit(vsizer) def OnCDMDtp(self, event): - print("select pattern %s" % self.cDMDtp.GetStringSelection()) + logger.debug("select pattern %s" % self.cDMDtp.GetStringSelection()) def OnBSendButton(self, event): self.lc.SetTestPattern(test_pattern.index(self.cDMDtp.GetStringSelection())) diff --git a/PYME/Acquire/Hardware/DigiData/.cvsignore b/PYME/Acquire/Hardware/DigiData/.cvsignore deleted file mode 100755 index e1bb0f82b..000000000 --- a/PYME/Acquire/Hardware/DigiData/.cvsignore +++ /dev/null @@ -1,9 +0,0 @@ -__init__.pyc -DigiDataClient.pyc -axDD132x.pyc -__init__.pyo -DigiData.pyo -axDD132x.pyo -RemoteDigiData.pyo -DigiData.pyc -DigiDataClient.pyo diff --git a/PYME/Acquire/Hardware/ExciterWheel.py b/PYME/Acquire/Hardware/ExciterWheel.py index b0584434d..4bea1b11c 100644 --- a/PYME/Acquire/Hardware/ExciterWheel.py +++ b/PYME/Acquire/Hardware/ExciterWheel.py @@ -26,8 +26,11 @@ import wx from .fw102 import FW102B as filtWheel +import logging +logger = logging.getLogger(__name__) + [wxID_FILTFRAME, wxID_FILTFRAMECHFILTWHEEL, wxID_FILTFRAMEPANEL1, -] = [wx.NewId() for _init_ctrls in range(3)] +] = [wx.NewIdRef() for _init_ctrls in range(3)] class WFilter: def __init__(self, pos, name, description, OD=None): @@ -90,7 +93,7 @@ def GetCurrentIndex(self): def DichroicSync(self): if self.DICHROIC_SYNC and self.dichroic.GetPosition()>=0: dname = self.dichroic.GetFilter() - print(dname) + logger.debug('Setting filters for %s' % dname) fpair = [f for n, f in enumerate(self.filterpair) if f.filtercube == dname][0] if fpair.exciter in self.GetFilterNames(): diff --git a/PYME/Acquire/Hardware/FilterWheel.py b/PYME/Acquire/Hardware/FilterWheel.py index 3d8da97bb..3c07f3137 100644 --- a/PYME/Acquire/Hardware/FilterWheel.py +++ b/PYME/Acquire/Hardware/FilterWheel.py @@ -26,7 +26,7 @@ import wx [wxID_FILTFRAME, wxID_FILTFRAMECHFILTWHEEL, wxID_FILTFRAMEPANEL1, -] = [wx.NewId() for _init_ctrls in range(3)] +] = [wx.NewIdRef() for _init_ctrls in range(3)] class WFilter: def __init__(self, pos, name, description, OD=None): diff --git a/PYME/Acquire/Hardware/FocCorrR.py b/PYME/Acquire/Hardware/FocCorrR.py index baac1ba4d..808f56def 100755 --- a/PYME/Acquire/Hardware/FocCorrR.py +++ b/PYME/Acquire/Hardware/FocCorrR.py @@ -171,9 +171,9 @@ def addMenuItems(self,parentWindow, menu): """Add menu items and keyboard accelerators for LED control to the specified menu & parent window""" #Create IDs - self.ID_TRACK_ON = wx.NewId() - self.ID_TRACK_ON_CALC = wx.NewId() - self.ID_TRACK_OFF = wx.NewId() + self.ID_TRACK_ON = wx.NewIdRef() + self.ID_TRACK_ON_CALC = wx.NewIdRef() + self.ID_TRACK_OFF = wx.NewIdRef() mTracking = wx.Menu(title = '') diff --git a/PYME/Acquire/Hardware/FrFilter.py b/PYME/Acquire/Hardware/FrFilter.py index fecce5c76..924a6b4db 100644 --- a/PYME/Acquire/Hardware/FrFilter.py +++ b/PYME/Acquire/Hardware/FrFilter.py @@ -29,7 +29,7 @@ def create(parent): return FiltFrame(parent) [wxID_FILTFRAME, wxID_FILTFRAMECHFILTWHEEL, wxID_FILTFRAMEPANEL1, -] = [wx.NewId() for _init_ctrls in range(3)] +] = [wx.NewIdRef() for _init_ctrls in range(3)] class FiltPanel(wx.Frame): def _init_ctrls(self, prnt): diff --git a/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuDCAM.py b/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuDCAM.py index de66e2684..40af933e7 100644 --- a/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuDCAM.py +++ b/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuDCAM.py @@ -197,6 +197,7 @@ class DCAMCAP_TRANSFERINFO(ctypes.Structure): dcam.dcamprop_getattr.argtypes = [HDCAM, ctypes.c_void_p] dcam.dcamprop_getvalue.argtypes = [HDCAM, ctypes.c_int32, ctypes.c_void_p] dcam.dcamprop_setvalue.argtypes = [HDCAM, ctypes.c_int32, ctypes.c_double] +dcam.dcamprop_getvaluetext.argtypes = [HDCAM, ctypes.c_void_p] dcam.dcamdev_close.argtypes = [HDCAM,] dcam.dcambuf_alloc.argtypes=[HDCAM,ctypes.c_int32] @@ -431,7 +432,7 @@ def setCamPropValue(self, prop_name, val): ---------- prop_name : str DCAM property string (e.g. 'EXPOSURE TIME') - value : float + val : float Value to set DCAM property. Returns @@ -455,6 +456,71 @@ def setCamPropValue(self, prop_name, val): self.checkStatus(dcam.dcamprop_setvalue(self.handle, iProp, ctypes.c_double(val)), "dcamprop_setvalue") + + def get_cam_prop_array_value(self, prop_name, element_number): + """get a property value for an array property + + Some of these properties are obvious because they include a [0] in + the property name. When called alone through `self.getCamPropValue` + you can only get the first (element_number=0) element of the array. This function will + access other elements of the array too. + + Parameters + ---------- + prop_name : str + DCAM property string (e.g. 'OUTPUT TRIGGER DELAY[0]') + element_number : int + index of the property array to access + + Returns + ------- + value : float + Value of DCAM property. + + Notes + ----- + Less checking is done here than in getCamPropValue, which is therefore + prefered if only dealing with the zeroth element of the property array. + """ + prop = self.getCamPropAttr(prop_name) + n = int(element_number) + prop_id = prop.iProp_ArrayBase + prop.iPropStep_Element * n + # Get the property value + val = ctypes.c_double(0) + self.checkStatus(dcam.dcamprop_getvalue(self.handle, prop_id, + ctypes.byref(val)), + "dcamprop_getvalue") + return float(val.value) + + def set_cam_prop_array_value(self, prop_name, element_number, val): + """Set a property value for an array property + + Some of these properties are obvious because they include a [0] in + the property name. When called alone through `self.setCamPropValue` + you can only set the first (element_number=0) element of the array. + This function will access other elements of the array too. + + Parameters + ---------- + prop_name : str + DCAM property string (e.g. 'OUTPUT TRIGGER DELAY[0]') + element_number : int + index of the property array to set + val : float + Value to set DCAM property. + + Notes + ----- + Less checking is done here than in setCamPropValue, which is therefore + prefered if only dealing with the zeroth element of the property array. + """ + prop = self.getCamPropAttr(prop_name) + n = int(element_number) + prop_id = prop.iProp_ArrayBase + prop.iPropStep_Element * n + # Set the property value + self.checkStatus(dcam.dcamprop_setvalue(self.handle, prop_id, + ctypes.c_double(val)), + "dcamprop_setvalue") def checkProp(self, prop_name): """ @@ -500,6 +566,97 @@ def checkStatus(self, fn_return, fn_name="unknown"): self.getCamInfo(fn_return)) return fn_return + def getCamPropValueText(self, prop_name): + """returns text associated with current property setting. Assumes + prop_name is a text/mode setting, not just a float. + + Parameters + ---------- + prop_name : str + DCAM property string (e.g. 'OUTPUT TRIGGER SOURCE[0]') + + Returns + ------- + value_text : str + name associated with current property value + """ + # Get the property id (if the property exists) + iProp = self.checkProp(prop_name) + + # Get the property value + val = ctypes.c_double(0) + self.checkStatus(dcam.dcamprop_getvalue(self.handle, iProp, + ctypes.byref(val)), + "dcamprop_getvalue") + + # get the property value text + c_buf_len = 64 + c_buf = ctypes.create_string_buffer(c_buf_len) + value_text = DCAMPROP_VALUETEXT() + value_text.iProp = iProp + value_text.value = val + value_text.size = ctypes.sizeof(value_text) + value_text.text = ctypes.addressof(c_buf) + value_text.textbytes = ctypes.c_int32(c_buf_len) + self.checkStatus(dcam.dcamprop_getvaluetext(self.handle, + ctypes.byref(value_text)), + "dcamprop_getvaluetext") + return value_text.text.decode() + + def getCamPropValueTextOptions(self, prop_name): + """enumerate available options for this camera property, with + associated text name. Assumes prop_name is a text/mode setting. Helpful + for development - see Notes. + + Parameters + ---------- + prop_name : str + DCAM property string (e.g. 'OUTPUT TRIGGER KIND[0]') + + Returns + ------- + options : dict + keys are floating point settings that would actually be passed to + DCAM functions, and values are text name + + Notes + ----- + DCAM API (SKD4_v21066291) appears to have a quirk or two, e.g. + 'OUTPUT TRIGGER SOURCE[0]' property has a property range of 2.0 to 5.0, + for 5 text-value options in the SDK, but returns + {2.0: 'READOUT END', 3.0: 'VSYNC', + 4.0: 'DCAM error for dcamprop_getvaluetext with return value -2147481567: Invalid Value!', + 5.0: 'DCAM error for dcamprop_getvaluetext with return value -2147481567: Invalid Value!'} + yet dcamprop.h shows 5 options: + 1: EXPOSURE, 2: READOUT_END, 3: VSYNC, 4: HSYNC, 6,: TRIGGER + A bit odd. + """ + # Get the property id (if the property exists) + iProp = self.checkProp(prop_name) + + # get the property range + lb, ub = self.getCamPropRange(prop_name) + + options = {} + for v in np.arange(lb, ub): + try: + # get the property value text + c_buf_len = 64 + c_buf = ctypes.create_string_buffer(c_buf_len) + value_text = DCAMPROP_VALUETEXT() + value_text.iProp = iProp + value_text.value = ctypes.c_double(v) + value_text.size = ctypes.sizeof(value_text) + value_text.text = ctypes.addressof(c_buf) + value_text.textbytes = ctypes.c_int32(c_buf_len) + self.checkStatus(dcam.dcamprop_getvaluetext(self.handle, ctypes.byref(value_text)), "dcamprop_getvaluetext") + options[v] = value_text.text.decode() + except DCAMException as e: + # DCAM is a special API - sometimes get invalid value errors for in-range values + options[v] = str(e) + + return options + def Shutdown(self): self.checkStatus(dcam.dcamdev_close(self.handle), "dcamdev_close") camReg.unregCamera() diff --git a/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuORCA.py b/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuORCA.py index e42cfab21..ce535b672 100644 --- a/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuORCA.py +++ b/PYME/Acquire/Hardware/HamamatsuDCAM/HamamatsuORCA.py @@ -75,39 +75,42 @@ DCAMPROP_TRIGGERSOURCE_EXTERNAL = 2 DCAMPROP_TRIGGERSOURCE_SOFTWARE = 3 -DCAMCAP_START_SEQUENCE = ctypes.c_int32(int("-1",0)) +DCAMPROP_TRIGGER_MODE__START = 6 +DCAMPROP_TRIGGERACTIVE__EDGE = 1 + +DCAMPROP_OUTPUTTRIGGER_SOURCE__EXPOSURE = 1 +DCAMPROP_OUTPUTTRIGGER_SOURCE__READOUTEND = 2 +DCAMPROP_OUTPUTTRIGGER_SOURCE__VSYNC = 3 +DCAMPROP_OUTPUTTRIGGER_SOURCE__HSYNC = 4 +DCAMPROP_OUTPUTTRIGGER_SOURCE__TRIGGER = 6 + +DCAMPROP_OUTPUTTRIGGER_POLARITY__NEGATIVE = 1 +DCAMPROP_OUTPUTTRIGGER_POLARITY__POSITIVE = 2 + +DCAMPROP_OUTPUTTRIGGER_ACTIVE__EDGE = 1 +DCAMPROP_OUTPUTTRIGGER_ACTIVE__LEVEL = 2 -noiseProperties = { -'100233' : { - 'ReadNoise': 1.65, #CHECKME - converted from an ADU value of 3.51 - 'ElectronsPerCount': 0.47, - 'NGainStages': 0, - 'ADOffset': 100, - 'DefaultEMGain': 1, - 'SaturationThreshold': (2**16 - 1) - }, -'720795' : { - 'ReadNoise': 0.997, # rn is sqrt(var) in units of electrons. Median of varmap is 0.9947778 [e-^2] #CHECKME - converted from 2.394 ADU - 'ElectronsPerCount': 0.416613, - 'NGainStages': 0, - 'ADOffset': 101.753685, - 'DefaultEMGain': 1, - 'SaturationThreshold': (2**16 - 1) - }, -} +DCAMPROP_OUTPUTTRIGGER_KIND__LOW = 1 +DCAMPROP_OUTPUTTRIGGER_KIND__EXPOSURE = 2 +DCAMPROP_OUTPUTTRIGGER_KIND__PROGRAMABLE = 3 +DCAMPROP_OUTPUTTRIGGER_KIND__TRIGGER_READY = 4 +DCAMPROP_OUTPUTTRIGGER_KIND__HIGH = 5 +DCAMPROP_OUTPUTTRIGGER_KIND__ANYROWEXPOSURE = 6 + +DCAMCAP_START_SEQUENCE = ctypes.c_int32(int("-1",0)) +#DCAMCAP_START_SNAP = ctypes.c_int32(int("0",0)) class DCAMZeroBufferedException(Exception): pass class HamamatsuORCA(HamamatsuDCAM, CameraMapMixin): - numpy_frames = 1 + supports_software_trigger = True def __init__(self, camNum): HamamatsuDCAM.__init__(self, camNum) - self.noiseProps = {} self.waitopen = DCAMWAIT_OPEN() self.waitstart = DCAMWAIT_START() self.initialized = False @@ -131,7 +134,6 @@ def Init(self): logger.debug('Initializing Hamamatsu Orca') HamamatsuDCAM.Init(self) if self.camNum < camReg.maxCameras: - self.noiseProps = noiseProperties[self.GetSerialNumber()] # Create a wait handle self.waitopen.size = ctypes.sizeof(self.waitopen) self.waitopen.hdcam = self.handle @@ -143,6 +145,7 @@ def Init(self): self.setDefectCorrectMode(False) self.enable_cooling(True) self._mode = self.MODE_CONTINUOUS + #self._mode = self.MODE_SINGLE_SHOT self.initialized = True logger.debug('Hamamatsu Orca initialized') @@ -171,16 +174,24 @@ def enable_cooling(self, on=True): #OFF =1 , ON = 2 onoff = 2.0 if on else 1.0 - self.setCamPropValue('SENSOR COOLER', onoff) + try: + self.setCamPropValue('SENSOR COOLER', onoff) + except DCAMException as e: + # api v20.10.641, BT FUSION does not have SENSOR COOLER property + # don't worry about it if cooling is already on + status = self.getCamPropValue('SENSOR COOLER STATUS') + if status != onoff: + raise e def GetAcquisitionMode(self): - #FIXME - actually support both continuous and single shot modes + # Support both continuous and single shot modes #return self.MODE_CONTINUOUS return self._mode def SetAcquisitionMode(self, mode): - if mode in [self.MODE_CONTINUOUS, self.MODE_SOFTWARE_TRIGGER]: + if mode in [self.MODE_CONTINUOUS, self.MODE_SOFTWARE_TRIGGER, + self.MODE_SINGLE_SHOT, self.MODE_HARDWARE_START_TRIGGER]: self._mode = mode else: raise RuntimeError('Mode %d not supported' % mode) @@ -188,6 +199,7 @@ def SetAcquisitionMode(self, mode): def FireSoftwareTrigger(self): self.checkStatus(dcam.dcamcap_firetrigger(self.handle, 0), 'dcamcap_firetrigger') + self._log_exposure_start() def StartExposure(self): self.nReadOut = 0 @@ -206,20 +218,36 @@ def StartExposure(self): if self._mode == self.MODE_SOFTWARE_TRIGGER: self.setCamPropValue('TRIGGER SOURCE', DCAMPROP_TRIGGERSOURCE_SOFTWARE) - else: - #continuous mode, internal trigger + self.checkStatus(dcam.dcamcap_start(self.handle, + DCAMCAP_START_SEQUENCE), + "dcamcap_start") + + elif self._mode == self.MODE_CONTINUOUS: + # Continuous mode, internal trigger self.setCamPropValue('TRIGGER SOURCE', DCAMPROP_TRIGGERSOURCE_INTERNAL) + self.checkStatus(dcam.dcamcap_start(self.handle, + DCAMCAP_START_SEQUENCE), + "dcamcap_start") + elif self._mode == self.MODE_SINGLE_SHOT: + # Spoofed single shot mode, using the software trigger + # NOTE: this should no longer be needed when we add software trigger support to z-stepping etc ... + self.setCamPropValue('TRIGGER SOURCE', DCAMPROP_TRIGGERSOURCE_SOFTWARE) + self.checkStatus(dcam.dcamcap_start(self.handle, + DCAMCAP_START_SEQUENCE), + "dcamcap_start") + self.FireSoftwareTrigger() + + elif self._mode == self.MODE_HARDWARE_START_TRIGGER: + self._set_trigger_start_mode() + self.checkStatus(dcam.dcamcap_start(self.handle, + DCAMCAP_START_SEQUENCE), + "dcamcap_start") - eventLog.logEvent('StartAq', '') + self._log_exposure_start() # Start the capture #print str(self.getCamPropValue('SENSOR MODE')) - #TODO - this is probably where we would need to deal with continuous vs single shot modes. - self.checkStatus(dcam.dcamcap_start(self.handle, - DCAMCAP_START_SEQUENCE), - "dcamcap_start") - self._aq_active = True return 0 @@ -376,9 +404,9 @@ def GetPicHeight(self): def CamReady(self): return self.initialized - @property - def noise_properties(self): - return self.noiseProps + #@property + #def noise_properties(self): + # return self.noiseProps def GetCCDTemp(self): # FIXME - actually read the CCD temperature @@ -410,7 +438,7 @@ def GenStartMetadata(self, mdh): HamamatsuDCAM.GenStartMetadata(self, mdh) if self.active: self.fill_camera_map_metadata(mdh) - mdh.setEntry('Camera.ADOffset', self.noiseProps['ADOffset']) + mdh.setEntry('Camera.ADOffset', self.noise_properties['ADOffset']) def SetShutter(self, mode): """ @@ -426,6 +454,113 @@ def SetShutter(self, mode): """ if self.external_shutter is not None: self.external_shutter.SetShutter(mode) + + def SetOutputTrigger(self, mode, delay=0, width=0.0001, positive=True, + trig_number=0): + """ + Set programmable output trigger of the camera. + + TODO: have a look at Andor and PCO SDKs to see if this is sufficiently similar in those, and potentially move/adapt to main camera spec. + + Parameters + ---------- + mode : str + Currently supported modes include: + low: sets output trigger to Low, i.e. TTL zero. Can be useful + for synchronization if one wants to change output trigger + during an acquisition protocol. Ignores pulse delay and + width parameters + readout start: sets output trigger to 'Vsync', which gives the + TTL of the specified pulse width output after the specified + delay from the start of the sensor readout + readout end: sets output trigger to readout end, which gives + the TTL of the specified pulse width output after the + specified delay from the end of the sensor readout + delay : float, optional + delay after trigger event, in seconds, to emit TTL high (assuming + posiive polarity), by default 0 s. + width : float, optional + TTL high pulse width, in seconds, by default 0.0001 s, or 0.1 ms + positive : bool, optional + Sets polarity of the output trigger to positive (True) or negative + (False). True, by default. + trig_number : int, optional + Which output trigger to set for cameras with multiple programmable + output triggers. + + """ + if positive: + self.set_cam_prop_array_value('OUTPUT TRIGGER POLARITY[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_POLARITY__POSITIVE) + else: + self.set_cam_prop_array_value('OUTPUT TRIGGER POLARITY[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_POLARITY__NEGATIVE) + + if mode == 'low': + self.set_cam_prop_array_value('OUTPUT TRIGGER KIND[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_KIND__LOW) + return # return early, no need to set delay and width + + # in case the camera is running, set the pulse parameters before + # changing the trigger source + self.set_cam_prop_array_value('OUTPUT TRIGGER DELAY[0]', trig_number, + delay) # [s], only relevant with 'EDGE' + self.set_cam_prop_array_value('OUTPUT TRIGGER PERIOD[0]', trig_number, + width) # [s], width of pulse, only relevant with 'EDGE' + if mode == 'readout start': + self.set_cam_prop_array_value('OUTPUT TRIGGER KIND[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_KIND__PROGRAMABLE) + self.set_cam_prop_array_value('OUTPUT TRIGGER SOURCE[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_SOURCE__VSYNC) + elif mode == 'readout end': + self.set_cam_prop_array_value('OUTPUT TRIGGER KIND[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_KIND__PROGRAMABLE) + self.set_cam_prop_array_value('OUTPUT TRIGGER SOURCE[0]', trig_number, + DCAMPROP_OUTPUTTRIGGER_SOURCE__READOUTEND) + else: + raise RuntimeError('Unsupported output trigger mode: %s' % mode) + + def _set_trigger_start_mode(self): + """Use to start internally-timed acquisition starting on an external + hardware trigger (currently hardcoded to edge) + + Notes + ----- + set DCAMPROP_TRIGGERSOURCE__EXTERNAL as DCAM_IDPROP_TRIGGERSOUCE + and DCAMPROP_TRIGGER_MODE__START as DCAM_IDPROP_TRIGGER_MODE. The + camera changes to internal mode when the camera receives the trigger. + The DCAM_IDPROP_TRIGGERSOURCE property will be + DCAMPROP_TRIGGERSOURCE__INTERNAL automatically. + """ + try: + self.setCamPropValue('TRIGGER ACTIVE', DCAMPROP_TRIGGERACTIVE__EDGE) + except: + # Sometimes TRIGGER ACTIVE is not writable + pass + self.setCamPropValue('TRIGGER SOURCE', DCAMPROP_TRIGGERSOURCE_EXTERNAL) + self.setCamPropValue('TRIGGER MODE', DCAMPROP_TRIGGER_MODE__START) + + def _get_global_exposure_delay(self): + """How long from the beginning of exposure does it take before + all lines in active ROI are being exposed (global exposure) + + Returns + ------- + delay: float + Global exposure delay, in [s] + + """ + return self.getCamPropValue('TIMING GLOBAL EXPOSURE DELAY') + + def _get_n_output_triggers(self): + """returns the number of programmable output triggers on the camera + + Returns + ------- + int + number of programmable output triggers + """ + return int(self.getCamPropValue('NUMBER OF OUTPUT TRIGGER CONNECTOR')) def Shutdown(self): # if self.initialized: @@ -433,10 +568,35 @@ def Shutdown(self): # "dcamwait_close") HamamatsuDCAM.Shutdown(self) +class Fusion(HamamatsuORCA): + """ + Orca Fusion is functionally the same as the Flash, however uses multiple gain modes. + TODO - check Flash return/fail on READOUT SPEED property so we can catch/return 'fixed' + and not necessarily introduce an extra class + """ + _gain_modes = { + 1:'Ultra-quiet', + 2:'Standard', + 3:'Fast' + } + + @property + def _gain_mode(self): + return self._gain_modes[int(self.getCamPropValue('READOUT SPEED'))] + class MultiviewOrca(MultiviewCameraMixin, HamamatsuORCA): def __init__(self, camNum, multiview_info): HamamatsuORCA.__init__(self, camNum) # default to the whole chip default_roi = dict(xi=0, xf=2048, yi=0, yf=2048) - MultiviewCameraMixin.__init__(self, multiview_info, default_roi, HamamatsuORCA) \ No newline at end of file + MultiviewCameraMixin.__init__(self, multiview_info, default_roi, HamamatsuORCA) + + +#TODO - replace MultiviewCameraMixin with a Multiview wrapper so that we don't need to have explicit multiview versions of all cameras. +class MultiviewFusion(MultiviewCameraMixin, Fusion): + def __init__(self, camNum, multiview_info): + Fusion.__init__(self, camNum) + # default to the whole chip + default_roi = dict(xi=0, xf=2304, yi=0, yf=2304) + MultiviewCameraMixin.__init__(self, multiview_info, default_roi, Fusion) diff --git a/PYME/Acquire/Hardware/HamamatsuDCAM/Hamamatsu_control_panel.py b/PYME/Acquire/Hardware/HamamatsuDCAM/Hamamatsu_control_panel.py new file mode 100644 index 000000000..e6bc7c817 --- /dev/null +++ b/PYME/Acquire/Hardware/HamamatsuDCAM/Hamamatsu_control_panel.py @@ -0,0 +1,54 @@ +import wx + +# Mostly copied from PYME.Acquire.Hardware.pco.pco_sdk_cam_control_panel +class ModeControl(wx.Panel): + def __init__(self, parent, cam): + wx.Panel.__init__(self, parent) + self.scope = parent.scope + self.parent = parent + self.cam = cam + self.options = ["Single shot", "Continuous", "Software trigger", "Hardware trigger"] + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, "Mode : "), 1, wx.ALL, 2) + + self.choice = wx.Choice(self, -1, size = [100,-1], choices=self.options) + self.choice.Bind(wx.EVT_CHOICE, self.on_change) + hsizer.Add(self.choice, 0, wx.ALL, 2) + + self.update() + + self.SetSizerAndFit(hsizer) + + def on_change(self, event=None): + self.scope.frameWrangler.stop() + self.cam.SetAcquisitionMode(int(self.choice.GetSelection())) + self.scope.frameWrangler.start() + self.parent.update() + + def update(self): + self.choice.SetSelection(self.cam.GetAcquisitionMode()) + +class HamamatsuControl(wx.Panel): + def __init__(self, parent, cam, scope): + wx.Panel.__init__(self, parent) + + self.cam = cam + self.scope = scope + + self.ctrls = [ModeControl(self, cam)] + + self._init_ctrls() + + def _init_ctrls(self): + vsizer = wx.BoxSizer(wx.VERTICAL) + + for c in self.ctrls: + vsizer.Add(c, 0, wx.EXPAND|wx.ALL, 2) + + self.SetSizerAndFit(vsizer) + + def update(self): + for c in self.ctrls: + c.update() + \ No newline at end of file diff --git a/PYME/Acquire/Hardware/MPBCommunications/MPBCW.py b/PYME/Acquire/Hardware/MPBCommunications/MPBCW.py index 442bc803f..5f5073221 100644 --- a/PYME/Acquire/Hardware/MPBCommunications/MPBCW.py +++ b/PYME/Acquire/Hardware/MPBCommunications/MPBCW.py @@ -30,7 +30,8 @@ class MPBCWLaser(Laser): - power_controllable = True + powerControlable = True + def __init__(self, serial_port='COM3', name='MPBCW', turn_on=False, init_power=200, **kwargs): """ diff --git a/PYME/Acquire/Hardware/Mercury/.cvsignore b/PYME/Acquire/Hardware/Mercury/.cvsignore deleted file mode 100644 index 90c83c535..000000000 --- a/PYME/Acquire/Hardware/Mercury/.cvsignore +++ /dev/null @@ -1,4 +0,0 @@ -__init__.pyc -mercuryStepper.pyc -Mercury.pyc -PI_Mercury_GCS_DLL.pyc diff --git a/PYME/Acquire/Hardware/Old/Sensicam/.cvsignore b/PYME/Acquire/Hardware/Old/Sensicam/.cvsignore deleted file mode 100755 index be8303ae7..000000000 --- a/PYME/Acquire/Hardware/Old/Sensicam/.cvsignore +++ /dev/null @@ -1,17 +0,0 @@ -__init__.pyc -__init__.py -setup.pyo -sensicamMetadata.pyo -sensicam_wrap.cpp -sensicam.py -build -sensicam.pyo -PYMESettings.db -sensicam.pyc -sensicam_wrap.c -sensicamMetadata.pyc -_sensicam.pyd -__init__.pyo -sensicam.i -setup.py -_senntcam.dll diff --git a/PYME/Acquire/Hardware/Old/Sensicam/meson.build b/PYME/Acquire/Hardware/Old/Sensicam/meson.build new file mode 100644 index 000000000..8c512ae5d --- /dev/null +++ b/PYME/Acquire/Hardware/Old/Sensicam/meson.build @@ -0,0 +1,16 @@ + +# Boilerplate to make sure things go in the right place - TODO can we do some of this in the top-level meson.build? +#py = import('python').find_installation(pure: false) +#np_include_dir = run_command(py, ['-c', '"import numpy; print(numpy.get_include())"'], check: true).stdout().strip() +install_dir = py.get_install_dir() / 'PYME/Acquire/Hardware/Old/Sensicam' + +py_sources = files( + 'sensicam.py', + 'sensicamMetadata.py', + '__init__.py', + 'setup.py', +) + +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Old/Sensicam') + +#FIXME - add_extension diff --git a/PYME/Acquire/Hardware/Old/esp300.py b/PYME/Acquire/Hardware/Old/esp300.py index 7ae3b0931..6a358cd45 100755 --- a/PYME/Acquire/Hardware/Old/esp300.py +++ b/PYME/Acquire/Hardware/Old/esp300.py @@ -93,9 +93,9 @@ def addMenuItems(self,parentWindow, menu): """Add menu items and keyboard accelerators for LED control to the specified menu & parent window""" #Create IDs - self.ID_stop_all = wx.NewId() - self.ID_LED_ON = wx.NewId() - self.ID_LED_OFF = wx.NewId() + self.ID_stop_all = wx.NewIdRef() + self.ID_LED_ON = wx.NewIdRef() + self.ID_LED_OFF = wx.NewIdRef() #Add menu items menu.AppendSeparator() diff --git a/PYME/Acquire/Hardware/Old/mc2000.py b/PYME/Acquire/Hardware/Old/mc2000.py index e9fbf119f..2f699e770 100755 --- a/PYME/Acquire/Hardware/Old/mc2000.py +++ b/PYME/Acquire/Hardware/Old/mc2000.py @@ -125,8 +125,8 @@ def addMenuItems(self,parentWindow, menu): """Add menu items and keyboard accelerators for joystick control to the specified menu & parent window""" #Create IDs - self.ID_JOY_ON = wx.NewId() - self.ID_JOY_OFF = wx.NewId() + self.ID_JOY_ON = wx.NewIdRef() + self.ID_JOY_OFF = wx.NewIdRef() #Add menu items menu.AppendSeparator() diff --git a/PYME/Acquire/Hardware/Piezos/.cvsignore b/PYME/Acquire/Hardware/Piezos/.cvsignore deleted file mode 100644 index 88726cff2..000000000 --- a/PYME/Acquire/Hardware/Piezos/.cvsignore +++ /dev/null @@ -1,8 +0,0 @@ -__init__.pyc -piezo_e816.pyo -piezo_test.pyo -piezo_e816_corr.pyo -piezo_e255.pyo -piezo_e816b.pyo -__init__.pyo -piezo_e662.pyo diff --git a/PYME/Acquire/Hardware/Piezos/base_piezo.py b/PYME/Acquire/Hardware/Piezos/base_piezo.py index defb569d4..7cfdb7551 100644 --- a/PYME/Acquire/Hardware/Piezos/base_piezo.py +++ b/PYME/Acquire/Hardware/Piezos/base_piezo.py @@ -12,6 +12,23 @@ class PiezoBase(object): Template for a display name. Used together with the `axis` argument to PYME.Acquire.microscope.register_piezo to generate a suitable display name for the stage. Can be over-ridden for asthetic purposes in e.g. stepper motors which use PiezoBase as a base, so their controls do not say piezo. + + Notes + ----- + When recording asynchronous sequences (as are used for, e.g. single molecule localisation imaging), the microscope software logs + an event and timestamp when the command to move a stage is given. These events are then used in processing to reconstruct the stage + position over time and assign positions to individual frames. If the frame rate is faster than (or on the same order as) the + stage motion, this can lead to frames between the command being issued and the stage reaching it's destination being assigned an + incorrect position. This can be partially mitigated if the stage is capable of detecting when it is on-target vs. moving. Stages + which are aware of this can log a 'PiezoOnTarget' event using PYME.Acquire.eventLog.logEvent with the actual settling position (in + micrometers) as the event description. If `PiezoOnTarget` events are detected in a series, frames between the commanded move and the + on-target event can be excluded from analysis. Note that this is somewhat fragile and there are several caveats with it's use - most notably that + this will only work if there is only one piezo class emitting OnTarget events, and if this is the z (focus) piezo. If x and y (or, e.g., phase) + piezos emit OnTarget events, it will likely preclude sensible analysis of the aquired data. The other major caveat is thread-safety - `logEvent` is + not gauranteed to be thread safe so calling it from, e.g. a polling thread could cause issues depending on which event backend is in use. + + As a consequence, `PiezoOnTarget` events should really be deprecated and replaced by events where the direction (x,y,z, etc) of the piezo issuing + the event is discernable from either the event name or payload. In the meantime they should be used with caution. """ gui_description = '%s-piezo' @@ -45,3 +62,47 @@ def GetFirmwareVersion(self): def OnTarget(self): raise NotImplementedError + @property + def effective_pos(self): + """ Return the effective position of the piezo. This is exclusively for use in simulation, + and allows for derived classes to simulate drifty piezos, etc. """ + return self.GetPos() + + +class SingleAxisWrapper(PiezoBase): + """ + Allows use of an axis on a multiaxis stage as a single-axis + PiezoBase object for compatibility with e.g. focus-lock code. + """ + def __init__(self, multiaxis_stage, axis=1): + super().__init__() + self.multiaxis_stage = multiaxis_stage + self.units_um = self.multiaxis_stage.units_um + self.axis = axis + + def MoveTo(self, iChannel, fPos, bTimeOut=True): + self.multiaxis_stage.MoveTo(self.axis, fPos, bTimeOut) + + def MoveRel(self, iChannel, incr, bTimeOut=True): + self.multiaxis_stage.MoveRel(self.axis, incr, bTimeOut) + + def GetPos(self, iChannel=0): + return self.multiaxis_stage.GetPos(self.axis) + + def GetTargetPos(self, iChannel=0): + return self.multiaxis_stage.GetTargetPos(self.axis) + + def GetMin(self, iChan=1): + return self.multiaxis_stage.GetMin(self.axis) + + def GetMax(self, iChan=1): + return self.multiaxis_stage.GetMax(self.axis) + + def GetFirmwareVersion(self): + return self.multiaxis_stage.GetFirmwareVersion() + + def OnTarget(self): + return self.multiaxis_stage.OnTarget() + + def SetServo(self, val=1): + self.multiaxis_stage.SetServo(val) diff --git a/PYME/Acquire/Hardware/Piezos/offsetPiezoREST.py b/PYME/Acquire/Hardware/Piezos/offsetPiezoREST.py index 5e7872277..5a346c79d 100644 --- a/PYME/Acquire/Hardware/Piezos/offsetPiezoREST.py +++ b/PYME/Acquire/Hardware/Piezos/offsetPiezoREST.py @@ -93,13 +93,14 @@ def CorrectOffset(self, correction): self.offset += correction # self.MoveTo(0, target) # move the base piezo to correct position self.basePiezo.MoveTo(0, target + self.offset, True) + self.LogFocusCorrection(self.offset) @webframework.register_endpoint('/LogShifts', output_is_json=False) def LogShifts(self, dx, dy, dz, active=True): import wx #eventLog.logEvent('ShiftMeasure', '%3.4f, %3.4f, %3.4f' % (dx, dy, dz)) - wx.CallAfter(eventLog.logEvent, 'ShiftMeasure', '%3.4f, %3.4f, %3.4f' % (float(dx), float(dy), float(dz))) - wx.CallAfter(eventLog.logEvent, 'PiezoOffset', '%3.4f, %d' % (self.GetOffset(), int(active))) + wx.CallAfter(eventLog.logEvent, 'ShiftMeasure', '%3.4f, %3.4f, %3.4f' % (float(dx), float(dy), float(dz)), time.time()) + wx.CallAfter(eventLog.logEvent, 'PiezoOffset', '%3.4f, %d' % (self.GetOffset(), int(active)), time.time()) @webframework.register_endpoint('/OnTarget', output_is_json=False) def OnTarget(self): @@ -108,7 +109,7 @@ def OnTarget(self): @webframework.register_endpoint('/LogFocusCorrection', output_is_json=False) def LogFocusCorrection(self, offset): import wx - wx.CallAfter(eventLog.logEvent, 'PiezoOffsetUpdate', '%3.4f' % float(offset)) + wx.CallAfter(eventLog.logEvent, 'PiezoOffsetUpdate', '%3.4f' % float(offset), time.time()) @webframework.register_endpoint('/GetMaxOffset', output_is_json=False) def GetMaxOffset(self): @@ -173,6 +174,7 @@ def OnTarget(self): return bool(res.json()) def LogFocusCorrection(self, offset): + logger.warning('timestamp will be off, log focus corrections directly in the server process') self._session.get(self.urlbase + '/LogFocusCorrection?offset=%3.3f' % (offset,)) def GetMaxOffset(self): @@ -266,7 +268,7 @@ def MoveTo(self, iChannel, fPos, bTimeOut=True): def MoveRel(self, iChannel, incr, bTimeOut=True): with self._move_lock: self._target_position += float(incr) - print('here - moving to %f' % self._target_position) + #print('here - moving to %f' % self._target_position) p = self.basePiezo.MoveTo(int(iChannel), self._target_position + self.offset, bool(bTimeOut)) @@ -280,9 +282,21 @@ def GetPos(self, iChannel=0): def GetTargetPos(self, iChannel=0): return self._target_position - - - + +class FocusLockingOffsetPiezo(TargetOwningOffsetPiezo): + def __init__(self, base_piezo, focus_lock): + TargetOwningOffsetPiezo.__init__(self, base_piezo) + self.focus_lock = focus_lock + + @webframework.register_endpoint('/CorrectOffset', output_is_json=False) + def CorrectOffset(self, correction): + if not self.focus_lock.lock_enabled: + logger.warning('focus lock is disabled, offset correction ignored') + return + + OffsetPiezo.CorrectOffset(self, correction) + + def main(): """For testing only""" from PYME.Acquire.Hardware.Simulator import fakePiezo diff --git a/PYME/Acquire/Hardware/Piezos/piezo_c867.py b/PYME/Acquire/Hardware/Piezos/piezo_c867.py index 731d3e1f1..fed22615e 100644 --- a/PYME/Acquire/Hardware/Piezos/piezo_c867.py +++ b/PYME/Acquire/Hardware/Piezos/piezo_c867.py @@ -29,6 +29,13 @@ import logging logger = logging.getLogger(__name__) +try: + import pipython.pidevice.gcserror as gcserr +except ImportError: + _has_gcserr = False +else: + _has_gcserr = True + #C867 controller for PiLine piezo linear motor stages #NB units are mm not um as for piezos @@ -42,12 +49,12 @@ def __init__(self, portname='COM1', maxtravel = 25.00, hasTrigger=False, referen self.ser_port = serial.Serial(portname, 38400, timeout=.1, writeTimeout=.1) #turn servo mode on - self.ser_port.write('SVO 1 1\n') - self.ser_port.write('SVO 2 1\n') + self.ser_port.write(b'SVO 1 1\n') + self.ser_port.write(b'SVO 2 1\n') if reference: #find reference switch (should be in centre of range) - self.ser_port.write('FRF\n') + self.ser_port.write(b'FRF\n') #self.lastPos = self.GetPos() #self.lastPos = [self.GetPos(1), self.GetPos(2)] @@ -56,28 +63,28 @@ def __init__(self, portname='COM1', maxtravel = 25.00, hasTrigger=False, referen self.hasTrigger = hasTrigger def SetServo(self, state=1): - self.ser_port.write('SVO 1 %d\n' % state) - self.ser_port.write('SVO 2 %d\n' % state) + self.ser_port.write(b'SVO 1 %d\n' % state) + self.ser_port.write(b'SVO 2 %d\n' % state) def ReInit(self, reference=True): - #self.ser_port.write('WTO A0\n') - self.ser_port.write('SVO 1 1\n') - self.ser_port.write('SVO 2 1\n') + #self.ser_port.write(b'WTO A0\n') + self.ser_port.write(b'SVO 1 1\n') + self.ser_port.write(b'SVO 2 1\n') if reference: #find reference switch (should be in centre of range) - self.ser_port.write('FRF\n') + self.ser_port.write(b'FRF\n') #self.lastPos = [self.GetPos(1), self.GetPos(2)] def SetVelocity(self, chan, vel): - self.ser_port.write('VEL %d %3.4f\n' % (chan, vel)) - #self.ser_port.write('VEL 2 %3.4f\n' % vel) + self.ser_port.write(b'VEL %d %3.4f\n' % (chan, vel)) + #self.ser_port.write(b'VEL 2 %3.4f\n' % vel) def GetVelocity(self, chan): self.ser_port.flushInput() self.ser_port.flushOutput() - self.ser_port.write('VEL?\n') + self.ser_port.write(b'VEL?\n') self.ser_port.flushOutput() #time.sleep(0.005) res = self.ser_port.readline() @@ -89,24 +96,24 @@ def GetVelocity(self, chan): def MoveTo(self, iChannel, fPos, bTimeOut=True): if (fPos >= 0): if (fPos <= self.max_travel): - self.ser_port.write('MOV %d %3.6f\n' % (iChannel, fPos)) + self.ser_port.write(b'MOV %d %3.6f\n' % (iChannel, fPos)) #self.lastPos[iChannel-1] = fPos else: - self.ser_port.write('MOV %d %3.6f\n' % (iChannel, self.max_travel)) + self.ser_port.write(b'MOV %d %3.6f\n' % (iChannel, self.max_travel)) #self.lastPos[iChannel-1] = self.max_travel else: - self.ser_port.write('MOV %d %3.6f\n' % (iChannel, 0.0)) + self.ser_port.write(b'MOV %d %3.6f\n' % (iChannel, 0.0)) #self.lastPos[iChannel-1] = 0.0 def MoveRel(self, iChannel, incr, bTimeOut=True): - self.ser_port.write('MVR %d %3.6f\n' % (iChannel, incr)) + self.ser_port.write(b'MVR %d %3.6f\n' % (iChannel, incr)) def MoveToXY(self, xPos, yPos, bTimeOut=True): xPos = min(max(xPos, 0),self.max_travel) yPos = min(max(yPos, 0),self.max_travel) - self.ser_port.write('MOV 1 %3.6f 2 %3.6f\n' % (xPos, yPos)) + self.ser_port.write(b'MOV 1 %3.6f 2 %3.6f\n' % (xPos, yPos)) #self.lastPos = [self.GetPos(1), self.GetPos(2)] #self.lastPos = fPos @@ -114,7 +121,7 @@ def MoveToXY(self, xPos, yPos, bTimeOut=True): def GetPos(self, iChannel=0): #self.ser_port.flush() #time.sleep(0.005) - #self.ser_port.write('POS? %d\n' % iChannel) + #self.ser_port.write(b'POS? %d\n' % iChannel) #self.ser_port.flushOutput() #time.sleep(0.005) #res = self.ser_port.readline() @@ -126,13 +133,13 @@ def GetPosXY(self): self.ser_port.flushInput() self.ser_port.flushOutput() #time.sleep(0.005) - self.ser_port.write('POS? 1 2\n') + self.ser_port.write(b'POS? 1 2\n') self.ser_port.flushOutput() #time.sleep(0.005) res1 = self.ser_port.readline() res2 = self.ser_port.readline() print((res1, res2)) - return float(res1.split('=')[1]), float(res2.split('=')[1]) + return float(res1.split(b'=')[1]), float(res2.split(b'=')[1]) @@ -149,7 +156,7 @@ def GetMax(self, iChan=1): def GetFirmwareVersion(self): import re - self.ser_port.write('*IDN?\n') + self.ser_port.write(b'*IDN?\n') self.ser_port.flush() verstring = self.ser_port.readline() @@ -175,14 +182,14 @@ def __init__(self, portname='COM1', maxtravel = 25.00, hasTrigger=False, referen self.ptol = 5e-4 #reboot stage - self.ser_port.write('RBT\n') + self.ser_port.write(b'RBT\n') time.sleep(1) #try to make motion smooth - self.ser_port.write('SPA 1 0x4D 2\n') - self.ser_port.write('SPA 2 0x4D 2\n') + self.ser_port.write(b'SPA 1 0x4D 2\n') + self.ser_port.write(b'SPA 2 0x4D 2\n') #turn servo mode on - self.ser_port.write('SVO 1 1\n') - self.ser_port.write('SVO 2 1\n') + self.ser_port.write(b'SVO 1 1\n') + self.ser_port.write(b'SVO 2 1\n') self.servo = True self.onTarget = False @@ -192,7 +199,7 @@ def __init__(self, portname='COM1', maxtravel = 25.00, hasTrigger=False, referen if reference: #find reference switch (should be in centre of range) - self.ser_port.write('FRF\n') + self.ser_port.write(b'FRF\n') time.sleep(.5) #self.lastPos = self.GetPos() @@ -209,10 +216,18 @@ def __init__(self, portname='COM1', maxtravel = 25.00, hasTrigger=False, referen self.targetVelocity = self.velocity.copy() self.lastTargetPosition = self.position.copy() + + self._servo_target = True self.lock = threading.Lock() self.tloop = threading.Thread(target=self._Loop) self.tloop.start() + + def _log_error(self, errCode): + if _has_gcserr: + logger.error('Stage Error: %s' % gcserr.GCSError.translate_error(self.errCode)) + else: + logger.error('Stage Error: %d' % self.errCode) def _Loop(self): while self.loopActive: @@ -222,44 +237,60 @@ def _Loop(self): self.ser_port.flushOutput() #check position - self.ser_port.write('POS? 1 2\n') + self.ser_port.write(b'POS? 1 2\n') self.ser_port.flushOutput() #time.sleep(0.005) res1 = self.ser_port.readline() res2 = self.ser_port.readline() #print res1, res2 - self.position[0] = float(res1.split('=')[1]) - self.position[1] = float(res2.split('=')[1]) + self.position[0] = float(res1.split(b'=')[1]) + self.position[1] = float(res2.split(b'=')[1]) - self.ser_port.write('ERR?\n') + self.ser_port.write(b'ERR?\n') self.ser_port.flushOutput() self.errCode = int(self.ser_port.readline()) if not self.errCode == 0: - #print(('Stage Error: %d' %self.errCode)) - logger.error('Stage Error: %d' %self.errCode) + self._log_error(self.errCode) + #print self.targetPosition, self.stopMove - if self.stopMove: - self.ser_port.write('HLT\n') + if self.stopMove: # SERVOCHECK: check if this is ok to process when servo is off!!! + logger.debug('piezo_c867T: stopMove issuing HLT command, expect error 10 to be set') + self.ser_port.write(b'HLT\n') time.sleep(.1) - self.ser_port.write('POS? 1 2\n') + + # issuing the HLT command sets an error condition (Error code 10) + # read and clear this + self.ser_port.write(b'ERR?\n') + self.ser_port.flushOutput() + errCode = int(self.ser_port.readline()) + if not ((errCode == 10) or (errCode==0)): + self.errCode = errCode + self._log_error(self.errCode) + + self.ser_port.write(b'POS? 1 2\n') self.ser_port.flushOutput() #time.sleep(0.005) res1 = self.ser_port.readline() res2 = self.ser_port.readline() #print res1, res2 - self.position[0] = float(res1.split('=')[1]) - self.position[1] = float(res2.split('=')[1]) + self.position[0] = float(res1.split(b'=')[1]) + self.position[1] = float(res2.split(b'=')[1]) self.targetPosition[:] = self.position[:] self.stopMove = False - + if self.servo != self._servo_target: + self.ser_port.write(b'SVO 1 %d\n' % int(self._servo_target)) + self.ser_port.write(b'SVO 2 %d\n' % int(self._servo_target)) + self.servo = self._servo_target + + if self.servo: if not np.all(self.velocity == self.targetVelocity): for i, vel in enumerate(self.targetVelocity): - self.ser_port.write('VEL %d %3.9f\n' % (i+1, vel)) + self.ser_port.write(b'VEL %d %3.9f\n' % (i+1, vel)) self.velocity = self.targetVelocity.copy() #print('v') logger.debug('Setting stage target vel: %s' % self.targetVelocity) @@ -269,26 +300,29 @@ def _Loop(self): #update our target position pos = np.clip(self.targetPosition, 0,self.max_travel) - self.ser_port.write('MOV 1 %3.9f 2 %3.9f\n' % (pos[0], pos[1])) + self.ser_port.write(b'MOV 1 %3.9f 2 %3.9f\n' % (pos[0], pos[1])) self.lastTargetPosition = pos.copy() #print('p') logger.debug('Setting stage target pos: %s' % pos) time.sleep(.01) - #check to see if we're on target - self.ser_port.write('ONT?\n') - self.ser_port.flushOutput() - time.sleep(0.005) - res1 = self.ser_port.readline() - ont1 = int(res1.split('=')[1]) == 1 - res1 = self.ser_port.readline() - ont2 = int(res1.split('=')[1]) == 1 + # check to see if we're on target + # NB - ONT only works if servo mode is on + # self.ser_port.write(b'ONT?\n') + # self.ser_port.flushOutput() + # time.sleep(0.005) + # res1 = self.ser_port.readline() + # ont1 = int(res1.split(b'=')[1]) == 1 + # res1 = self.ser_port.readline() + # ont2 = int(res1.split(b'=')[1]) == 1 + + # onT = (ont1 and ont2) or (self.servo == False) + # self.onTarget = onT and self.onTargetLast + # self.onTargetLast = onT + + # just do a crude 'on-target' calculation for software servoing + self.onTarget = np.allclose(self.position, self.targetPosition, atol=self.ptol) - onT = (ont1 and ont2) or (self.servo == False) - self.onTarget = onT and self.onTargetLast - self.onTargetLast = onT - self.onTarget = np.allclose(self.position, self.targetPosition, atol=self.ptol) - #time.sleep(.1) except serial.SerialTimeoutException: @@ -310,38 +344,35 @@ def close(self): #time.sleep(.01) #self.ser_port.close() - + # directly enabling/disabling the servo outside the thread loop seems to hang PYME + # the implementation below sets a flag with the change being performed in the monitoring thread + # TODO - change SetServo signature to take bool? def SetServo(self, state=1): - self.lock.acquire() - try: - self.ser_port.write('SVO 1 %d\n' % state) - self.ser_port.write('SVO 2 %d\n' % state) - self.servo = state == 1 - finally: - self.lock.release() - + self._servo_target = state > 0 + + # def SetParameter(self, paramID, state): # self.lock.acquire() # try: -# self.ser_port.write('SVO 1 %d\n' % state) -# self.ser_port.write('SVO 2 %d\n' % state) +# self.ser_port.write(b'SVO 1 %d\n' % state) +# self.ser_port.write(b'SVO 2 %d\n' % state) # self.servo = state == 1 # finally: # self.lock.release() def ReInit(self, reference=True): - #self.ser_port.write('WTO A0\n') + #self.ser_port.write(b'WTO A0\n') self.lock.acquire() try: - self.ser_port.write('RBT\n') + self.ser_port.write(b'RBT\n') time.sleep(1) - self.ser_port.write('SVO 1 1\n') - self.ser_port.write('SVO 2 1\n') + self.ser_port.write(b'SVO 1 1\n') + self.ser_port.write(b'SVO 2 1\n') self.servo = True if reference: #find reference switch (should be in centre of range) - self.ser_port.write('FRF\n') + self.ser_port.write(b'FRF\n') time.sleep(1) self.stopMove = True @@ -351,8 +382,8 @@ def ReInit(self, reference=True): #self.lastPos = [self.GetPos(1), self.GetPos(2)] def SetVelocity(self, chan, vel): - #self.ser_port.write('VEL %d %3.4f\n' % (chan, vel)) - #self.ser_port.write('VEL 2 %3.4f\n' % vel) + #self.ser_port.write(b'VEL %d %3.4f\n' % (chan, vel)) + #self.ser_port.write(b'VEL 2 %3.4f\n' % vel) self.targetVelocity[chan-1] = vel def GetVelocity(self, chan): @@ -367,7 +398,7 @@ def MoveTo(self, iChannel, fPos, bTimeOut=True, vel=None): self.onTarget = False #def MoveRel(self, iChannel, incr, bTimeOut=True): - # self.ser_port.write('MVR %d %3.6f\n' % (iChannel, incr)) + # self.ser_port.write(b'MVR %d %3.6f\n' % (iChannel, incr)) def MoveToXY(self, xPos, yPos, bTimeOut=True, vel=None): @@ -382,7 +413,7 @@ def MoveToXY(self, xPos, yPos, bTimeOut=True, vel=None): def GetPos(self, iChannel=0): #self.ser_port.flush() #time.sleep(0.005) - #self.ser_port.write('POS? %d\n' % iChannel) + #self.ser_port.write(b'POS? %d\n' % iChannel) #self.ser_port.flushOutput() #time.sleep(0.005) #res = self.ser_port.readline() @@ -434,22 +465,22 @@ def OnTarget(self): def GetFirmwareVersion(self): import re - self.ser_port.write('*IDN?\n') + self.ser_port.write(b'*IDN?\n') self.ser_port.flush() verstring = self.ser_port.readline() return float(re.findall(r'V(\d\.\d\d)', verstring)[0]) - + class c867Joystick: def __init__(self, stepper): self.stepper = stepper def Enable(self, enabled = True): - if not self.IsEnabled() == enabled: + if not self.IsEnabled() == enabled: # we need to switch state self.stepper.SetServo(enabled) def IsEnabled(self): return self.stepper.servo - + diff --git a/PYME/Acquire/Hardware/Piezos/piezo_e709.py b/PYME/Acquire/Hardware/Piezos/piezo_e709.py index 5365f1c83..feec90f30 100644 --- a/PYME/Acquire/Hardware/Piezos/piezo_e709.py +++ b/PYME/Acquire/Hardware/Piezos/piezo_e709.py @@ -36,7 +36,7 @@ def __init__(self, portname='COM1', maxtravel = 400.00, Osen=None, hasTrigger=Fa self.ser_port = serial.Serial(portname, 115200, rtscts=0, timeout=.1, writeTimeout=.1) if not Osen is None: - #self.ser_port.write('SPA A8 %3.4f\n' % Osen) + #self.ser_port.write(b'SPA A8 %3.4f\n' % Osen) self.osen = Osen else: self.osen = 0 @@ -45,8 +45,8 @@ def __init__(self, portname='COM1', maxtravel = 400.00, Osen=None, hasTrigger=Fa # if self.GetFirmwareVersion() > 3.2: # self.MAXWAVEPOINTS = 256 -# self.ser_port.write('WTO A0\n') - self.ser_port.write('SVO Z 1\n') +# self.ser_port.write(b'WTO A0\n') + self.ser_port.write(b'SVO Z 1\n') self.lastPos = self.GetPos() @@ -54,45 +54,45 @@ def __init__(self, portname='COM1', maxtravel = 400.00, Osen=None, hasTrigger=Fa self.hasTrigger = hasTrigger def ReInit(self): -# self.ser_port.write('WTO A0\n') - self.ser_port.write('SVO Z 1\n') +# self.ser_port.write(b'WTO A0\n') + self.ser_port.write(b'SVO Z 1\n') self.lastPos = self.GetPos() def SetServo(self,val = 1): - self.ser_port.write('SVO Z %d\n' % val) + self.ser_port.write(b'SVO Z %d\n' % val) def MoveTo(self, iChannel, fPos, bTimeOut=True): if (fPos >= 0): if (fPos <= self.max_travel): - self.ser_port.write('MOV Z %3.4f\n' % fPos) + self.ser_port.write(b'MOV Z %3.4f\n' % fPos) self.lastPos = fPos else: - self.ser_port.write('MOV Z %3.4f\n' % self.max_travel) + self.ser_port.write(b'MOV Z %3.4f\n' % self.max_travel) self.lastPos = self.max_travel else: - self.ser_port.write('MOV Z %3.4f\n' % 0.0) + self.ser_port.write(b'MOV Z %3.4f\n' % 0.0) self.lastPos = 0.0 def MoveRel(self, iChannel, incr, bTimeOut=True): - self.ser_port.write('MVR Z %3.4f\n' % incr) + self.ser_port.write(b'MVR Z %3.4f\n' % incr) def GetPos(self, iChannel=0): self.ser_port.flush() time.sleep(0.005) - self.ser_port.write('POS? Z\n') + self.ser_port.write(b'POS? Z\n') self.ser_port.flushOutput() time.sleep(0.005) res = self.ser_port.readline() #print res - return float(res.split('=')[-1]) + self.osen + return float(res.split(b'=')[-1]) + self.osen # def SetDriftCompensation(self, dc = True): # if dc: -# self.ser_port.write('DCO A1\n') +# self.ser_port.write(b'DCO A1\n') # self.ser_port.flushOutput() # self.driftCompensation = True # else: -# self.ser_port.write('DCO A0\n') +# self.ser_port.write(b'DCO A0\n') # self.ser_port.flushOutput() # self.driftCompensation = False @@ -110,7 +110,7 @@ def GetPos(self, iChannel=0): # time.sleep(0.05) # # for i, v in zip(range(self.numWavePoints), data): -# self.ser_port.write('SWT A%d %3.4f\n' % (i, v)) +# self.ser_port.write(b'SWT A%d %3.4f\n' % (i, v)) # self.ser_port.flushOutput() # time.sleep(0.01) # res = self.ser_port.readline() @@ -125,7 +125,7 @@ def GetPos(self, iChannel=0): # time.sleep(0.05) # # for i in range(64): -# self.ser_port.write('SWT? A%d\n' %i) +# self.ser_port.write(b'SWT? A%d\n' %i) # self.ser_port.flushOutput() # time.sleep(0.05) # res = self.ser_port.readline() @@ -143,14 +143,14 @@ def GetPos(self, iChannel=0): # # if dwellTime == None: # #triggered -# self.ser_port.write('WTO A%d\n' % self.numWavePoints) +# self.ser_port.write(b'WTO A%d\n' % self.numWavePoints) # else: -# self.ser_port.write('WTO A%d %3.4f\n' % (self.numWavePoints, dwellTime)) +# self.ser_port.write(b'WTO A%d %3.4f\n' % (self.numWavePoints, dwellTime)) # # self.ser_port.flushOutput() # # def StopWaveOutput(self, iChannel=0): -# self.ser_port.write('WTO A0\n') +# self.ser_port.write(b'WTO A0\n') # self.ser_port.flushOutput() def GetControlReady(self): @@ -166,7 +166,7 @@ def GetMax(self, iChan=1): def GetFirmwareVersion(self): import re - self.ser_port.write('*IDN?\n') + self.ser_port.write(b'*IDN?\n') self.ser_port.flush() verstring = self.ser_port.readline() @@ -188,12 +188,12 @@ def __init__(self, portname='COM1', maxtravel = 400., Osen=None, hasTrigger=Fals self.units = 'um' #reboot stage - #self.ser_port.write('RBT\n') + #self.ser_port.write(b'RBT\n') #time.sleep(1) #turn servo mode on - self.ser_port.write('SVO Z 1\n') - #self.ser_port.write('SVO 2 1\n') + self.ser_port.write(b'SVO Z 1\n') + #self.ser_port.write(b'SVO 2 1\n') self.servo = True @@ -230,16 +230,16 @@ def _Loop(self): #check position time.sleep(0.005) - self.ser_port.write('POS? Z\n') + self.ser_port.write(b'POS? Z\n') self.ser_port.flushOutput() time.sleep(0.005) res1 = self.ser_port.readline() #res2 = self.ser_port.readline() #print res1, res2 - self.position[0] = float(res1.split('=')[1]) - #self.position[1] = float(res2.split('=')[1]) + self.position[0] = float(res1.split(b'=')[1]) + #self.position[1] = float(res2.split(b'=')[1]) - self.ser_port.write('ERR?\n') + self.ser_port.write(b'ERR?\n') self.ser_port.flushOutput() self.errCode = int(self.ser_port.readline()) @@ -249,23 +249,23 @@ def _Loop(self): #print self.targetPosition, self.stopMove if self.stopMove: - self.ser_port.write('STP\n') + self.ser_port.write(b'STP\n') time.sleep(.1) - self.ser_port.write('POS? Z\n') + self.ser_port.write(b'POS? Z\n') self.ser_port.flushOutput() time.sleep(0.005) res1 = self.ser_port.readline() #res2 = self.ser_port.readline() #print res1, res2 - self.position[0] = float(res1.split('=')[1]) - #self.position[1] = float(res2.split('=')[1]) + self.position[0] = float(res1.split(b'=')[1]) + #self.position[1] = float(res2.split(b'=')[1]) self.targetPosition[:] = self.position[:] self.stopMove = False #if not np.all(self.velocity == self.targetVelocity): # for i, vel in enumerate(self.targetVelocity): - # self.ser_port.write('VEL %d %3.9f\n' % (i+1, vel)) + # self.ser_port.write(b'VEL %d %3.9f\n' % (i+1, vel)) # self.velocity = self.targetVelocity.copy() # print 'v' # print 'l' @@ -273,17 +273,17 @@ def _Loop(self): #update our target position pos = np.clip(self.targetPosition, 0,self.max_travel) - self.ser_port.write('MOV Z %3.9f\n' % (pos[0], )) + self.ser_port.write(b'MOV Z %3.9f\n' % (pos[0], )) self.lastTargetPosition = pos.copy() #print('p') logging.debug('Moving piezo to target: %f' % (pos[0],)) #check to see if we're on target - self.ser_port.write('ONT? Z\n') + self.ser_port.write(b'ONT? Z\n') self.ser_port.flushOutput() time.sleep(0.005) res1 = self.ser_port.readline() - self.onTarget = int(res1.split('=')[1]) == 1 + self.onTarget = int(res1.split(b'=')[1]) == 1 #time.sleep(.1) @@ -312,8 +312,8 @@ def close(self): def SetServo(self, state=1): self.lock.acquire() try: - self.ser_port.write('SVO Z %d\n' % state) - #self.ser_port.write('SVO 2 %d\n' % state) + self.ser_port.write(b'SVO Z %d\n' % state) + #self.ser_port.write(b'SVO 2 %d\n' % state) self.servo = state == 1 finally: self.lock.release() @@ -321,25 +321,25 @@ def SetServo(self, state=1): # def SetParameter(self, paramID, state): # self.lock.acquire() # try: -# self.ser_port.write('SVO 1 %d\n' % state) -# self.ser_port.write('SVO 2 %d\n' % state) +# self.ser_port.write(b'SVO 1 %d\n' % state) +# self.ser_port.write(b'SVO 2 %d\n' % state) # self.servo = state == 1 # finally: # self.lock.release() def ReInit(self, reference=True): - #self.ser_port.write('WTO A0\n') + #self.ser_port.write(b'WTO A0\n') self.lock.acquire() try: - self.ser_port.write('RBT\n') + self.ser_port.write(b'RBT\n') time.sleep(1) - self.ser_port.write('SVO z 1\n') - #self.ser_port.write('SVO 2 1\n') + self.ser_port.write(b'SVO z 1\n') + #self.ser_port.write(b'SVO 2 1\n') self.servo = True #if reference: #find reference switch (should be in centre of range) - # self.ser_port.write('FRF\n') + # self.ser_port.write(b'FRF\n') time.sleep(1) self.stopMove = True @@ -349,8 +349,8 @@ def ReInit(self, reference=True): #self.lastPos = [self.GetPos(1), self.GetPos(2)] #def SetVelocity(self, chan, vel): - #self.ser_port.write('VEL %d %3.4f\n' % (chan, vel)) - #self.ser_port.write('VEL 2 %3.4f\n' % vel) + #self.ser_port.write(b'VEL %d %3.4f\n' % (chan, vel)) + #self.ser_port.write(b'VEL 2 %3.4f\n' % vel) #self.targetVelocity[chan-1] = vel # def GetVelocity(self, chan): @@ -383,7 +383,7 @@ def OnTarget(self): def GetPos(self, iChannel=0): #self.ser_port.flush() #time.sleep(0.005) - #self.ser_port.write('POS? %d\n' % iChannel) + #self.ser_port.write(b'POS? %d\n' % iChannel) #self.ser_port.flushOutput() #time.sleep(0.005) #res = self.ser_port.readline() @@ -427,8 +427,8 @@ def GetMax(self, iChan=1): def GetFirmwareVersion(self): import re - self.ser_port.write('*IDN?\n') + self.ser_port.write(b'*IDN?\n') self.ser_port.flush() verstring = self.ser_port.readline() - return float(re.findall(r'V(\d\.\d\d)', verstring)[0]) \ No newline at end of file + return float(re.findall(r'V(\d\.\d\d)', verstring)[0]) diff --git a/PYME/Acquire/Hardware/Piezos/piezo_e816.py b/PYME/Acquire/Hardware/Piezos/piezo_e816.py index fc3c25b4f..cd8573a1c 100755 --- a/PYME/Acquire/Hardware/Piezos/piezo_e816.py +++ b/PYME/Acquire/Hardware/Piezos/piezo_e816.py @@ -180,7 +180,7 @@ def GetTargetPos(self,iChannel=0): import numpy as np class piezo_e816T(PiezoBase): - def __init__(self, portname='COM1', maxtravel=12.00, Osen=None, hasTrigger=False): + def __init__(self, portname='COM1', maxtravel=12.00, Osen=None, hasTrigger=False, targetTolerance=0.002): self.max_travel = maxtravel #self.waveData = None #self.numWavePoints = 0 @@ -205,6 +205,7 @@ def __init__(self, portname='COM1', maxtravel=12.00, Osen=None, hasTrigger=False self.servo = True self.errCode = 0 self.onTarget = False + self.targetTolerance = targetTolerance #self.lastPos = self.GetPos() @@ -263,7 +264,7 @@ def _Loop(self): # print('p') logging.debug('Moving piezo to target: %f' % (pos[0],)) - if np.allclose(self.position, self.targetPosition, atol=.002): + if np.allclose(self.position, self.targetPosition, atol=self.targetTolerance): self.onTarget = True # check to see if we're on target diff --git a/PYME/Acquire/Hardware/Piezos/piezo_e816_dll.py b/PYME/Acquire/Hardware/Piezos/piezo_e816_dll.py index 99d3f40e1..7a68bf059 100755 --- a/PYME/Acquire/Hardware/Piezos/piezo_e816_dll.py +++ b/PYME/Acquire/Hardware/Piezos/piezo_e816_dll.py @@ -35,6 +35,8 @@ from .base_piezo import PiezoBase from PYME.Acquire.Hardware.GCS import gcs +logger = logging.getLogger(__name__) + def get_connected_devices(): n, devs= gcs.EnumerateUSB('E-816') @@ -231,8 +233,8 @@ def __init__(self, identifier=None, maxtravel=12.00, Osen=None, hasTrigger=False if identifier is None: devices = get_connected_devices() identifier = devices[0] - - self.id = gcs.ConnectUSB(identifier) + self._identifier = identifier + self.id = gcs.ConnectUSB(self._identifier) if not Osen is None: # self.ser_port.write('SPA A8 %3.4f\n' % Osen) @@ -257,8 +259,6 @@ def __init__(self, identifier=None, maxtravel=12.00, Osen=None, hasTrigger=False self.driftCompensation = False self.hasTrigger = hasTrigger - self.loopActive = True - self.stopMove = False self.position = np.array([0.]) # self.velocity = np.array([self.maxvelocity, self.maxvelocity]) @@ -266,8 +266,10 @@ def __init__(self, identifier=None, maxtravel=12.00, Osen=None, hasTrigger=False # self.targetVelocity = self.velocity.copy() self.lastTargetPosition = self.position.copy() - - + self._start_loop() + + def _start_loop(self): + self.loopActive = True self.tloop = threading.Thread(target=self._Loop) self.tloop.daemon=True self.tloop.start() @@ -284,7 +286,7 @@ def _Loop(self): self.errCode = int(gcs.qERR(self.id)) - if not self.errCode == 0: + if not self.errCode == 0: # I have yet to see this work logging.info(('Stage Error: %d' % self.errCode)) # print self.targetPosition, self.stopMove @@ -319,12 +321,32 @@ def _Loop(self): # logging.exception('Value error on response from ONT') # time.sleep(.1) - - - except IndexError: - print('IndexException') + except RuntimeError as e: + # gcs.fcnWrap.HandleError throws Runtimes for everything + logger.error(str(e)) + try: + self.errCode = int(gcs.qERR(self.id)) + logger.error('error code: %s' % str(self.errCode)) + except: + logger.error('no error code retrieved') + if '-1' in str(e): + logger.debug('reinitializing GCS connection, 10 s pause') + gcs.CloseConnection(self.id) + time.sleep(10.0) # this takes at least more than 1 s + try: + self.id = gcs.ConnectUSB(self._identifier) + logger.debug('restablished connection to piezo') + except RuntimeError as e: + logger.error('trying to get new device ID') + devices = get_connected_devices() + self._identifier = devices[0] + logger.debug('new device ID acquired') + self.id = gcs.ConnectUSB(self._identifier) + time.sleep(1.0) + logger.debug('turning on servo') + gcs.SVO(self.id, b'A', [1]) + time.sleep(1.0) finally: - self.stopMove = False self.lock.release() # close port on loop exit @@ -342,11 +364,22 @@ def close(self): # self.ser_port.close() def ReInit(self): + """ + Reinitialize a closed connection to the pifoc. Note the pifoc + connection must have already been closed, whether by using `close` + or the polling loop failing. + """ with self.lock: - #self.ser_port.write('WTO A0\n') + logging.info('restarting e816') + self.loopActive = False + time.sleep(1.0) + self.id = gcs.ConnectUSB(self._identifier) gcs.SVO(self.id, b'A', [1]) - time.sleep(1) + time.sleep(1.0) self.lastPos = self.GetPos() + + logging.info('reinitialized, starting loop') + self._start_loop() def OnTarget(self): return self.onTarget diff --git a/PYME/Acquire/Hardware/Piezos/piezo_pipython_gcs.py b/PYME/Acquire/Hardware/Piezos/piezo_pipython_gcs.py new file mode 100644 index 000000000..16846c5ed --- /dev/null +++ b/PYME/Acquire/Hardware/Piezos/piezo_pipython_gcs.py @@ -0,0 +1,560 @@ + +# Defines a GCSPiezo class which uses the pipython package distributed +# by Physik Instrumente to initialize and run their stages. +# Tested with a E-727 controller, on pipython 2.9.0.4 (pypi) +# see https://github.com/PI-PhysikInstrumente/PIPython + +# GETTING STARTED: +# 1. Install https://github.com/PI-PhysikInstrumente/PIPython +# 2. In a Python shell, import this file and call get_gcs_usb() +# to find your stage (assuming it has the drivers it needs, is turned on, etc.) +# 3. Use the full description string returned by get_gcs_usb() to +# find the axes names as used by PI by calling +# get_stage_axes(description). You'll want to use this description +# exactly in the next step, as PIPython can be sensitive to e.g. +# ['1', '2'] vs [1, 2], or '['A', 'B'] vs. ['a', 'b']. +# 4. Initialize the stage with GCSPiezo(description, axes) in your +# PYME init script. +# 5: Consider using GCSPiezoThreaded for performance improvements, +# particularly if you are using a multi-axis stage, or multiple GCSPiezos +# in your setup, as updating the position of stages happens in order to +# update the GUI and can slow down PYMEAcquire considerably. + + +from PYME.Acquire.Hardware.Piezos.base_piezo import PiezoBase +from pipython import GCSDevice, pitools +from pipython import PILogger, WARNING +import numpy as np +import logging +import time +PILogger.setLevel(WARNING) + +logger = logging.getLogger(__name__) + + + + +def get_gcs_usb(): + """ + returns list of PI devices connected by USB and their serial numbers. Use + to get str description suitable for initializing GCSPiezo, e.g. + 'PI E-727 Controller SN 0118035989' + """ + with GCSDevice() as pidev: + return pidev.EnumerateUSB() + +def get_stage_axes(description): + try: + pi = GCSDevice() + pi.ConnectUSB(description) + return pitools.getaxeslist(pi, None) + finally: + pi.CloseConnection() + + +class GCSPiezo(PiezoBase): + units_um = 1 # assumes controllers is configured in units of um. + def __init__(self, description=None, axes=None): + """ + Parameters + ---------- + descriptions: str + description as found by GCS enumerate, e.g. + PI E-727 Controller SN 0118035989. + axes : list + list of axes if this is a multi-axis controller. e.g. [1, 2, 3] + for a 3-axis stage. Note some PI firmwares assume ['a', 'b', 'c'], + or ['X', 'Y', 'Z']. After initialization these will be indexed into + for method calls, i.e. GetPos(iChannel=0) will use axes[0] for the + GCS axis descriptor. + + """ + PiezoBase.__init__(self) + self.pi = GCSDevice() + self.pi.ConnectUSB(description) + assert self.pi.IsConnected() + + if axes is None: + logger.error('NO AXES SPECIFIED. Will try and run with all axes') + self.axes = pitools.getaxeslist(self.pi, None) + else: + self.axes = axes + + self._min = {} # key'd on iChan + self._max = {} + # try: + # units = self.pi.qPUN(self.axes) + # logger.debug('stage units: %s' % [units[axis] for axis in self.axes]) + # except: + # pass + # PI appears to use unicoded um to set units to um, not funcitonal at the moment, but + # ideally we would remove the unit check/log above and just force to um here. + # self.pi.PUN(axes, values) + + def SetServo(self, val=1): + # due to form of SetServo in the baseclass, just set all axes for now + self.pi.SVO(self.axes, [val for axis in self.axes]) + + def MoveTo(self, iChannel, fPos, bTimeOut=True): + self.pi.MOV(self.axes[iChannel], fPos) + + def MoveRel(self, iChannel, incr, bTimeOut=True): + """ relative to current target position + @param axes: Axis or list of axes or dictionary {axis : value}. + @param values : Float or list of floats or None. + """ + self.pi.MVR(self.axes[iChannel], incr) + + def GetPos(self, iChannel=1): + try: + assert np.isscalar(iChannel) + except: + raise AssertionError('GetPos only supports single-axis query') + axis = self.axes[iChannel] + return self.pi.qPOS([axis])[axis] + + def GetTargetPos(self, iChannel=1): + # assume that target pos = current pos. Over-ride in derived class if possible + # axis = self.axes[iChannel] + return self.GetPos(iChannel) + + def GetMin(self, iChan=1): + """ + Get lower limits ("soft limits") + """ + try: + assert np.isscalar(iChan) + except: + raise AssertionError('GetMin only supports single-axis query') + + try: + return self._min[iChan] + except KeyError: + logger.debug('Fetching %s axis min' % iChan) + axis = self.axes[iChan] + self._min[iChan] = self.pi.qTMN(axis)[axis] + return self._min[iChan] + # return self.pi.qNLM(axes=[iChan])[iChan] + # qCMN min commandable closed-loop target + + def GetMax(self, iChan=1): + """ + Get upper limits ("soft limits") + """ + try: + assert np.isscalar(iChan) + except: + raise AssertionError('GetMax only supports single-axis query') + try: + return self._max[iChan] + except KeyError: + logger.debug('Fetching %s axis max' % iChan) + axis = self.axes[iChan] + self._max[iChan] = self.pi.qTMX(axis)[axis] + return self._max[iChan] + + def GetFirmwareVersion(self): + raise NotImplementedError + + def OnTarget(self, axes=None): + """ + For multiaxis stages, query with None reports for all of them + """ + return all(self.pi.qONT(axes)) + + def close(self): + self.pi.CloseConnection() + + +import threading +from PYME.Acquire.eventLog import logEvent +class GCSPiezoThreaded(PiezoBase): + units_um = 1 # assumes controllers is configured in units of um. + def __init__(self, description=None, axes=None, update_rate=0.01, startup=True, + stages=None, refmodes=None, servostates=True, controlmodes=None, joystick=None, + target_tol=0.001, adc_channels=None, analog_drive_info=None): + """ + Parameters + ---------- + descriptions: str + description as found by GCS enumerate, e.g. + PI E-727 Controller SN 0118035989. + axes : list + list of axes if this is a multi-axis controller. e.g. ['1', '2', '3'] + for a 3-axis stage. Note some PI firmwares assume ['a', 'b', 'c'], + or ['X', 'Y', 'Z']. After initialization these will be indexed into + for method calls, i.e. GetPos(iChannel=0) will use axes[0] for the + GCS axis descriptor. + update_rate : float + number of seconds pause between threaded polling of position / + on-targets + startup: boolean to use pitools.startup function to startup device + + next keyword parameters below are passed to the pitools.startup command: + + stages: Name of stage(s) to initialize as string or list (not tuple!) or 'None' to skip. + refmodes: Referencing command(s) as string (for all stages) or list (not tuple!) or 'None' to skip. + Please see the manual of the controller for the valid reference procedure. + 'refmodes' can be: + 'FRF': References the stage using the reference position. + 'FNL': References the stage using the negative limit switch. + 'FPL': References the stage using the positive limit switch. + 'POS': Sets the current position of the stage to 0.0. + 'ATZ': Runs an auto zero procedure. + servostates: Desired servo state(s) as Boolean (for all stages) or dict {axis : state} or 'None' to skip. + For controllers with GCS 3.0 syntax: + If True the axis is switched to control mode 0x2. + If False the axis is switched to control mode 0x1. + controlmodes: Only valid for controllers with GCS 3.0 syntax! + Switch the axis to the given control mode: + int (for all axes) or dict {axis : controlmode} or 'None' to skip. + To skip any control mode switch make sure the servostate is also 'None'! + If 'controlmodes' is set (not 'None') the parameter 'servostates' is ignored. + + joystick: should be a joystick object or None if no joystick; needs to support a few standard methods + + target_tol: float, optional + tolerance [um] for on-target position, default is 0.001 um. Applied to + all axes. Does not change servo settings in the controller, but flags + when a move is considered complete and re-sends a move command durring polling + if target position is not reached within tolerance. + + adc_channels: list, optional + list of ADC channels to use for axes in `axes`. You will need + to consult the manual for your controller, as the available ADC + channels may be offset from the axis number and/or flexibly configurable. + You will also need to set the command level to 1 (in your init script) + with e.,g. stage.pi.gcsdevice.CCL(1, 'password') before using the `set_analog_control` + method. The password should also be in your controller manual. + + analog_drive_info: dict, optional + dictionary holding controller-specific information for setting up analog control, namely + the IDs for parameters which need to be set on the controller. Required keys are: + 'ADC_CHANNEL_FOR_TARGET', 'OFFSET', and 'GAIN'. See your controller manual for details. + Example: + analog_drive_info = { # these values are for the E-727 controller + 'ADC_CHANNEL_FOR_TARGET': 0x06000500, + 'OFFSET': 0x02000200, # sensor mech correction 1, analog drive offset + 'GAIN': 0x02000300} # sensor mech correction 2, analog drive gain + """ + PiezoBase.__init__(self) + self.pi = GCSDevice() + self.pi.ConnectUSB(description) + assert self.pi.IsConnected() + self._lock = threading.Lock() + + if axes is None: + logger.error('NO AXES SPECIFIED. Will try and run with all axes') + self.axes = pitools.getaxeslist(self.pi, None) + else: + self.axes = axes + + self.adc_channels = adc_channels + if self.adc_channels is None: + logger.debug('No analog control channels specified') + + self.analog_drive_info = analog_drive_info + if self.analog_drive_info is None: + logger.debug('No analog drive info specified') + + + # startup device with supplied per axes parameters + if startup: + pitools.startup(self.pi, stages=stages, refmodes=refmodes, + servostates=servostates, controlmodes=controlmodes) + if not refmodes is None: # if any referencing is carried out wait to finish + pitools.waitonreferencing(self.pi, None, timeout=120) # for now 2 min timeout + + self._min = [pitools.getmintravelrange(self.pi, axis)[axis] for axis in self.axes] + self._max = [pitools.getmaxtravelrange(self.pi, axis)[axis] for axis in self.axes] + + # before the loop is active, need to query positions explicitly to make self.positions valid + self.positions = np.array([self.pi.qPOS([axis])[axis] for axis in self.axes]) + self._ontarget_tol = target_tol + self.joystick = joystick + if self.joystick is not None: + self.joystick.init(self) # the joystick object should have an init method + self.disable_joystick() # this includes enabling updating_ontarget + else: + self.enable_updating_ontarget() + + self._update_rate = update_rate + self._start_loop() + + def enable_joystick(self): + if self.joystick is None: + return + self.disable_updating_ontarget() + with self._lock: + self.joystick.enablecommands() + self._joystick_enabled = True + + def disable_joystick(self): + if self.joystick is None: + return + with self._lock: + self.joystick.disablecommands() + self.enable_updating_ontarget() + self._joystick_enabled = False + + def SetServo(self, val=1): + with self._lock: + # due to form of SetServo in the baseclass, just set all axes for now + self.pi.SVO(self.axes, [val for axis in self.axes]) + + def MoveTo(self, iChannel, fPos, bTimeOut=True): + self.target_positions[iChannel] = fPos + # self.pi.MOV(self.axes[iChannel], fPos) + + def MoveRel(self, iChannel, incr, bTimeOut=True): + """ relative to current target position + @param axes: Axis or list of axes or dictionary {axis : value}. + @param values : Float or list of floats or None. + """ + new_pos = self.target_positions[iChannel] + incr + self.target_positions[iChannel] = new_pos + # self.pi.MVR(self.axes[iChannel], incr) + + def GetPos(self, iChannel=1): + try: + assert np.isscalar(iChannel) + except: + raise AssertionError('GetPos only supports single-axis query') + return self.positions[iChannel] + # axis = self.axes[iChannel] + # return self.pi.qPOS([axis])[axis] + + def GetTargetPos(self, iChannel=1): + # assume that target pos = current pos. Over-ride in derived class if possible + # axis = self.axes[iChannel] + # return self.GetPos(iChannel) + return self.target_positions[iChannel] + + def GetMin(self, iChan=1): + """ + Get lower limits ("soft limits") + """ + try: + assert np.isscalar(iChan) + except: + raise AssertionError('GetMin only supports single-axis query') + + return self._min[iChan] + + def GetMax(self, iChan=1): + """ + Get upper limits ("soft limits") + """ + try: + assert np.isscalar(iChan) + except: + raise AssertionError('GetMax only supports single-axis query') + + return self._max[iChan] + + def GetFirmwareVersion(self): + if self.pi.HasqVER(): + ver = self.pi.qVER() # under lock? + verinfo = ver.strip() + else: + verinfo = 'Controller has no version info' + + return verinfo + + def OnTarget(self): + """ + For multiaxis stages, query with None reports for all of them + """ + return self._all_on_target + + def close(self): + self.loop_active = False + with self._lock: + self.pi.CloseConnection() + + def _start_loop(self): + self.loop_active = True + self.tloop = threading.Thread(target=self._Loop) + self.tloop.daemon=True + self.tloop.start() + + def enable_updating_ontarget(self): + # first reset the relevant variables + self.target_positions = np.copy(self.positions) + self._last_target_positions = np.copy(self.positions) + self._all_on_target = True + self._on_target = np.asarray([True for axis in self.axes]) + # now also switch the flag + self._updating_ontarget = True + + def disable_updating_ontarget(self): # does this need a lock, any race conditions? + self._updating_ontarget = False + + def _Loop(self): + while self.loop_active: + time.sleep(self._update_rate) # we should sleep while NOT holding the lock so that others can grab it if needed + with self._lock: + try: + # check position + for ind, axis in enumerate(self.axes): + # does this need a lock? + self.positions[ind] = self.pi.qPOS([axis])[axis] + + if not self._updating_ontarget: + continue # skip below if not currently in update target mode (e.g. when joystick active) + + # update ontarget + old_on_target = np.copy(self._on_target) + on_targets = pitools.ontarget(self.pi, None) + self._on_target = np.asarray([on_targets[axis] for axis in self.axes]) + if not np.all(self._on_target == old_on_target): + # FIXME - something to log which axis would be cool? + # FIXME - analog control of even 1 axis will essentially break this + logEvent('PiezoOnTarget', '%s' % self.positions, time.time()) + self._all_on_target = np.all(self._on_target) + targets_matched = np.isclose(self.target_positions, self._last_target_positions, + rtol=0, atol=self._ontarget_tol) + if all(targets_matched): + self._all_on_target = True + else: + self._all_on_target = False + for ind, matched in enumerate(targets_matched): + if not matched: + new_pos = np.clip(self.target_positions[ind], self._min[ind], self._max[ind]) + self.pi.MOV(self.axes[ind], new_pos) + self._on_target[ind] = False + + self._last_target_positions = np.copy(self.target_positions) + + except Exception as e: + logger.error(str(e)) + + logger.debug('exiting') + + def set_analog_control(self, channel, adc_channel=None, vmin=-10, vmax=10): + """ Configure and enable analog control for a given axis + + Parameters + ---------- + channel : int + index into self.axes for the axis to configure + adc_channel : int, optional + if `self.adc_channels` is set, the ADC channel specified for + `channel` axis will be configured and enabled. Can also pass 0 here + which will DISABLE analog control for the axis. + vmin : float, optional + minimum control voltage, and the voltage which will correspond to + the minimum of the movement range for this axis. Default is -10V. + vmax : float, optional + maximum control voltage, and the voltage which will correspond to + the maximum of the movement range for this axis. Default is 10V. + + Raises + ------ + NotImplementedError + if no ADC channel is specified and `self.adc_channels` is not + configured. + + Notes + ----- + OnTarget events are not aware of analog control, and will not + generally fire if even one axis is under analog control. + + Requires command level 1. which can be enabled with + self.pi.gcsdevice.CCL(1, password) + + Not sure why qSGA is an 'unknown command' to query the gain. + Note also that the qAOS command for querying analog input offset only + works for the axes present, not the ADC channels which is what we need + to query. + + """ + # voltage is assigned to a normalized range. For E-727, -100 to 100 [norm. units] corresponds to -10 to 10 [V] + min_norm, max_norm = min([10*vmin, -100]), max([10*vmax, 100]) + gain = (self._max[channel] - self._min[channel]) / (max_norm - min_norm) + offset = self._max[channel] - gain * max_norm + # scaled_value = offset + gain * norm_value + if adc_channel is None: + try: + adc_channel = self.adc_channels[channel] + except: + raise NotImplementedError('No ADC channel configured for analog control of axis %s' % channel) + elif adc_channel == 0: + # disable analog control + self.pi.SPA(self.axes[channel], self.analog_drive_info['ADC_CHANNEL_FOR_TARGET'], 0) + return + + # set parameters in RAM/temporary settings + self.pi.SPA(adc_channel, self.analog_drive_info['OFFSET'], offset) + self.pi.SPA(adc_channel, self.analog_drive_info['GAIN'], gain) + # and finally hook up the analog control + self.pi.SPA(self.axes[channel], self.analog_drive_info['ADC_CHANNEL_FOR_TARGET'], adc_channel) + # note qSGA is an 'unknown command' to query the gain + # note qAOS does not work for querying the ADC channels + + def disable_analog_control(self, channel): + self.set_analog_control(channel, adc_channel=0) + + def get_analog_control_settings(self, channel): + adc_channel = self.pi.qSPA(self.axes[channel], + self.analog_drive_info['ADC_CHANNEL_FOR_TARGET'])[self.axes[channel]][int(self.analog_drive_info['ADC_CHANNEL_FOR_TARGET'])] + enabled = adc_channel != 0 + offset = self.pi.qSPA(adc_channel, self.analog_drive_info['OFFSET'])[adc_channel][int(self.analog_drive_info['OFFSET'])] + gain = self.pi.qSPA(adc_channel, self.analog_drive_info['GAIN'])[adc_channel][int(self.analog_drive_info['GAIN'])] + return enabled, adc_channel, offset, gain + + +# a piezo_pipython_gcs compatible joystick class +# - provides Enable() and IsEnabled() methods for use by PYME position ui and scope object +# - an init() method to set the joystick parameters via GCS commands and register the gcspiezo "parent" instance +# - an enablecommands() method of GCS commands that will be used by the gcspiezo "parent" object to enable the joystick +# - a disablecommands() method of GCS commands that will be used by the gcspiezo "parent" object to disable the joystick + +# example usage: +# from PYME.Acquire.Hardware.Piezos.piezo_pipython_gcs import GCSPiezoThreaded +# from PYMEcs.Acquire.Hardware.Piezos.joystick_c867_digital import DigitalJoystick +# scope.stage = GCSPiezoThreaded('PI C-867 Piezomotor Controller SN 0122013807', axes=['1', '2'], +# refmodes='FRF',joystick=DigitalJoystick()) + +class JoystickBase(object): + def __init__(self): + self.gcspiezo = None + self._initialised = False + + # this method needs to be tweaked to the specific controller and joystick model in derived class + def init(self,gcspiezo): + # register the piezo device + self.gcspiezo = gcspiezo # needs to be retained in derived class implementation + + # further initialisation code should go below (e.g. GCS commands) + # i.e., this method needs to be implemented in a derived class + + # when completed needs to set the initialised flag: self._initialised = True + + raise NotImplementedError('Needs to be implemented in derived class.') + + def Enable(self, enabled = True): + self.check_initialised() + if not self.IsEnabled() == enabled: + if enabled: + self.gcspiezo.enable_joystick() + else: + self.gcspiezo.disable_joystick() + + def IsEnabled(self): + self.check_initialised() + return self.gcspiezo._joystick_enabled + + def check_initialised(self): + if not self._initialised: + raise RuntimeError("joystick must be initialised before using these methods") + + # GCS commands to enable the joystick + def enablecommands(self): # this method is supposed to be used from the parent gcspiezo object + # e.g. self.gcspiezo.pi.HIN(self.gcspiezo.axes,[True for axis in self.gcspiezo.axes]) + raise NotImplementedError('Needs to be implemented in derived class.') + + # GCS commands to enable the joystick + def disablecommands(self): # this method is supposed to be used from the parent gcspiezo object + # e.g. self.gcspiezo.pi.HIN(self.gcspiezo.axes,[False for axis in self.gcspiezo.axes]) + raise NotImplementedError('Needs to be implemented in derived class.') diff --git a/PYME/Acquire/Hardware/Simulator/.cvsignore b/PYME/Acquire/Hardware/Simulator/.cvsignore deleted file mode 100644 index c4a0b97e2..000000000 --- a/PYME/Acquire/Hardware/Simulator/.cvsignore +++ /dev/null @@ -1,18 +0,0 @@ -wormlike.pyo -__init__.pyc -fakeCam.pyo -lasersliders.pyo -fluor.pyc -dSimControl.pyo -wormlike2.pyo -rend_im.pyo -lasersliders.pyc -wormlike2.pyc -dSimControl.pyc -wormlike_curled.pyo -fluor.pyo -fakeCam.pyc -__init__.pyo -rend_im.pyc -fakePiezo.pyo -fakePiezo.pyc diff --git a/PYME/Acquire/Hardware/Simulator/dSimControl.py b/PYME/Acquire/Hardware/Simulator/dSimControl.py index cc82cde54..8ec9fce4e 100755 --- a/PYME/Acquire/Hardware/Simulator/dSimControl.py +++ b/PYME/Acquire/Hardware/Simulator/dSimControl.py @@ -23,21 +23,19 @@ #Boa:Dialog:dSimControl -import wx -import wx.grid -from . import fluor -from . import wormlike2 import json -# import pylab -import scipy +import logging + import numpy as np -#import os -from . import rend_im +import scipy +import wx +import wx.grid -#import PYME.ui.autoFoldPanel as afp import PYME.ui.manualFoldPanel as afp +from PYME.simulation import wormlike2 +from . import fluor +from . import rend_im -import logging logger = logging.getLogger(__name__) def create(parent): @@ -56,9 +54,9 @@ def create(parent): wxID_DSIMCONTROLSTCUROBJPOINTS, wxID_DSIMCONTROLSTSTATUS, wxID_DSIMCONTROLTEXPROBE, wxID_DSIMCONTROLTEXSWITCH, wxID_DSIMCONTROLTKBP, wxID_DSIMCONTROLTNUMFLUOROPHORES, -] = [wx.NewId() for _init_ctrls in range(28)] +] = [wx.NewIdRef() for _init_ctrls in range(28)] -[wxID_DSIMCONTROLTREFRESH] = [wx.NewId() for _init_utils in range(1)] +[wxID_DSIMCONTROLTREFRESH] = [wx.NewIdRef() for _init_utils in range(1)] from PYME.recipes.traits import HasTraits, Float, Dict, Bool, List @@ -72,7 +70,7 @@ class PSFSettings(HasTraits): four_pi = Bool(False) def default_traits_view(self): - from traitsui.api import View, Item, Group, ListEditor + from traitsui.api import View, Item #from PYME.ui.custom_traits_editors import CBEditor return View(Item(name='wavelength_nm'), @@ -532,7 +530,7 @@ def OnBGenWormlikeButton(self, event): numFluors = int(self.tNumFluorophores.GetValue()) persistLength= float(self.tPersist.GetValue()) #wc = wormlike2.fibre30nm(kbp, 10*kbp/numFluors) - wc = wormlike2.wiglyFibre(kbp, persistLength, kbp/numFluors) + wc = wormlike2.wiglyFibre(kbp, persistLength, kbp / numFluors) XVals = self.scope.cam.XVals YVals = self.scope.cam.YVals @@ -581,7 +579,7 @@ def OnBGenWormlikeButton(self, event): def OnBLoadPointsButton(self, event): fn = wx.FileSelector('Read point positions from file') if fn is None: - print('No file selected') + logger.warning('No file selected, cancelling') return if fn.endswith('.npy'): @@ -599,7 +597,7 @@ def OnBLoadPointsButton(self, event): def OnBSavePointsButton(self, event): fn = wx.SaveFileSelector('Save point positions to file', '.txt') if fn is None: - print('No file selected') + logger.warning('No file selected, cancelling') return #self.points = pylab.load(fn) @@ -625,7 +623,7 @@ def OnBSetPSFModel(self, event=None): self.st_psf.SetLabelText('PSF: 4Pi %s [%1.2f NA @ %d nm, zerns=%s]' % ('vectorial' if psf_settings.vectorial else 'scalar',psf_settings.NA, psf_settings.wavelength_nm, z_modes)) else: - print('Setting PSF with zernike modes: %s' % z_modes) + logger.info('Setting PSF with zernike modes: %s' % z_modes) rend_im.genTheoreticalModel(rend_im.mdh, zernikes=z_modes, lamb=psf_settings.wavelength_nm, NA=psf_settings.NA, vectorial=psf_settings.vectorial) @@ -633,7 +631,7 @@ def OnBSetPSFModel(self, event=None): def OnBSetPSF(self, event): fn = wx.FileSelector('Read PSF from file', default_extension='psf', wildcard='PYME PSF Files (*.psf)|*.psf|TIFF (*.tif)|*.tif') - print(fn) + logger.debug('Setting PSF from file: %s' % fn) if fn == '': #rend_im.genTheoreticalModel(rend_im.mdh) return @@ -673,9 +671,9 @@ def OnBGenFloursButton(self, event): spec_sig[:,0] = self.spectralSignatures[c, 0] spec_sig[:,1] = self.spectralSignatures[c, 1] - fluors = fluor.specFluors(x, y, z, transTens, exCrosses, activeState=self.activeState, spectralSig=spec_sig) + fluors = fluor.SpectralFluorophores(x, y, z, transTens, exCrosses, activeState=self.activeState, spectralSig=spec_sig) else: - fluors = fluor.fluors(x, y, z, transTens, exCrosses, activeState=self.activeState) + fluors = fluor.Fluorophores(x, y, z, transTens, exCrosses, activeState=self.activeState) chan_z_offsets, chan_specs = self.getSplitterInfo() self.scope.cam.setSplitterInfo(chan_z_offsets, chan_specs) @@ -779,7 +777,7 @@ def OnBLoadEmpiricalHistButton(self, event): from . import EmpiricalHist fn = wx.FileSelector('Read point positions from file') if fn is None: - print('No file selected') + logger.warning('No file selected, cancelling') return with open(fn,'r') as f: diff --git a/PYME/Acquire/Hardware/Simulator/fakeCam.py b/PYME/Acquire/Hardware/Simulator/fakeCam.py index cacce6e33..550467745 100755 --- a/PYME/Acquire/Hardware/Simulator/fakeCam.py +++ b/PYME/Acquire/Hardware/Simulator/fakeCam.py @@ -46,6 +46,9 @@ from PYME.Acquire.Hardware import EMCCDTheory from PYME.Acquire.Hardware import ccdCalibrator +import logging +logger = logging.getLogger(__name__) + def generate_camera_maps(size_x = 1024, size_y = 1024, seed=100, read_median=1.38, offset=100): """ Generate camera maps for sCMOS simulation, using a constant random seed so that the maps are reproducible @@ -145,10 +148,10 @@ def noisify(self, im): if self.approximate_read_noise: o = self._read_approx(im.shape) else: - o = self.ADOffset + (self.ReadoutNoise / self.ElectronsPerCount) * scipy.random.standard_normal(im.shape) + o = self.ADOffset + (self.ReadoutNoise / self.ElectronsPerCount) * np.random.standard_normal(im.shape) if self.shutterOpen: - o = o + (M/(self.ElectronsPerCount*F2))*scipy.random.poisson((self.QE*F2)*(im + self.background)) + o = o + (M/(self.ElectronsPerCount*F2))*np.random.poisson((self.QE*F2)*(im + self.background)) return o @@ -159,19 +162,20 @@ def getbg(self): return self.ADOffset + M*(int(self.shutterOpen)*(0 + self.background)*self.QE*F2)/(self.ElectronsPerCount*F2) - +WELL_DEPTH= (2 << 15) -1 #calculate image in a separate thread to maintain GUI reponsiveness class compThread(threading.Thread): def __init__(self,XVals, YVals,zPiezo, zOffset, fluors, noisemaker, laserPowers, intTime, contMode = True, - bufferlength=500, biplane = False, biplane_z = 500, xpiezo=None, ypiezo=None, illumFcn = 'ConstIllum'): + bufferlength=500, biplane = False, biplane_z = 500, xpiezo=None, ypiezo=None, illumFcn = 'ConstIllum', objects=None): #TODO - Do we need to change the default buffer length. This shouldn't really be an issue as we pause the simulation the buffer starts to fill up. threading.Thread.__init__(self) self.XVals = XVals self.YVals = YVals - self.fluors = fluors + self.fluors = fluors # type: PYME.Acquire.Hardware.Simulator.fluor.Fluorophores + self.objects = objects #list of Fluorophores instances #self.zPos = zPos self.laserPowers = laserPowers self.intTime = intTime @@ -198,21 +202,19 @@ def __init__(self,XVals, YVals,zPiezo, zOffset, fluors, noisemaker, laserPowers, self.stopAq = False self.startAq = False - print(laserPowers) - print(intTime) - - #self.frameLock = threading.Lock() - #self.frameLock.acquire() - - def setSplitterInfo(self, chan_z_offsets, chan_specs): + def setSplitterInfo(self, chan_z_offsets, chan_specs, chan_x_offsets=None): self._chan_z_offsets = chan_z_offsets self._chan_specs = chan_specs - nChans = len(chan_z_offsets) - x_pixels = len(self.XVals) - x_chan_pixels = x_pixels/nChans - x_chan_size = (self.XVals[1] - self.XVals[0])*x_chan_pixels - self._chan_x_offsets = [i*x_chan_size for i in range(nChans)] + if chan_x_offsets: + self._chan_x_offsets = chan_x_offsets + else: + nChans = len(chan_z_offsets) + x_pixels = len(self.XVals) + x_chan_pixels = x_pixels/nChans + x_chan_size = (self.XVals[1] - self.XVals[0])*x_chan_pixels + + self._chan_x_offsets = [i*x_chan_size for i in range(nChans)] @property def ChanXOffsets(self): @@ -252,27 +254,36 @@ def ChanSpecs(self): def run(self): - #self.im = self.noiseMaker.noisify(rend_im.simPalmIm(self.XVals, self.YVals, self.zPos,self.fluors, - # laserPowers=self.laserPowers, intTime=self.intTime))[:,:].astype('uint16') - while not self.kill: #self.frameLock.acquire() while ((not self.aqRunning) or (self.numBufferedImages > self.bufferlength/2.)) and (not self.kill) : time.sleep(.01) - zPos = (self.zPiezo.GetPos() - self.zOffset)*1e3 + zPos = (self.zPiezo.effective_pos - self.zOffset)*1e3 xp = 0 yp = 0 if not self.xPiezo is None: - xp = (self.xPiezo.GetPos() - self.xPiezo.max_travel/2)*1e3 + xp = (self.xPiezo.effective_pos - self.xPiezo.max_travel/2)*1e3 if not self.xPiezo is None: - yp = (self.yPiezo.GetPos() - self.yPiezo.max_travel/2)*1e3 + yp = (self.yPiezo.effective_pos - self.yPiezo.max_travel/2)*1e3 + + roi_bbox = (xp,yp, xp+self.XVals[-1], yp + self.YVals[-1]) #print self.ChanSpecs, self.ChanXOffsets - - r_i = rend_im.simPalmImFI(self.XVals + xp, self.YVals + yp, zPos,self.fluors, + + if self.objects is not None: + r_i = np.zeros((len(self.XVals), len(self.YVals)), 'f') + for obj in self.objects: + if obj.hit_test(roi_bbox): + rend_im.simPalmImFI(self.XVals + xp, self.YVals + yp, zPos,obj, + laserPowers=self.laserPowers, intTime=self.intTime, + position=[xp,yp,zPos], illuminationFunction=self.illumFcn, + ChanXOffsets=self.ChanXOffsets, ChanZOffsets=self.ChanZOffsets, + ChanSpecs=self.ChanSpecs, im=r_i) + else: + r_i = rend_im.simPalmImFI(self.XVals + xp, self.YVals + yp, zPos,self.fluors, laserPowers=self.laserPowers, intTime=self.intTime, position=[xp,yp,zPos], illuminationFunction=self.illumFcn, ChanXOffsets=self.ChanXOffsets, ChanZOffsets=self.ChanZOffsets, @@ -285,7 +296,7 @@ def run(self): r_i = r_i[:,:] _im = self.noiseMaker.noisify(r_i) - self.im = _im.astype('uint16') + self.im = np.clip(_im, 0, WELL_DEPTH).astype('uint16') self.buffer[self.bufferWritePos,:,:] = self.im self.bufferWritePos +=1 @@ -363,27 +374,27 @@ def __init__(self, XVals, YVals, noiseMaker, zPiezo, zOffset=50.0, fluors=None, self.pixel_size_nm = XVals[1] - XVals[0] + rend_im.set_pixelsize_nm(self.pixel_size_nm) + self.zPiezo=zPiezo self.xPiezo = xpiezo self.yPiezo = ypiezo self.fluors=fluors + self._objects=None self.noiseMaker=noiseMaker - self.SaturationThreshold = (2**16) - 1 + self._saturation_threshold = (2**16) - 1 self.DefaultEMGain = 150 - + self.preampGain = 1 + self.laserPowers=laserPowers self.illumFcn = illumFcn self.intTime=0.1 self.zOffset = zOffset - self.compT = None #thread which is currently being computed - #self.compT = None #finished thread holding image (c.f. camera buffer) - #self.compT = compThread(self.XVals[self.ROIx[0]:self.ROIx[1]], self.YVals[self.ROIy[0]:self.ROIy[1]], self.zPiezo, self.zOffset,self.fluors, self.noiseMaker, laserPowers=self.laserPowers, intTime=self.intTime, xpiezo=self.xPiezo, ypiezo=self.yPiezo, illumFcn=self.illumFcn) - #self.compT.start() + self.compT = None #thread which is currently being computed self._restart_compT() - #self._frameRate = 0 self._acquisition_mode = self.MODE_CONTINUOUS #self.contMode = True @@ -411,6 +422,11 @@ def setFluors(self, fluors): self.fluors = fluors self._restart_compT() + + def set_objects(self, objs): + self._objects = objs + + self._restart_compT() def SetSensorDimensions(self, x_size=256, y_size=256, pixel_size_nm=70., restart=True): self.XVals = pixel_size_nm*np.arange(0.0, float(x_size)) @@ -422,8 +438,11 @@ def SetSensorDimensions(self, x_size=256, y_size=256, pixel_size_nm=70., restart if restart: self._restart_compT() + def _preamp_mode_repr(self): + return 'Preamp mode %d' % self.preampGain + def GetSerialNumber(self): - return 0 + return 'FAKE-000' def SetIntegTime(self, iTime): self.intTime=iTime#*1e-3 @@ -460,23 +479,29 @@ def _restart_compT(self): except AttributeError: running = False - #print (self.fluors.fl['state'] == 2).sum() - #print running - #print self.compT.laserPowers - self.compT = compThread(self.XVals[self.ROIx[0]:self.ROIx[1]], self.YVals[self.ROIy[0]:self.ROIy[1]], + if self._objects is not None: + self.compT = compThread(self.XVals[self.ROIx[0]:self.ROIx[1]], self.YVals[self.ROIy[0]:self.ROIy[1]], + self.zPiezo, self.zOffset, None, self.noiseMaker, laserPowers=self.laserPowers, + intTime=self.intTime, xpiezo=self.xPiezo, ypiezo=self.yPiezo, illumFcn=self.illumFcn, objects=self._objects) + else: + self.compT = compThread(self.XVals[self.ROIx[0]:self.ROIx[1]], self.YVals[self.ROIy[0]:self.ROIy[1]], self.zPiezo, self.zOffset, self.fluors, self.noiseMaker, laserPowers=self.laserPowers, intTime=self.intTime, xpiezo=self.xPiezo, ypiezo=self.yPiezo, illumFcn=self.illumFcn) - + try: - self.compT.setSplitterInfo(self._chan_z_offsets, self._chan_specs) + #vx = self.XVals[1] - self.XVals[0] + chan_x_offsets = getattr(self, '_chan_x_offsets', None) + print('chan_x_offsets:', chan_x_offsets) + #except AttributeError: + # chan_x_offsets=None + + self.compT.setSplitterInfo(self._chan_z_offsets, self._chan_specs, chan_x_offsets=chan_x_offsets) except AttributeError: pass self.compT.start() - #print (self.fluors.fl['state'] == 2).sum() - self.compT.aqRunning = running def GetROI(self): @@ -496,11 +521,8 @@ def StopAq(self): #pass def StartExposure(self): - eventLog.logEvent('StartAq', '') + self._log_exposure_start() self.compT.StartExp() - #self.compTOld = self.compTCur - #self.compTCur = compThread(self.XVals, self.YVals, (self.zPiezo.GetPos() - self.zOffset)*1e3,self.fluors, self.noiseMaker, laserPowers=self.laserPowers, intTime=self.intTime*1e-3) - #self.compTCur.start() return 0 @@ -509,9 +531,6 @@ def ExpReady(self): return self.compT.numFramesBuffered() > 0 def ExtractColor(self, chSlice, mode): - #im = self.noiseMaker.noisify(rend_im.simPalmIm(self.XVals, self.YVals, self.zPiezo.GetPos() - self.zOffset,self.fluors, laserPowers=self.laserPowers, intTime=self.intTime*1e-3))[:,:].astype('uint16') - - #chSlice[:,:] = self.noiseMaker.noisify(rend_im.simPalmIm(self.XVals, self.YVals, (self.zPiezo.GetPos() - self.zOffset)*1e3,self.fluors, laserPowers=self.laserPowers, intTime=self.intTime*1e-3))[:,:].astype('uint16') try: d = self.compT.getIm() #print d.nbytes, chSlice.nbytes @@ -520,12 +539,8 @@ def ExtractColor(self, chSlice, mode): #chSlice[:,:] = d #grab image from completed computation thread #self.compTOld = None #set computation thread to None such that we get an error if we try and obtain the same result twice except AttributeError: # triggered if called with None - print("Grabbing problem: probably called with 'None' thread") - #pylab.figure(2) - #pylab.hist([f.state for f in self.fluors], [0, 1, 2, 3], hold=False) - #pylab.gca().set_xticks([0.5,1.5,2.5,3.5]) - #pylab.gca().set_xticklabels(['Caged', 'On', 'Blinked', 'Bleached']) - #pylab.show() + logger.error("Grabbing problem: probably called with 'None' thread") + def GetNumImsBuffered(self): return self.compT.numFramesBuffered() @@ -578,10 +593,20 @@ def GenStartMetadata(self, mdh): mdh['Multiview.NumROIs'] = nChans mdh['Multiview.ROISize'] = [x_chan_pixels, y_pixels] mdh['Multiview.ChannelColor'] = list(chan_specs) + mdh['Splitter.Flip'] = False + + # write shift information (zero shifts) + mdh['chroma.dx'] = '{"PYME.Analysis.points.twoColour.lin2Model": {"mx": 0, "my": 0, "x0": 0}}' + mdh['chroma.dy'] = '{"PYME.Analysis.points.twoColour.lin2Model": {"mx": 0, "my": 0, "x0": 0}}' + for i in range(nChans): mdh['Multiview.ROI%dOrigin' % i] = [i*x_chan_pixels, 0] mdh['Splitter.Channel%dROI' % i] = [i*x_chan_pixels, 0, x_chan_pixels, y_pixels] + + mdh['Simulator.ChanZOffsets'] = self._chan_z_offsets + mdh['Simulator.ChanSpecs'] = self._chan_specs + #functions to make us look more like andor camera def GetEMGain(self): return self.noiseMaker.EMGain diff --git a/PYME/Acquire/Hardware/Simulator/fakePiezo.py b/PYME/Acquire/Hardware/Simulator/fakePiezo.py index f54fc8d5e..edde5e316 100755 --- a/PYME/Acquire/Hardware/Simulator/fakePiezo.py +++ b/PYME/Acquire/Hardware/Simulator/fakePiezo.py @@ -22,6 +22,9 @@ ################## from PYME.Acquire.Hardware.Piezos.base_piezo import PiezoBase +import threading +import numpy as np +import time class FakePiezo(PiezoBase): gui_description = 'Fake %s-piezo' @@ -64,4 +67,29 @@ def GetMax(self, iChan=1): def __getattr__(self, name): if name == 'lastPos': return self.curpos - else: raise AttributeError(name) # <<< DON'T FORGET THIS LINE !! + else: + raise AttributeError(name) # <<< DON'T FORGET THIS LINE !! + +class DriftyFakePiezo(FakePiezo): + """Fake piezo that drifts over time using a random-walk model. Drift rate is specified in um/s^2. + + We use a 100ms time step. + """ + def __init__(self, maxtravel = 100.00, drift_rate = 0.01): + FakePiezo.__init__(self, maxtravel) + self._100ms_drift_rate = drift_rate*np.sqrt(0.1) + + self.drift = 0.0 # initial drift + + self._drift_thread = threading.Thread(target=self._drift) + self._drift_thread.setDaemon(True) + self._drift_thread.start() + + def _drift(self): + while True: + self.drift += self._100ms_drift_rate*np.random.randn() + time.sleep(0.1) + + @property + def effective_pos(self): + return self.curpos + self.drift diff --git a/PYME/Acquire/Hardware/Simulator/fluor.py b/PYME/Acquire/Hardware/Simulator/fluor.py index f667bb260..265c35173 100644 --- a/PYME/Acquire/Hardware/Simulator/fluor.py +++ b/PYME/Acquire/Hardware/Simulator/fluor.py @@ -21,7 +21,6 @@ # ################## -from scipy import * import numpy as np import threading import time @@ -56,7 +55,7 @@ def ConstIllum(fluors, position): return 1.0 def createSimpleTransitionMatrix(pPA=[1e6,.1,0] , pOnDark=[0,0,.1], pDarkOn=[0,.001,0], pOnBleach=[0,0,0], pCagedBlinked = [0,0,0]): - M = zeros((states.n,states.n,len(pPA)), 'f') + M = np.zeros((states.n,states.n,len(pPA)), 'f') M[states.caged, states.active, :] = pPA M[states.active, states.blinked, :] = pOnDark M[states.blinked, states.active, :] = pDarkOn @@ -64,58 +63,81 @@ def createSimpleTransitionMatrix(pPA=[1e6,.1,0] , pOnDark=[0,0,.1], pDarkOn=[0,. M[states.caged, states.blinked, :] = pCagedBlinked return M -class fluorophore: - def __init__(self, x, y, z, transitionProbablilities, excitationCrossections, thetas = [0,0], initialState=states.active, activeState=states.active): - """Create a new 'fluorophore' having one dark state where: - transitionProbablilities is a 4x4x[number of laser wavelengths + 1] tensor of transition probablilites (units = 1/mJ) - the diagonal elements (transition from one state to itself) should be zero as they'll be calculated later to make sum(P) =1 - excitationCrossections is an array of length [number of laser wavelengths] giving the number of photons per mJ emmited when in the on state - thetas is an array of length [number of laser wavelengths] giving the angle (in rad.) between dipole moment of the fluoropore and the laser polarisations - initialState is the initial state of the fluorophore""" - - self.x = x; - self.y = y; - self.z = z; - - self.state = initialState - self.activeState = activeState - self.thetas = thetas - self.transitionProbabilities = transitionProbablilities * concatenate(([1], abs(cos(thetas))),0) - self.excitationCrossections = excitationCrossections *abs(cos(thetas)) - - def illuminate(self, laserPowers, expTime): - dose = concatenate(([1],laserPowers),0)*expTime - #grab transition matrix - transVec = (self.transitionProbabilities[self.state,:,:]*dose).sum(1) - transVec[self.state]= 1 - transVec.sum() - transCs = transVec.cumsum() +# class fluorophore: +# def __init__(self, x, y, z, transitionProbablilities, excitationCrossections, thetas = [0,0], initialState=states.active, activeState=states.active): +# """Create a new 'fluorophore' having one dark state where: +# transitionProbablilities is a 4x4x[number of laser wavelengths + 1] tensor of transition probablilites (units = 1/mJ) +# the diagonal elements (transition from one state to itself) should be zero as they'll be calculated later to make sum(P) =1 +# excitationCrossections is an array of length [number of laser wavelengths] giving the number of photons per mJ emmited when in the on state +# thetas is an array of length [number of laser wavelengths] giving the angle (in rad.) between dipole moment of the fluoropore and the laser polarisations +# initialState is the initial state of the fluorophore""" + +# self.x = x +# self.y = y +# self.z = z + +# self.state = initialState +# self.activeState = activeState +# self.thetas = thetas +# self.transitionProbabilities = transitionProbablilities * np.concatenate(([1], abs(np.cos(thetas))),0) +# self.excitationCrossections = excitationCrossections *abs(np.cos(thetas)) + +# def illuminate(self, laserPowers, expTime): +# dose = np.concatenate(([1],laserPowers),0)*expTime +# #grab transition matrix +# transVec = (self.transitionProbabilities[self.state,:,:]*dose).sum(1) +# transVec[self.state]= 1 - transVec.sum() +# transCs = transVec.cumsum() - r = rand() +# r = np.random.RandomState().rand() # replace with np.random.default_rng() on later numpy versions - for i in range(len(transVec)): - if (r < transCs[i]): - self.state = i - if (i == self.activeState): - return (laserPowers*self.excitationCrossections).sum()*expTime - else: - return 0 +# for i in range(len(transVec)): +# if (r < transCs[i]): +# self.state = i +# if (i == self.activeState): +# return (laserPowers*self.excitationCrossections).sum()*expTime +# else: +# return 0 -class fluors: +class Fluorophores(object): + ''' + Base class representing a collection of fluorophores + + key method is `illuminate()` which conducts the necessary state transisitions and returns the fluoresence intensity of each + fluorophore for a given illumination intensity. + ''' def __init__(self,x, y, z, transitionProbablilities, excitationCrossections, thetas = [0,0], initialState=states.active, activeState=states.active): - self.fl = zeros(len(x), [('x', 'f'),('y', 'f'),('z', 'f'),('exc', '2f'), ('abcosthetas', '2f'),('state', 'i')]) + self.fl = np.zeros(len(x), [('x', 'f'),('y', 'f'),('z', 'f'),('exc', '2f'), ('abcosthetas', '2f'),('state', 'i')]) self.fl['x'] = x self.fl['y'] = y self.fl['z'] = z #fl['exc'][:] = abs(cos(thetas)) self.fl['exc'][:] = excitationCrossections - self.fl['abcosthetas'][:] = abs(cos(thetas)) + self.fl['abcosthetas'][:] = abs(np.cos(thetas)) self.fl['state'][:] = initialState self.transitionTensor = transitionProbablilities.astype('f') self.activeState = activeState + + self._bounds = (x.min()-100, y.min()-100,x.max()+100, y.max()+100) #self.TM = self.transitionTensor[self.fl['state'],:,:].copy() #self.illuminationFunction = illuminationFunction + def hit_test(self, box): + ''' + return true if any of our points are within the given bounding box + + Parameters + ---------- + box : 4-tuple of float + (x0, y0, x1, y1) + ''' + + x0, y0, x1, y1 = box + xb0, yb0, xb1, yb1 = self._bounds + + return ((xb0 < x1)*(yb0 < y1)*(xb1 > x0)*(yb1>y0)) > 0 + #return fl if HAVE_ILLUMINATE_MOD: #use faster cythoned version of function if available @@ -125,7 +147,7 @@ def illuminate(self,laserPowers, expTime, position=[0,0,0], illuminationFunction return illuminate.illuminate(self.transitionTensor, self.fl, self.fl['state'], self.fl['abcosthetas'], dose, ilFrac, self.activeState) else: def illuminate(self, laserPowers, expTime, position=[0,0,0], illuminationFunction = 'ConstIllum'): - dose = concatenate(([1.0],laserPowers),0)*expTime + dose = np.concatenate(([1.0],laserPowers),0)*expTime #grab transition matrix transMat = self.transitionTensor[self.fl['state'],:,:].copy() @@ -149,44 +171,60 @@ def illuminate(self, laserPowers, expTime, position=[0,0,0], illuminationFunctio transVec[m, i]= 1 - tvs[m] transCs = transVec.cumsum(1) - r = rand(len(self.fl)) + r = np.random.RandomState().rand(len(self.fl)) # replace with np.random.default_rng() on later numpy versions self.fl['state'] = (transCs < r[:, None]).sum(1) return (self.fl['state'] == self.activeState)*(self.fl['exc'][:,0]*c0 + self.fl['exc'][:,1]*c1) -class specFluors(fluors): - def __init__(self,x, y, z, transitionProbablilities, excitationCrossections, thetas = [0,0], spectralSig = [1,0], initialState=states.caged, activeState=states.active): - self.fl = zeros(len(x), [('x', 'f'),('y', 'f'),('z', 'f'),('exc', '2f'), ('abcosthetas', '2f'),('state', 'i'), ('spec', '2f')]) +class SpectralFluorophores(Fluorophores): + ''' + Extension of the base fluorophore class which also records a spectral signature for ratiometric imaging. + ''' + def __init__(self,x, y, z, transitionProbablilities, excitationCrossections, thetas = [0,0], spectralSig = [1,0], initialState=states.active, activeState=states.active): + self.fl = np.zeros(len(x), [('x', 'f'),('y', 'f'),('z', 'f'),('exc', '2f'), ('abcosthetas', '2f'),('state', 'i'), ('spec', '2f')]) self.fl['x'] = x self.fl['y'] = y self.fl['z'] = z #fl['exc'][:] = abs(cos(thetas)) self.fl['exc'][:] = excitationCrossections - self.fl['abcosthetas'][:] = abs(cos(thetas)) + self.fl['abcosthetas'][:] = abs(np.cos(thetas)) self.fl['state'][:] = initialState self.fl['spec'][:] = spectralSig self.transitionTensor = transitionProbablilities.astype('f') self.activeState = activeState + + self._bounds = (x.min()-100, y.min()-100,x.max()+100, y.max()+100) + + def status(self): + cts = np.zeros(4) + for i in range(len(cts)): + cts[i] = int((self.fl['state'] == i).sum()) + + return cts + + def __repr__(self): + return 'SpectralFluorophores object: %s' % self.status() -class EmpiricalHistFluors(fluors): +class EmpiricalHistFluors(Fluorophores): """ Fluorophores with on/off times generated by empirical data. - Note - ---- - This class relies on empirical data. For fluorophore simulation based on - first principles see fluors. + Notes + ----- + This class relies on empirical data. For fluorophore simulation based on + first principles see Fluorophores. + """ def __init__(self, x, y, z, histogram, spectralSig = [1,0], initialState=states.active, activeState=states.active): self.histogram = histogram - self.fl = zeros(len(x), [('x', 'f'), ('y', 'f'), ('z', 'f'), + self.fl = np.zeros(len(x), [('x', 'f'), ('y', 'f'), ('z', 'f'), ('state', 'i'), ('spec', '2f')]) self.fl['x'] = x self.fl['y'] = y @@ -194,6 +232,8 @@ def __init__(self, x, y, z, histogram, spectralSig = [1,0], self.fl['state'][:] = initialState self.fl['spec'][:] = spectralSig + self._bounds = (x.min()-100, y.min()-100,x.max()+100, y.max()+100) + self.laserPowers = [1.0,1.0] self.expTime = 0.01 @@ -272,7 +312,7 @@ def illuminate(self, laserPowers, expTime, position=[0, 0, 0], self.expTime = expTime self.count_on = [] self.count_off = [] - print('exptime', self.expTime) + #print('exptime', self.expTime) with self.stateQueue.mutex: self.stateQueue.queue.clear() time.sleep(.5) diff --git a/PYME/Acquire/Hardware/Simulator/meson.build b/PYME/Acquire/Hardware/Simulator/meson.build new file mode 100644 index 000000000..1e1bb254c --- /dev/null +++ b/PYME/Acquire/Hardware/Simulator/meson.build @@ -0,0 +1,41 @@ + +# Boilerplate to make sure things go in the right place - TODO can we do some of this in the top-level meson.build? +#py = import('python').find_installation(pure: false) +#np_include_dir = run_command(py, ['-c', '"import numpy; print(numpy.get_include())"'], check: true).stdout().strip() +install_dir = py.get_install_dir() / 'PYME/Acquire/Hardware/Simulator' + +py_sources = files( + 'fakePiezo.py', + 'simcontrol.py', + '__init__.py', + 'fakeCam.py', + 'lasersliders.py', + 'dSimControl.py', + 'fluor.py', + 'setup.py', + 'simui_wx.py', + 'EmpiricalHist.py', + 'rend_im.py', +) + +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Simulator') + +data_files = files( + 'illuminate.pyx', +) + +install_data(data_files, install_dir: install_dir) + +#FIXME - Extension() +py.extension_module( + 'illuminate', + [ + 'illuminate.pyx', + ], + include_directories: [np_include_dir], + subdir: 'PYME/Acquire/Hardware/Simulator', + dependencies: py.dependency(), + c_args: ['-O3', '-fno-exceptions', '-ffast-math'], + install: true, + #cpp_args: ['-std=c++17'], +) \ No newline at end of file diff --git a/PYME/Acquire/Hardware/Simulator/rend_im.py b/PYME/Acquire/Hardware/Simulator/rend_im.py index 1e744a0bf..9a3feed8c 100755 --- a/PYME/Acquire/Hardware/Simulator/rend_im.py +++ b/PYME/Acquire/Hardware/Simulator/rend_im.py @@ -22,7 +22,6 @@ ################## from PYME.Analysis.PSFGen import * -from scipy import * from numpy.fft import ifftshift, fftn, ifftn from . import fluor from PYME.Analysis import MetaData @@ -47,9 +46,9 @@ def renderIm(X, Y, z, points, roiSize, A): #X = mgrid[xImSlice] #Y = mgrid[yImSlice] - im = zeros((len(X), len(Y)), 'f') + im = np.zeros((len(X), len(Y)), 'f') - P = arange(0,1.01,.1) + P = np.arange(0,1.01,.1) for (x0,y0,z0) in points: ix = abs(X - x0).argmin() @@ -85,6 +84,7 @@ def interpModel(chan=0): dz = None mdh = MetaData.NestedClassMDHandler(MetaData.TIRFDefault) +mdh['voxelsize.z'] = 0.05 # set 50nm z spacing so we are not relying quite as much on interpolation for the z shape def set_pixelsize_nm(pixelsize): @@ -98,13 +98,13 @@ def genTheoreticalModel(md, zernikes={}, **kwargs): if True:#not dx == md.voxelsize.x*1e3 or not dy == md.voxelsize.y*1e3 or not dz == md.voxelsize.z*1e3: vs = md.voxelsize_nm - IntXVals = vs.x*mgrid[-150:150] - IntYVals = vs.y*mgrid[-150:150] - IntZVals = vs.z*mgrid[-30:30] + IntXVals = vs.x * np.mgrid[-150:150] + IntYVals = vs.y * np.mgrid[-150:150] + IntZVals = vs.z * np.mgrid[-30:30] dx, dy, dz = vs - P = arange(0,1.01,.01) + P = np.arange(0,1.01,.01) #interpModel = genWidefieldPSF(IntXVals, IntYVals, IntZVals, P ,1e3, 0, 0, 0, 2*pi/525, 1.47, 10e3).astype('f') im = fourierHNA.GenZernikeDPSF(IntZVals, zernikes, X=IntXVals, Y=IntYVals, dx=vs.x, **kwargs) @@ -125,9 +125,9 @@ def genTheoreticalModel4Pi(md, zernikes=[{},{}], phases=[0, np.pi/2, np.pi, 3*np if True:#not dx == md.voxelsize.x*1e3 or not dy == md.voxelsize.y*1e3 or not dz == md.voxelsize.z*1e3: vs = md.voxelsize_nm - IntXVals = vs.x*mgrid[-150:150] - IntYVals = vs.y*mgrid[-150:150] - IntZVals = 20*mgrid[-60:60] + IntXVals = vs.x * np.mgrid[-150:150] + IntYVals = vs.y * np.mgrid[-150:150] + IntZVals = 20 * np.mgrid[-60:60] dx, dy = vs.x, vs.y dz = 20.#md.voxelsize.z*1e3 @@ -163,9 +163,9 @@ def setModel(modName, md): mod, vs_nm = load_psf.load_psf(modName) mod = resizePSF(mod, interpModel().shape) - IntXVals = vs_nm.x*mgrid[-(mod.shape[0]/2.):(mod.shape[0]/2.)] - IntYVals = vs_nm.y*mgrid[-(mod.shape[1]/2.):(mod.shape[1]/2.)] - IntZVals = vs_nm.z*mgrid[-(mod.shape[2]/2.):(mod.shape[2]/2.)] + IntXVals = vs_nm.x * np.mgrid[-(mod.shape[0]/2.):(mod.shape[0]/2.)] + IntYVals = vs_nm.y * np.mgrid[-(mod.shape[1]/2.):(mod.shape[1]/2.)] + IntZVals = vs_nm.z * np.mgrid[-(mod.shape[2]/2.):(mod.shape[2]/2.)] dx, dy, dz = vs_nm @@ -173,9 +173,9 @@ def setModel(modName, md): interpModel_by_chan[0] = np.maximum(mod/mod[:,:,len(IntZVals)/2].sum(), 0) #normalise to 1 and clip def interp(X, Y, Z): - X = atleast_1d(X) - Y = atleast_1d(Y) - Z = atleast_1d(Z) + X = np.atleast_1d(X) + Y = np.atleast_1d(Y) + Z = np.atleast_1d(Z) ox = X[0] oy = Y[0] @@ -286,9 +286,9 @@ def interp2(X, Y, Z): return m def interp3(X, Y, Z): - X = atleast_1d(X) - Y = atleast_1d(Y) - Z = atleast_1d(Z) + X = np.atleast_1d(X) + Y = np.atleast_1d(Y) + Z = np.atleast_1d(Z) ox = X[0] oy = Y[0] @@ -303,9 +303,9 @@ def interp3(X, Y, Z): @fluor.registerIllumFcn def PSFIllumFunction(fluors, position): im = interpModel() - xi = maximum(minimum(round_((fluors['x'] - position[0])/dx + im.shape[0]/2).astype('i'), im.shape[0]-1), 0) - yi = maximum(minimum(round_((fluors['y'] - position[1])/dy + im.shape[1]/2).astype('i'), im.shape[1]-1), 0) - zi = maximum(minimum(round_((fluors['z'] - position[2])/dz + im.shape[2]/2).astype('i'), im.shape[2]-1), 0) + xi = np.maximum(np.minimum(np.round_((fluors['x'] - position[0])/dx + im.shape[0]/2).astype('i'), im.shape[0]-1), 0) + yi = np.maximum(np.minimum(np.round_((fluors['y'] - position[1])/dy + im.shape[1]/2).astype('i'), im.shape[1]-1), 0) + zi = np.maximum(np.minimum(np.round_((fluors['z'] - position[2])/dz + im.shape[2]/2).astype('i'), im.shape[2]-1), 0) return im[xi, yi, zi] @@ -334,10 +334,25 @@ def setIllumPattern(pattern, z0): illPattern = abs(ifftshift(ifftn(fftn(il)*fftn(ps)))).astype('f') illPCache = None + +illum_roi_size=256 + +@fluor.registerIllumFcn +def ROIIllumFunction(fluors, position): + ''' + Very crude ROI-based illumination. Assumes hard edges, no diffraction. + ''' + + xi = np.round_((fluors['x'] - position[0])/mdh.voxelsize_nm.x) + yi = np.round_((fluors['y'] - position[1])/mdh.voxelsize_nm.y) + #zi = np.round_((fluors['z'] - position[2])/dz) + + return (xi>0)*(xi0)*(yi 0): -# ix = abs(X - f.x).argmin() -# iy = abs(Y - f.y).argmin() -# -# imp =genWidefieldPSF(X[(ix - roiSize):(ix + roiSize + 1)], Y[(iy - roiSize):(iy + roiSize + 1)], z, P,A*1e3, f.x, f.y, f.z,depthInSample=0) -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# return im - - -# def simPalmImF(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=10, laserPowers = [.1,1]): -# im = zeros((len(X), len(Y)), 'f') -# -# if fluors is None: -# return im -# -# P = arange(0,1.01,.1) -# -# A = zeros(len(fluors.fl)) -# -# #tLock.acquire() -# -# for n in range(numSubSteps): -# A += fluors.illuminate(laserPowers,intTime/numSubSteps) -# -# #tLock.release() -# -# flOn = where(A > 0)[0] -# -# #print flOn -# -# for i in flOn: -# ix = abs(X - fluors.fl['x'][i]).argmin() -# iy = abs(Y - fluors.fl['y'][i]).argmin() -# -# imp =genWidefieldPSF(X[(ix - roiSize):(ix + roiSize + 1)], Y[(iy - roiSize):(iy + roiSize + 1)], z, P,A[i]*1e3, fluors.fl['x'][i], fluors.fl['y'][i], fluors.fl['z'][i],depthInSample=50e3) -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# return im - - -# def simPalmImFI_(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=15, laserPowers = [.1,1], position=[0,0,0], illuminationFunction='ConstIllum'): -# if interpModel is None: -# genTheoreticalModel(MetaData.TIRFDefault) -# -# im = zeros((len(X), len(Y)), 'f') -# -# if fluors is None: -# return im -# -# #P = arange(0,1.01,.1) -# -# A = zeros(len(fluors.fl)) -# -# #tLock.acquire() -# -# for n in range(numSubSteps): -# A += fluors.illuminate(laserPowers,intTime/numSubSteps, position=position, illuminationFunction=illuminationFunction) -# -# -# #print position -# #tLock.release() -# -# flOn = where(A > 0.1)[0] -# -# #print flOn -# dx = X[1] - X[0] -# dy = Y[1] - Y[0] -# -# #print interpModel.shape, interpModel.strides -# -# maxz = dz*interpModel.shape[2]/2. -# #s= min(roiSize, 20- roiSize)*dx -# s1 = min(roiSize, 20- roiSize) -# -# x0 = X[0] -# y0 = Y[0] -# ix_l = -s1 -# ix_h = len(X) + s1 -# iy_l = -s1 -# iy_h = len(Y) + s1 -# -# -# for i in flOn: -# x = fluors.fl['x'][i] #+ position[0] -# y = fluors.fl['y'][i] #+ position[1] -# -# #delX = abs(X - x) -# #delY = abs(Y - y) -# -# #ix = delX.argmin() -# #iy = delY.argmin() -# -# ix = int((x - x0)/dx) -# iy = int((y - y0)/dy) -# -# -# #if delX[ix] < s and delY[iy] < s: -# if (ix > ix_l) and (ix < ix_h) and (iy > iy_l) and (iy < iy_h): -# #print ix, iy -# -# ix0 = max(ix - roiSize, 0) -# ix1 = min(ix + roiSize + 1, im.shape[0]) -# iy0 = max(iy - roiSize, 0) -# iy1 = min(iy + roiSize + 1, im.shape[1]) -# #imp =interp3(X[max(ix - roiSize, 0):(ix + roiSize + 1)] - x, Y[max(iy - roiSize, 0):(iy + roiSize + 1)] - y, z - fluors.fl['z'][i])* A[i] -# imp = cInterp.Interpolate(interpModel, -(X[ix0] - x), -(Y[iy0] - y), min(max(z - fluors.fl['z'][i], -maxz), maxz), ix1-ix0, iy1-iy0,dx,dy,dz)* A[i] -# -# #if imp.min() < 0 or isnan(A[i]): -# # print ix0, ix1, iy0, iy1, (X[ix0] - x)/dx, (Y[iy0]- y)/dx, A[i], imp.min() -# im[ix0:ix1, iy0:iy1] += imp[:,:,0] -# -# return im - - def _rFluorSubset(im, fl, A, x0, y0, z, dx, dy, dz, maxz, ChanXOffsets=[0,], ChanZOffsets=[0,], ChanSpecs = None): if ChanSpecs is None: @@ -509,21 +399,21 @@ def _rFluorSubset(im, fl, A, x0, y0, z, dx, dy, dz, maxz, ChanXOffsets=[0,], Cha z_, A * fl['spec'][:, spec_chan], roiSize, dx, dy, dz) -def simPalmImFI(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=100, laserPowers = [.1,1], position=[0,0,0], illuminationFunction='ConstIllum', ChanXOffsets=[0,], ChanZOffsets=[0,], ChanSpecs = None): +def simPalmImFI(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=100, laserPowers = [.1,1], position=[0,0,0], illuminationFunction='ConstIllum', ChanXOffsets=[0,], ChanZOffsets=[0,], ChanSpecs = None, im=None): if interpModel() is None: genTheoreticalModel(mdh) - im = zeros((len(X), len(Y)), 'f') + if im is None: + im = np.zeros((len(X), len(Y)), 'f') if fluors is None: return im - A = zeros(len(fluors.fl), 'f') + A = np.zeros(len(fluors.fl), 'f') for n in range(numSubSteps): A += fluors.illuminate(laserPowers,intTime/numSubSteps, position=position, illuminationFunction=illuminationFunction) - flOn = where(A > 0.1)[0] dx = X[1] - X[0] dy = Y[1] - Y[0] @@ -537,14 +427,9 @@ def simPalmImFI(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=100, laserPo fl = fluors.fl[m] A2 = A[m] - - #z2 = np.minimum(np.maximum(z - fl['z'], -maxz), maxz)#.astype('f') - - #roiS = np.minimum(3 + np.abs(z2)*(2.5/70), 100).astype('i') - #roiS = np.minimum(8 + np.abs(z2)*(2.5/70), 140).astype('i') - nCPUs = int(min(multiprocessing.cpu_count(), len(flOn))) + nCPUs = int(min(multiprocessing.cpu_count(), len(A2))) if nCPUs > 0: threads = [threading.Thread(target = _rFluorSubset, args=(im, fl[i::nCPUs], A2[i::nCPUs], x0, y0, z, dx, dy, dz, maxz, ChanXOffsets, ChanZOffsets, ChanSpecs)) for i in range(nCPUs)] @@ -555,122 +440,7 @@ def simPalmImFI(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=100, laserPo for p in threads: p.join() - #for i in flOn: - # x = fluors.fl['x'][i] #+ position[0] - # y = fluors.fl['y'][i] #+ position[1] - # cInterp.InterpolateInplace(interpModel, im, x - x0, y - y0, min(max(z - fluors.fl['z'][i], -maxz), maxz-dz), roiSize, roiSize,dx,dy,dz, A[i]) - return im -# def simPalmImFSpec(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=10, laserPowers = [.1,1], deltaY=64, deltaZ = 300): -# im = zeros((len(X), len(Y)), 'f') -# -# deltaY = (Y[1] - Y[0])*deltaY #convert to nm -# #print deltaY -# -# if fluors is None: -# return im -# -# P = arange(0,1.01,.1) -# -# A = zeros(len(fluors.fl)) -# -# for n in range(numSubSteps): -# A += fluors.illuminate(laserPowers,intTime/numSubSteps) -# -# flOn = where(A > 0)[0] -# -# #print flOn -# -# for i in flOn: -# ix = abs(X - fluors.fl['x'][i]).argmin() -# iy = abs(Y - deltaY - fluors.fl['y'][i]).argmin() -# -# imp =fluors.fl[i]['spec'][0]*genWidefieldPSF(X[(ix - roiSize):(ix + roiSize + 1)], Y[(iy - roiSize):(iy + roiSize + 1)], z, P,A[i]*1e3, fluors.fl['x'][i], fluors.fl['y'][i] + deltaY , fluors.fl['z'][i]) -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# -# ix = abs(X - fluors.fl['x'][i]).argmin() -# iy = abs(flipud(Y) - deltaY - fluors.fl['y'][i]).argmin() -# imp =fluors.fl[i]['spec'][1]*genWidefieldPSF(X[(ix - roiSize):(ix + roiSize + 1)], flipud(Y)[(iy - roiSize):(iy + roiSize + 1)], z, P,A[i]*1e3, fluors.fl['x'][i], fluors.fl['y'][i] + deltaY, fluors.fl['z'][i]) -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# return im - -# def simPalmImFSpecI(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=10, laserPowers = [.1,1], deltaY=64, deltaZ = 300): -# if interpModel is None: -# genTheoreticalModel(MetaData.TIRFDefault) -# -# im = zeros((len(X), len(Y)), 'f') -# -# deltaY = (Y[1] - Y[0])*deltaY #convert to nm -# #print deltaY -# -# if fluors is None: -# return im -# -# P = arange(0,1.01,.1) -# -# A = zeros(len(fluors.fl)) -# -# for n in range(numSubSteps): -# A += fluors.illuminate(laserPowers,intTime/numSubSteps) -# -# flOn = where(A > 0)[0] -# -# #print flOn -# -# for i in flOn: -# ix = abs(X - fluors.fl['x'][i]).argmin() -# iy = abs(Y - deltaY - fluors.fl['y'][i]).argmin() -# -# imp =fluors.fl[i]['spec'][0]*A[i]*1e3*interp3(X[(ix - roiSize):(ix + roiSize + 1)] - fluors.fl['x'][i], Y[(iy - roiSize):(iy + roiSize + 1)] - (fluors.fl['y'][i]+ deltaY), z - fluors.fl['z'][i]) -# -# if not imp.shape[2] == 0: -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# iy2 = abs(flipud(Y) - deltaY - fluors.fl['y'][i]).argmin() -# -# imp =fluors.fl[i]['spec'][1]*A[i]*1e3*interp3(X[(ix - roiSize):(ix + roiSize + 1)] - fluors.fl['x'][i], Y[(iy - roiSize):(iy + roiSize + 1)] - (fluors.fl['y'][i] + deltaY), z - fluors.fl['z'][i]+deltaZ) -# -# if not imp.shape[2] == 0: -# im[(ix - roiSize):(ix + roiSize + 1), (iy2 - roiSize):(iy2 + roiSize + 1)] += imp[:, ::-1, 0] -# -# -# return im -# def simPalmImFBP(X,Y, z, fluors, intTime=.1, numSubSteps=10, roiSize=10, laserPowers = [.1,1], deltaY=64, deltaZ = 500): -# im = zeros((len(X), len(Y)), 'f') -# -# deltaY = (Y[1] - Y[0])*deltaY #convert to nm -# #print deltaY -# -# if fluors is None: -# return im -# -# P = arange(0,1.01,.1) -# -# A = zeros(len(fluors.fl)) -# -# for n in range(numSubSteps): -# A += fluors.illuminate(laserPowers,intTime/numSubSteps) -# -# flOn = where(A > 0)[0] -# -# #print flOn -# -# for i in flOn: -# ix = abs(X - fluors.fl['x'][i]).argmin() -# iy = abs(Y - deltaY - fluors.fl['y'][i]).argmin() -# -# imp =genWidefieldPSF(X[(ix - roiSize):(ix + roiSize + 1)], Y[(iy - roiSize):(iy + roiSize + 1)], z, P,A[i]*1e3, fluors.fl['x'][i], fluors.fl['y'][i] + deltaY , fluors.fl['z'][i]) -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# -# ix = abs(X - fluors.fl['x'][i]).argmin() -# iy = abs(flipud(Y) - deltaY - fluors.fl['y'][i]).argmin() -# imp =genWidefieldPSF(X[(ix - roiSize):(ix + roiSize + 1)], flipud(Y)[(iy - roiSize):(iy + roiSize + 1)], z + deltaZ, P,A[i]*1e3, fluors.fl['x'][i], fluors.fl['y'][i] + deltaY, fluors.fl['z'][i]) -# im[(ix - roiSize):(ix + roiSize + 1), (iy - roiSize):(iy + roiSize + 1)] += imp[:,:,0] -# -# return im diff --git a/PYME/Acquire/Hardware/Simulator/setup.py b/PYME/Acquire/Hardware/Simulator/setup.py index 9ab0a8654..e95603b34 100644 --- a/PYME/Acquire/Hardware/Simulator/setup.py +++ b/PYME/Acquire/Hardware/Simulator/setup.py @@ -40,7 +40,7 @@ def configuration(parent_package = '', top_path = None): ext = Extension(name='.'.join([parent_package, 'Simulator', 'illuminate']), sources=[os.path.join(os.path.dirname(__file__),'illuminate.pyx')], include_dirs=get_numpy_include_dirs(), - extra_compile_args=['-O3', '-fno-exceptions', '-ffast-math', '-march=native', '-mtune=native'], + extra_compile_args=['-O3', '-fno-exceptions', '-ffast-math'], #'-march=native', '-mtune=native'], extra_link_args=linkArgs) config = Configuration('Simulator', parent_package, top_path, ext_modules=cythonize([ext])) diff --git a/PYME/Acquire/Hardware/Simulator/simcontrol.py b/PYME/Acquire/Hardware/Simulator/simcontrol.py index db4132ea4..e772eff13 100644 --- a/PYME/Acquire/Hardware/Simulator/simcontrol.py +++ b/PYME/Acquire/Hardware/Simulator/simcontrol.py @@ -1,15 +1,16 @@ -from . import fluor -from . import wormlike2 import json -#import pylab -import scipy import numpy as np +import scipy + +from PYME.simulation import wormlike2 +from . import fluor from . import rend_im import logging logger = logging.getLogger(__name__) -from PYME.recipes.traits import HasTraits, Float, Dict, Bool, List +from PYME.recipes.traits import HasTraits, Float, Dict, Bool, List, Tuple, Int, Instance +from PYME.simulation import pointsets class PSFSettings(HasTraits): @@ -22,7 +23,7 @@ class PSFSettings(HasTraits): four_pi = Bool(False) def default_traits_view(self): - from traitsui.api import View, Item, Group, ListEditor + from traitsui.api import View, Item #from PYME.ui.custom_traits_editors import CBEditor return View(Item(name='wavelength_nm'), @@ -35,6 +36,72 @@ def default_traits_view(self): resizable=True, buttons=['OK']) + +class Group(HasTraits): + generators = List(Instance(HasTraits)) + + def points(self): + for g in self.generators: + for pts in g.points(): + yield pts + +class AssignChannel(HasTraits): + channel = Int(0) + generator = Instance(HasTraits) + + def points(self): + for pts in self.generator.points(): + pts[:,3] = self.channel + yield pts +class Shift(HasTraits): + dx = Float(0) + dy = Float(0) + + generator = Instance(HasTraits) + + def points(self): + for pts in self.generator.points(): + pts[:,0] += self.dx + pts[:,1] += self.dy + yield pts + +class RandomShift(HasTraits): + magnitude = Float(1000) + + generator = Instance(HasTraits) + + def points(self): + dx, dy = np.random.uniform(-self.magnitude, self.magnitude, 2) + for pts in self.generator.points(): + pts[:,0] += dx + pts[:,1] += dy + yield pts +class RandomDistribution(HasTraits): + n_instances = Int(1) + region_size = Float(5000) + generator = Instance(HasTraits) + # force one of the points to be at the origin (dirty hack to make sure there is a structure present in the simulator at startup) + force_at_origin = Bool(False) + + def points(self): + xp = self.region_size*np.random.uniform(-1, 1, self.n_instances) + yp = self.region_size*np.random.uniform(-1, 1, self.n_instances) + + if self.force_at_origin: + xp[0] = 0.0 + yp[0] = 0.0 + + + for xi, yi in zip(xp, yp): + for p in self.generator.points(): + p1 = np.copy(p) + p1[:,0] += xi + p1[:,1] += yi + + yield p1 + + + class SimController(object): """ Non-GUI part of simulation control""" @@ -42,7 +109,8 @@ def __init__(self, scope=None, states=['Caged', 'On', 'Blinked', 'Bleached'], stateTypes=[fluor.FROM_ONLY, fluor.ALL_TRANS, fluor.ALL_TRANS, fluor.TO_ONLY], transistion_tensor=None, excitation_crossections=(1., 100.), activeState=fluor.states.active, n_chans=1, splitter_info=([0, -200, 300., 500.], [0, 1, 1, 0]), - spectral_signatures=[[1, 0.3], [.7, .7], [0.2, 1]]): + spectral_signatures=[[1, 0.3], [.7, .7], [0.2, 1]], + point_gen=None): """ Parameters @@ -77,7 +145,10 @@ def __init__(self, scope=None, states=['Caged', 'On', 'Blinked', 'Bleached'], self.states = states self.stateTypes = stateTypes self.activeState = activeState - self.scope = scope + self.scope = scope # type: PYME.Acquire.microscope.Microscope + + if scope: + scope.StatusCallbacks.append(self.simulation_status) if (transistion_tensor is None): #use defaults transistion_tensor = fluor.createSimpleTransitionMatrix() @@ -91,50 +162,41 @@ def __init__(self, scope=None, states=['Caged', 'On', 'Blinked', 'Bleached'], self.points = [] self._empirical_hist = None + + self.point_gen = point_gen @property def splitter_info(self): return self.z_offsets[:self.n_chans], self.spec_chans[:self.n_chans] + def gen_fluors_wormlike(self, kbp=50e3, persistLength=1500, numFluors=1000, flatten=False, z_scale=1.0, num_colours=1, wrap=True): import numpy as np #wc = wormlike2.fibre30nm(kbp, 10*kbp/numFluors) - wc = wormlike2.wiglyFibre(kbp, persistLength, kbp / numFluors) + #wc = wormlike2.wiglyFibre(kbp, persistLength, kbp / numFluors) + xp, yp, zp = pointsets.WiglyFibreSource(length=kbp, persistLength=persistLength, numFluors=numFluors, flatten=flatten, zScale=z_scale).getPoints() XVals = self.scope.cam.XVals YVals = self.scope.cam.YVals x_pixels = len(XVals) - y_pixels = len(YVals) - x_chan_pixels = int(x_pixels / self.n_chans) x_chan_size = XVals[x_chan_pixels - 1] - XVals[0] - y_chan_size = YVals[-1] - YVals[0] - wc.xp = wc.xp - wc.xp.mean() + x_chan_size / 2 - wc.yp = wc.yp - wc.yp.mean() + y_chan_size / 2 + #shift to centre of ROI + xp += x_chan_size / 2 + yp += y_chan_size / 2 if wrap: - wc.xp = np.mod(wc.xp, x_chan_size) + XVals[0] - wc.yp = np.mod(wc.yp, y_chan_size) + YVals[0] - - if flatten: - wc.zp *= 0 - else: - wc.zp -= wc.zp.mean() - wc.zp *= z_scale - - self.points = [] + xp = np.mod(xp, x_chan_size) + XVals[0] + yp = np.mod(yp, y_chan_size) + YVals[0] num_colours = min(num_colours, len(self.spectralSignatures)) - - for i in range(len(wc.xp)): - if num_colours > 1: - self.points.append((wc.xp[i], wc.yp[i], wc.zp[i], float(i / ((len(wc.xp) + 1) / num_colours)))) - else: - self.points.append((wc.xp[i], wc.yp[i], wc.zp[i], 0)) + c = np.linspace(0, num_colours, len(xp)).astype('i') + + self.points = np.array([xp,yp,zp,c], 'f').T self.scope.cam.setFluors(None) self.generate_fluorophores() @@ -166,7 +228,7 @@ def set_psf_model(self, psf_settings): label = 'PSF: 4Pi %s [%1.2f NA @ %d nm, zerns=%s]' % ('vectorial' if psf_settings.vectorial else 'scalar', psf_settings.NA, psf_settings.wavelength_nm, z_modes) else: - print('Setting PSF with zernike modes: %s' % z_modes) + logger.info('Setting PSF with zernike modes: %s' % z_modes) rend_im.genTheoreticalModel(rend_im.mdh, zernikes=z_modes, lamb=psf_settings.wavelength_nm, NA=psf_settings.NA, vectorial=psf_settings.vectorial) @@ -185,30 +247,25 @@ def get_psf(self): def save_psf(self, filename): self.get_psf().Save(filename) - - def generate_fluorophores_theoretical(self): - if (len(self.points) == 0): - raise RuntimeError('No points defined') - - points_a = scipy.array(self.points).astype('f') - x = points_a[:, 0] - y = points_a[:, 1] - z = points_a[:, 2] - - if points_a.shape[1] == 4: #4th entry is index into spectrum table - c = points_a[:, 3].astype('i') - spec_sig = scipy.ones((len(x), 2)) + + def fluorophores_from_points_theoretical(self, points): + x = points[:, 0] + y = points[:, 1] + z = points[:, 2] + + if points.shape[1] == 4: #4th entry is index into spectrum table + c = points[:, 3].astype('i') + spec_sig = np.ones((len(x), 2)) spec_sig[:, 0] = self.spectralSignatures[c, 0] spec_sig[:, 1] = self.spectralSignatures[c, 1] - fluors = fluor.specFluors(x, y, z, self.transition_tensor, self.excitation_crossections, + fluors = fluor.SpectralFluorophores(x, y, z, self.transition_tensor, self.excitation_crossections, activeState=self.activeState, spectralSig=spec_sig) else: - fluors = fluor.fluors(x, y, z, self.transition_tensor, self.excitation_crossections, + fluors = fluor.Fluorophores(x, y, z, self.transition_tensor, self.excitation_crossections, activeState=self.activeState) - self.scope.cam.setSplitterInfo(*self.splitter_info) - self.scope.cam.setFluors(fluors) + return fluors def load_empirical_histogram(self, filename): from . import EmpiricalHist @@ -217,24 +274,42 @@ def load_empirical_histogram(self, filename): data = json.load(f) self._empirical_hist = EmpiricalHist(**data[data.keys().pop()]) - def generate_fluorophores_empirical(self): - points_a = scipy.array(self.points).astype('f') - x = points_a[:, 0] - y = points_a[:, 1] - z = points_a[:, 2] + def fluorophores_from_points_emperical(self, points): + x = points[:, 0] + y = points[:, 1] + z = points[:, 2] - fluors = fluor.EmpiricalHistFluors(x, y, z, + if points.shape[1] == 4: #4th entry is index into spectrum table + c = points[:, 3].astype('i') + spec_sig = np.ones((len(x), 2)) + spec_sig[:, 0] = self.spectralSignatures[c, 0] + spec_sig[:, 1] = self.spectralSignatures[c, 1] + + fluors = fluor.EmpiricalHistFluors(x, y, z, + histogram=self._empirical_hist, + activeState=self.activeState, spectralSig=spec_sig) + else: + fluors = fluor.EmpiricalHistFluors(x, y, z, histogram=self._empirical_hist, activeState=self.activeState) - self.scope.cam.setSplitterInfo(*self.splitter_info) - self.scope.cam.setFluors(fluors) + return fluors def generate_fluorophores(self, mode='theoretical'): if mode == 'emperical': - self.generate_fluorophores_empirical() + gen_fcn = self.fluorophores_from_points_emperical else: - self.generate_fluorophores_theoretical() + gen_fcn = self.fluorophores_from_points_theoretical + + if self.point_gen: + objs = [gen_fcn(pts) for pts in self.point_gen.points()] + else: + if (len(self.points) == 0): + raise RuntimeError('No points defined') + objs = [gen_fcn(np.array(self.points).astype('f')),] + + self.scope.cam.setSplitterInfo(*self.splitter_info) + self.scope.cam.set_objects(objs) def change_num_channels(self, n_chans): @@ -248,4 +323,26 @@ def change_num_channels(self, n_chans): except AttributeError: logger.exception('Error setting new camera dimensions') pass + + def simulation_status(self): + if self.scope.cam._objects: + if len(self.scope.cam._objects) > 1: + #fixme for multiple objects + return 'Multiple objects defined' + fl = self.scope.cam._objects[0] + elif self.scope.cam.fluors is None: + return 'No fluorophores defined' + else: + fl = self.scope.cam.fluors + + cts = np.zeros((len(self.states))) + for i in range(len(cts)): + cts[i] = int((fl.fl['state'] == i).sum()) + + status = '/'.join(self.states) + ' = ' + '/'.join(['%d' % c for c in cts]) + + return status + + + diff --git a/PYME/Acquire/Hardware/Simulator/simui_wx.py b/PYME/Acquire/Hardware/Simulator/simui_wx.py index 92cc48300..dffe72d8d 100644 --- a/PYME/Acquire/Hardware/Simulator/simui_wx.py +++ b/PYME/Acquire/Hardware/Simulator/simui_wx.py @@ -15,14 +15,17 @@ wxID_DSIMCONTROLSTCUROBJPOINTS, wxID_DSIMCONTROLSTSTATUS, wxID_DSIMCONTROLTEXPROBE, wxID_DSIMCONTROLTEXSWITCH, wxID_DSIMCONTROLTKBP, wxID_DSIMCONTROLTNUMFLUOROPHORES, -] = [wx.NewId() for _init_ctrls in range(28)] +] = [wx.NewIdRef() for _init_ctrls in range(28)] -[wxID_DSIMCONTROLTREFRESH] = [wx.NewId() for _init_utils in range(1)] +[wxID_DSIMCONTROLTREFRESH] = [wx.NewIdRef() for _init_utils in range(1)] import numpy as np from . import fluor from . import simcontrol +import logging +logger = logging.getLogger(__name__) + class dSimControl(afp.foldPanel): def _init_coll_nTransitionTensor_Pages(self, parent): # generated method, don't edit @@ -37,8 +40,9 @@ def _init_coll_nTransitionTensor_Pages(self, parent): def _init_utils(self): #pass # generated method, don't edit - self.tRefresh = wx.Timer(id=wxID_DSIMCONTROLTREFRESH, owner=self) - self.Bind(wx.EVT_TIMER, self.OnTRefreshTimer, + if self._show_status: + self.tRefresh = wx.Timer(id=wxID_DSIMCONTROLTREFRESH, owner=self) + self.Bind(wx.EVT_TIMER, self.OnTRefreshTimer, id=wxID_DSIMCONTROLTREFRESH) def _init_ctrls(self, prnt): @@ -58,7 +62,7 @@ def _init_ctrls(self, prnt): hsizer.Add(wx.StaticText(pane, -1, 'Number of detection channels: '), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 2) self.cNumSplitterChans = wx.Choice(pane, -1, choices=['1 - Standard', '2 - Ratiometric/Biplane', '4 - HT / 4Pi-SMS']) - self.cNumSplitterChans.SetSelection(0) + self.cNumSplitterChans.SetSelection({1:0, 2:1, 4:2}[self.sim_controller.n_chans]) self.cNumSplitterChans.Bind(wx.EVT_CHOICE, self.OnNumChannelsChanged) hsizer.Add(self.cNumSplitterChans, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10) sbsizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) @@ -261,7 +265,7 @@ def _init_ctrls(self, prnt): hsizer = wx.BoxSizer(wx.HORIZONTAL) - self.bGenFlours = wx.Button(pFirstPrinciples, -1, 'Go') + self.bGenFlours = wx.Button(pFirstPrinciples, -1, 'Set') self.bGenFlours.Bind(wx.EVT_BUTTON, self.OnBGenFloursButton) hsizer.Add(self.bGenFlours, 1, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 2) @@ -290,7 +294,7 @@ def _init_ctrls(self, prnt): hsizer = wx.BoxSizer(wx.HORIZONTAL) - self.bGenEmpiricalHistFluors = wx.Button(pEmpiricalModel, -1, 'Go') + self.bGenEmpiricalHistFluors = wx.Button(pEmpiricalModel, -1, 'Set') self.bGenEmpiricalHistFluors.Bind(wx.EVT_BUTTON, self.OnBGenEmpiricalHistFluorsButton) hsizer.Add(self.bGenEmpiricalHistFluors, 1, wx.ALIGN_CENTER_VERTICAL, 2) @@ -315,26 +319,27 @@ def _init_ctrls(self, prnt): item.AddNewElement(pane) self.AddPane(item) - ######## Status ######### - - #sbsizer=wx.StaticBoxSizer(wx.StaticBox(self, -1, 'Status'), - # wx.VERTICAL) - - item = afp.foldingPane(self, -1, caption='Status', pinned=True) - pane = wx.Panel(item, -1) - sbsizer = wx.BoxSizer(wx.VERTICAL) - - self.stStatus = wx.StaticText(pane, -1, - label='hello\nworld\n\n\nfoo') - sbsizer.Add(self.stStatus, 0, wx.ALL | wx.EXPAND, 2) - - self.bPause = wx.Button(pane, -1, 'Pause') - self.bPause.Bind(wx.EVT_BUTTON, self.OnBPauseButton) - sbsizer.Add(self.bPause, 0, wx.ALL | wx.ALIGN_RIGHT, 2) - - pane.SetSizerAndFit(sbsizer) - item.AddNewElement(pane) - self.AddPane(item) + if self._show_status: + ######## Status ######### + + #sbsizer=wx.StaticBoxSizer(wx.StaticBox(self, -1, 'Status'), + # wx.VERTICAL) + + item = afp.foldingPane(self, -1, caption='Status', pinned=True) + pane = wx.Panel(item, -1) + sbsizer = wx.BoxSizer(wx.VERTICAL) + + self.stStatus = wx.StaticText(pane, -1, + label='hello\nworld\n\n\nfoo') + sbsizer.Add(self.stStatus, 0, wx.ALL | wx.EXPAND, 2) + + self.bPause = wx.Button(pane, -1, 'Pause') + self.bPause.Bind(wx.EVT_BUTTON, self.OnBPauseButton) + sbsizer.Add(self.bPause, 0, wx.ALL | wx.ALIGN_RIGHT, 2) + + pane.SetSizerAndFit(sbsizer) + item.AddNewElement(pane) + self.AddPane(item) #vsizer.Add(sbsizer, 0, wx.ALL|wx.EXPAND, 2) @@ -418,15 +423,18 @@ def getSplitterInfo(self): self.sim_controller.n_chans = nChans - def __init__(self, parent, sim_controller): + def __init__(self, parent, sim_controller, show_status=True): afp.foldPanel.__init__(self, parent, -1) - self.sim_controller = sim_controller # type: .simcontrol.SimController + self.sim_controller = sim_controller # type: PYME.Acquire.Hardware.Simulator.simcontrol.SimController + self._show_status=show_status + self._init_ctrls(parent) self.fillGrids(self.sim_controller.transition_tensor) - self.tRefresh.Start(200) + if self._show_status: + self.tRefresh.Start(200) def OnBGenWormlikeButton(self, event): @@ -448,7 +456,7 @@ def OnBGenWormlikeButton(self, event): def OnBLoadPointsButton(self, event): fn = wx.FileSelector('Read point positions from file') if fn is None: - print('No file selected') + logger.warning('No file selected') else: self.sim_controller.load_fluors(fn) @@ -456,7 +464,7 @@ def OnBLoadPointsButton(self, event): def OnBSavePointsButton(self, event): fn = wx.SaveFileSelector('Save point positions to file', '.txt') if fn is None: - print('No file selected') + logger.warning('No file selected') else: self.sim_controller.save_points(fn) @@ -470,7 +478,7 @@ def OnBSetPSFModel(self, event=None): def OnBSetPSF(self, event): fn = wx.FileSelector('Read PSF from file', default_extension='psf', wildcard='PYME PSF Files (*.psf)|*.psf|TIFF (*.tif)|*.tif') - print(fn) + logger.debug('Setting PSF from file: %s' %fn) if fn == '': return else: @@ -487,7 +495,7 @@ def OnBViewPSF(self, event): ViewIm3D(self.sim_controller.get_psf(), mode='psf') def OnBGenFloursButton(self, event): - if (len(self.sim_controller.points) == 0): + if (len(self.sim_controller.points) == 0) and (not self.sim_controller.point_gen): wx.MessageBox('No fluorophore positions - either generate of load a set of positions', 'Error', wx.OK | wx.ICON_HAND) return @@ -496,7 +504,7 @@ def OnBGenFloursButton(self, event): self.sim_controller.excitation_crossections = [float(self.tExSwitch.GetValue()), float(self.tExProbe.GetValue())] self.getSplitterInfo() - self.sim_controller.generate_fluorophores_theoretical() + self.sim_controller.generate_fluorophores() def _generate_and_set_fluorophores(self): self.getSplitterInfo() @@ -504,9 +512,9 @@ def _generate_and_set_fluorophores(self): self.sim_controller.transition_tensor = self.getTensorFromGrids() self.sim_controller.excitation_crossections = [float(self.tExSwitch.GetValue()), float(self.tExProbe.GetValue())] - self.sim_controller.generate_fluorophores_theoretical() + self.sim_controller.generate_fluorophores() else: - self.sim_controller.generate_fluorophores_empirical() + self.sim_controller.generate_fluorophores(mode='empirical') def OnBPauseButton(self, event): @@ -526,12 +534,13 @@ def OnTRefreshTimer(self, event): self.stStatus.SetLabel('No fluorophores defined') return - for i in range(len(cts)): - cts[i] = (self.sim_controller.scope.cam.fluors.fl['state'] == i).sum() + #for i in range(len(cts)): + # cts[i] = (self.sim_controller.scope.cam.fluors.fl['state'] == i).sum() labStr = 'Total # of fluorophores = %d\n' % len(self.sim_controller.scope.cam.fluors.fl) - for i in range(len(cts)): - labStr += "Num '%s' = %d\n" % (self.sim_controller.states[i], cts[i]) + #for i in range(len(cts)): + # labStr += "Num '%s' = %d\n" % (self.sim_controller.states[i], cts[i]) + labStr += self.sim_controller.simulation_status() self.stStatus.SetLabel(labStr) #event.Skip() @@ -580,13 +589,13 @@ def OnModelPresets(self, event=None): def OnBLoadEmpiricalHistButton(self, event): fn = wx.FileSelector('Read point positions from file') if fn is None: - print('No file selected') + logger.warning('No file selected') else: self.sim_controller.load_empirical_histogram(fn) self.stEmpiricalHist.SetLabel('File: %s' % fn) def OnBGenEmpiricalHistFluorsButton(self, event): - self.sim_controller.generate_fluorophores_empirical() + self.sim_controller.generate_fluorophores(mode='empirical') class PAINTPresetDialog(wx.Dialog): @@ -596,16 +605,16 @@ def __init__(self, *args, **kwargs): sizer = wx.BoxSizer(wx.VERTICAL) hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'Unbinding rate [per s]:'), 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(wx.StaticText(self, -1, 'Unbinding rate [per s]:'), 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tOnDark = wx.TextCtrl(self, -1, '1.0') - hsizer.Add(self.tOnDark, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tOnDark, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'Binding rate [per s]:'), 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, + hsizer.Add(wx.StaticText(self, -1, 'Binding rate [per s]:'), 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tDarkOn = wx.TextCtrl(self, -1, '0.001') - hsizer.Add(self.tDarkOn, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tDarkOn, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) # hsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -646,30 +655,30 @@ def __init__(self, *args, **kwargs): sizer = wx.BoxSizer(wx.VERTICAL) hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'On-Dark rate [per mWs]:'), 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(wx.StaticText(self, -1, 'On-Dark rate [per mWs]:'), 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tOnDark = wx.TextCtrl(self, -1, '0.1') - hsizer.Add(self.tOnDark, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tOnDark, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'Spontaneous Dark-On rate [per s]:'), 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, + hsizer.Add(wx.StaticText(self, -1, 'Spontaneous Dark-On rate [per s]:'), 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) - self.tDarkOn = wx.TextCtrl(self, -1, '0.001') - hsizer.Add(self.tDarkOn, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + self.tDarkOn = wx.TextCtrl(self, -1, '0.02') + hsizer.Add(self.tDarkOn, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'UV induced Dark-On rate [per mWs]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tDarkOnUV = wx.TextCtrl(self, -1, '0.001') - hsizer.Add(self.tDarkOnUV, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tDarkOnUV, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'Bleaching rate [per mWs]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) - self.tOnBleach = wx.TextCtrl(self, -1, '0.03') - hsizer.Add(self.tOnBleach, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) + self.tOnBleach = wx.TextCtrl(self, -1, '0.01') + hsizer.Add(self.tOnBleach, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) btnsizer = self.CreateButtonSizer(wx.OK)#wx.StdDialogButtonSizer() @@ -696,46 +705,46 @@ def __init__(self, *args, **kwargs): sizer = wx.BoxSizer(wx.VERTICAL) hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'Photoactivation rate [per mWs]:'), 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, + hsizer.Add(wx.StaticText(self, -1, 'Photoactivation rate [per mWs]:'), 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tPhotoactivation = wx.TextCtrl(self, -1, '0.001') - hsizer.Add(self.tPhotoactivation, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tPhotoactivation, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'Photoactivation rate (readout laser) [per mWs]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tPhotoactivationReadout = wx.TextCtrl(self, -1, '0') - hsizer.Add(self.tPhotoactivationReadout, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tPhotoactivationReadout, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'Bleaching rate [per mWs]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tOnBleach = wx.TextCtrl(self, -1, '0.03') - hsizer.Add(self.tOnBleach, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tOnBleach, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'On-Dark rate (blinking) [per mWs]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tOnDark = wx.TextCtrl(self, -1, '0') - hsizer.Add(self.tOnDark, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tOnDark, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'Spontaneous Dark-On rate (blinking) [per s]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tDarkOn = wx.TextCtrl(self, -1, '0') - hsizer.Add(self.tDarkOn, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tDarkOn, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'UV induced Dark-On rate (blinking) [per mWs]:'), 0, - wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) self.tDarkOnUV = wx.TextCtrl(self, -1, '0') - hsizer.Add(self.tDarkOnUV, 0, wx.ALL | wx.ALIGN_CENTRE_HORIZONTAL, 2) + hsizer.Add(self.tDarkOnUV, 0, wx.ALL | wx.ALIGN_CENTRE_VERTICAL, 2) sizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 2) btnsizer = self.CreateButtonSizer(wx.OK)#wx.StdDialogButtonSizer() @@ -754,3 +763,32 @@ def get_trans_tensor(self): pDarkOn=[float(self.tDarkOn.GetValue()), float(self.tDarkOnUV.GetValue()), 0], pOnBleach=[0, 0, float(self.tOnBleach.GetValue())]) + +class MiniSimPanel(wx.Panel): + def __init__(self, parent, sim_controler, **kw): + super().__init__(parent, **kw) + + self._sim_control = sim_controler + + vsizer = wx.BoxSizer(wx.VERTICAL) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.bPause = wx.Button(self, -1, 'Pause') + self.bPause.Bind(wx.EVT_BUTTON, self.OnBPauseButton) + hsizer.AddStretchSpacer() + hsizer.Add(self.bPause, 0, wx.ALL, 2) + + vsizer.Add(hsizer) + + self.SetSizerAndFit(vsizer) + + def OnBPauseButton(self, event): + if self._sim_control.scope.frameWrangler.isRunning(): + self._sim_control.scope.frameWrangler.stop() + self.bPause.SetLabel('Resume') + else: + self._sim_control.scope.frameWrangler.start() + self.bPause.SetLabel('Pause') + + + \ No newline at end of file diff --git a/PYME/Acquire/Hardware/Simulator/wormlike2.py b/PYME/Acquire/Hardware/Simulator/wormlike2.py deleted file mode 100755 index 700073a39..000000000 --- a/PYME/Acquire/Hardware/Simulator/wormlike2.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/python - -############### -# wormlike2.py -# -# Copyright David Baddeley, 2012 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################ - - -from scipy import * - -import numpy as np - -def bareDNA(kbp, steplength=10): - return wormlikeChain(kbp, steplength, lengthPerKbp=.34e3, persistLength=75.0) - -def fibre30nm(kbp, steplength=10): - return wormlikeChain(kbp, steplength, lengthPerKbp=10.0, persistLength=150.0) - -def fibre10nm(kbp, steplength=10): - return wormlikeChain(kbp, steplength, lengthPerKbp=57.0, persistLength=75.0) - -def wiglyFibre(length, persistLength, steplength=10): - return wormlikeChain(length, steplength, lengthPerKbp=1., persistLength=persistLength) - -class wormlikeChain: - def __init__(self, kbp, steplength=10.0, lengthPerKbp=10.0, persistLength=150.0): - numsteps = int(round(lengthPerKbp*kbp/steplength)) - - exp_costheta = (exp(-steplength/persistLength)); - theta = sqrt(2*log(1/exp_costheta))*abs(randn(numsteps)); - phi = 2*pi*rand(numsteps); - #phi = 0.5*pi*randn(numsteps, 1)+pi/20; - - phi = cumsum(concatenate(([0], phi),0)) - - xs = 1 - 2*rand() - ys = 1 - 2*rand() - zs = 1 - 2*rand() - - nrm = sqrt(xs**2 + ys**2 + zs**2) - - xs = xs/nrm; - ys = ys/nrm; - zs = zs/nrm; - - so = array([xs, ys, zs]) - - xs = steplength*xs*ones(theta.shape) - ys = steplength*ys*ones(theta.shape) - zs = steplength*zs*ones(theta.shape) - - for i in range(2,numsteps): - sh = cross(so, so + array([0,0,2])) - #sh = sh./sqrt(dot(sh, sh)); - #sh = sh./sqrt(sh*sh.'); - sh = sh/dot(sh, sh.T) - sk = cross(so, sh) - #sk = sk./sqrt(dot(sk, sk)); - #sk = sk./sqrt(sk*sk'); - sk = sk/dot(sk, sk.T) - - sn = cos(theta[i])*so + sin(theta[i])*sin(phi[i])*sh + sin(theta[i])*cos(phi[i])*sk - snn = np.sqrt((sn * sn).sum()) - sn /= snn - - sh_n = cos(theta[i])*(cos(phi[i])*sh + sin(phi[i])*sk) + sin(theta[i])*sn - sk_n = cos(theta[i])*(cos(phi[i])*sh + sin(phi[i])*sk) + sin(theta[i])*sn - - xs[i] = steplength*sn[0] - ys[i] = steplength*sn[1] - zs[i] = steplength*sn[2] - - so = sn; - #i/numsteps - - - self.xp = cumsum(concatenate(([0], xs),0)) - self.yp = cumsum(concatenate(([0], ys),0)) - self.zp = cumsum(concatenate(([0], zs),0)) - - #plot3(xp, yp, zp) - #grid - #daspect([1 1 1]) - - #if (length(xp) > 3) - #[K, V] = convhulln([xp, yp, zp]); - - #V = V/1e9; - - - -#xr = xp; -#yr = yp; -#zr = zp; - -#end_d = sqrt((xp(1) - xp(length(xp))).^2 + (yp(1) - yp(length(xp))).^2 + (zp(1) - zp(length(xp))).^2) \ No newline at end of file diff --git a/PYME/Acquire/Hardware/camera_noise.py b/PYME/Acquire/Hardware/camera_noise.py new file mode 100644 index 000000000..59b5ade0d --- /dev/null +++ b/PYME/Acquire/Hardware/camera_noise.py @@ -0,0 +1,270 @@ +""" +Handle camera noise properties for all cameras +Support legacy, hard-coded properties as well as a more flexible .yaml based configuration within the PYME.config framework + +This module provides a dictionary, `noise_properties` which is indexed by the camera serial number, under the assumption that serial numbers collisions +between manufacturers will be unlikely. + +Camera noise properties may be specified in .yaml files within the `cameras` subdirectory of any of the PYME config directories (see `PYME.config` for details),. +The yaml files should encode a dictionary in the following form: + +.. code-block:: yaml + + # An Andor Zyla entry + VSC-00954: + noise_properties: + 12-bit (high well capacity): + ADOffset: 100 + ElectronsPerCount: 6.97 + ReadNoise: 5.96 + SaturationThreshold: 2047 + 12-bit (low noise): + ADOffset: 100 + ElectronsPerCount: 0.28 + ReadNoise: 1.1 + SaturationThreshold: 2047 + 16-bit (low noise & high well capacity): + ADOffset: 100 + ElectronsPerCount: 0.5 + ReadNoise: 1.33 + SaturationThreshold: 65535 + + # An Andor IXon entry: + 5414: + default_preamp_gain: 0 + noise_properties: + Preamp Gain 0: + ADOffset: 413 + DefaultEMGain: 90 + ElectronsPerCount: 25.24 + NGainStages: 536 + ReadNoise: 61.33 + SaturationThreshold: 16383 + + # A HamamatsuORCA entry: + '100233': + noise_properties: + fixed: + ADOffset: 100 + DefaultEMGain: 1 + ElectronsPerCount: 0.47 + NGainStages: 0 + ReadNoise: 1.65 + SaturationThreshold: 65535 + + # A UEye entry: + '4103211322': + noise_properties: + 12-bit: + ElectronsPerCount: 2.706 + ReadNoise: 2.425 + ADOffset: 7.67 + SaturationThreshold: 4095 + +This dictionary is indexed by camera serial number. Each camera entry is itself a dictionary, and must have a dictionary called `noise_properties` as one entry. +The `noise_properties` dictionary contains a set of dictionaries of readout characteristics for each gain mode of the camera (the keys here will typically vary between +camera types). It is permissible to put additional entries in the camera dictionaries e.g. the `default_preamp_gain` entry for the IXon above, but these should be treated +as informational and code should ideally not depend on their prescence (we make a slight exception here for the IXon code as the noise properties for non-default modes have +not been recorded, but the behaviour is discouraged). + +All .yaml files in the cameras subdirectory are read and their contents amalgamated. + +A number of hard-coded camera noise values are also provided in this file (_legacy_noise_properties) + +NOTE: we handle this here, rather than in PYME.config to keep legacy info out of PYME.config and avoid introducing a back-dependancy +from PYME.config on PYMEAcquire +""" +from PYME import config +import yaml +import os +import glob + +noise_properties = {} + +# we have historically recorded noise properties in the code of the relevant camera class. All these legacy settings are now moved here. +_legacy_noise_properties = { + # Andor IXon cameras + 1823 : { + 'default_preamp_gain' : 0, + 'noise_properties': { + 'Preamp Gain 0': { + 'ReadNoise' : 109.8, + 'ElectronsPerCount' : 27.32, + 'NGainStages' : 536, + 'ADOffset' : 971, + 'DefaultEMGain' : 150, + 'SaturationThreshold' : (2**14 -1) + }}}, + 5414 : { + 'default_preamp_gain' : 0, + 'noise_properties': { + 'Preamp Gain 0': { + 'ReadNoise' : 61.33, + 'ElectronsPerCount' : 25.24, + 'NGainStages' : 536, + 'ADOffset' : 413, + 'DefaultEMGain' : 90, + 'SaturationThreshold' : (2**14 -1) + }}}, + 7863 : { #Gain setting of 3 + 'default_preamp_gain' : 2, + 'noise_properties': { + 'Preamp Gain 2': { + 'ReadNoise' : 88.1, + 'ElectronsPerCount' : 4.99, + 'NGainStages' : 536, + 'ADOffset' : 203, + 'DefaultEMGain' : 90, + 'SaturationThreshold' : 5.4e4#(2**16 -1) + }}}, + 7546 : { + 'default_preamp_gain' : 2, + 'noise_properties': { + 'Preamp Gain 2': { + # preamp: currently using most sensitive setting (default according to docs) + # if I understand the code correctly the fastest Horizontal Shift Speed will be selected + # which should be 17 MHz for this camera; therefore using 17 MHz data + 'ReadNoise' : 85.23, + 'ElectronsPerCount' : 4.82, + 'NGainStages' : 536, # relevant? + 'ADOffset' : 150, # from test measurement at EMGain 85 (realgain ~30) + 'DefaultEMGain' : 85, # we start carefully and can bumb this later to be in the vicinity of 30 + 'SaturationThreshold' : (2**16 -1) # this cam has 16 bit data + }}}, + + # Andor Zyla cameras + 'VSC-00954': { + 'model' : 'Zyla', # model param currently not used + 'noise_properties': { + '12-bit (low noise)': { + 'ReadNoise' : 1.1, + 'ElectronsPerCount' : 0.28, + 'ADOffset' : 100, # check mean (or median) offset + 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit + }, + '12-bit (high well capacity)': { + 'ReadNoise' : 5.96, + 'ElectronsPerCount' : 6.97, + 'ADOffset' : 100, + 'SaturationThreshold' : 2**11-1#(2**16 -1) + }, + '16-bit (low noise & high well capacity)': { + 'ReadNoise' : 1.33, + 'ElectronsPerCount' : 0.5, + 'ADOffset' : 100, + 'SaturationThreshold' : (2**16 -1) + }}}, + 'CSC-00425': { # this is info for a Sona + 'noise_properties': { + u'12-bit (low noise)': { + 'ReadNoise' : 1.21, + 'ElectronsPerCount' : 0.45, + 'ADOffset' : 100, # check mean (or median) offset + 'SaturationThreshold' : 1776 #(2**16 -1) # check this is really 11 bit + }, + u'16-bit (high dynamic range)': { + 'ReadNoise' : 1.84, + 'ElectronsPerCount' : 1.08, + 'ADOffset' : 100, + 'SaturationThreshold' : 44185 + }}}, + 'VSC-02858': { + 'noise_properties': { + '12-bit (low noise)': { + 'ReadNoise' : 1.19, + 'ElectronsPerCount' : 0.3, + 'ADOffset' : 100, # check mean (or median) offset + 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit + }, + '12-bit (high well capacity)': { + 'ReadNoise' : 6.18, + 'ElectronsPerCount' : 7.2, + 'ADOffset' : 100, + 'SaturationThreshold' : 2**11-1#(2**16 -1) + }, + '16-bit (low noise & high well capacity)': { + 'ReadNoise' : 1.42, + 'ElectronsPerCount' : 0.5, + 'ADOffset' : 100, + 'SaturationThreshold' : (2**16 -1) + }}}, + 'VSC-02698': { + 'noise_properties': { + '12-bit (low noise)': { + 'ReadNoise' : 1.16, + 'ElectronsPerCount' : 0.26, + 'ADOffset' : 100, # check mean (or median) offset + 'SaturationThreshold' : 2**11-1#(2**16 -1) # check this is really 11 bit + }, + '12-bit (high well capacity)': { + 'ReadNoise' : 6.64, + 'ElectronsPerCount' : 7.38, + 'ADOffset' : 100, + 'SaturationThreshold' : 2**11-1#(2**16 -1) + }, + '16-bit (low noise & high well capacity)': { + 'ReadNoise' : 1.36, + 'ElectronsPerCount' : 0.49, + 'ADOffset' : 100, + 'SaturationThreshold' : (2**16 -1) + }}}, + + # Hamamatsu ORCA flash + '100233' : { + 'noise_properties': { + 'fixed' : { + 'ReadNoise': 1.65, #CHECKME - converted from an ADU value of 3.51 + 'ElectronsPerCount': 0.47, + 'NGainStages': 0, + 'ADOffset': 100, + 'DefaultEMGain': 1, + 'SaturationThreshold': (2**16 - 1) + }}}, + '301777' : { + 'noise_properties': { + 'fixed' : { + 'ReadNoise': 1.63, + 'ElectronsPerCount': 0.47, + 'NGainStages': 0, + 'ADOffset': 100, + 'DefaultEMGain': 1, + 'SaturationThreshold': (2**16 - 1) + }}}, + '720795' : { + 'noise_properties': { + 'fixed' : { + 'ReadNoise': 0.997, # rn is sqrt(var) in units of electrons. Median of varmap is 0.9947778 [e-^2] #CHECKME - converted from 2.394 ADU + 'ElectronsPerCount': 0.416613, + 'NGainStages': 0, + 'ADOffset': 101.753685, + 'DefaultEMGain': 1, + 'SaturationThreshold': (2**16 - 1) + }}}, +} + +# add the legacy camera info to noise_properties +noise_properties.update(_legacy_noise_properties) + +# parse info from .yamls in config/cameras/ +for config_dir in config.config_dirs: + cam_yamls = glob.glob(os.path.join(config_dir, 'cameras','*.yaml')) + for yamlfile in cam_yamls: + with open(yamlfile,'r') as fi: + noise_properties.update(yaml.safe_load(fi)) + +def add_camera_noise_info(serial_num, noise_info): + """ + Programatically (e.g. in PYMEAcquire init script) add to our database of noise info. + + NOTE: this will not persist across sessions. + """ + + # do some checks so we fail promptly + np = noise_info['noise_properties'] + + for k, v in np.items(): + #check for required keys + rn = v['ReadNoise'] # will raise KeyError if not present + epc = v['ElectronsPerCount'] # ditto + + noise_properties[serial_num] = noise_info diff --git a/PYME/Acquire/Hardware/ccdCalibrator.py b/PYME/Acquire/Hardware/ccdCalibrator.py index d9b832332..b51964cae 100644 --- a/PYME/Acquire/Hardware/ccdCalibrator.py +++ b/PYME/Acquire/Hardware/ccdCalibrator.py @@ -28,6 +28,9 @@ global scope scope = None +import logging +logger = logging.getLogger(__name__) + def setScope(sc): global scope scope = sc @@ -94,11 +97,11 @@ def __init__(self, gains = np.arange(0, 220, 5)): def finish(self): import wx - print('Disconnecting') + logger.debug('Disconnecting') self.pa.onFrame.disconnect(self.tick) self.cam.SetEMGain(self.emgain) time.sleep(0.5) - print('Disconnected') + logger.debug('Disconnected') wx.CallAfter(self.plot) def plot(self): @@ -140,7 +143,7 @@ def tick(self, sender, frameData, **kwargs): self.pos += 1 wx.CallAfter(self.pd.Update,self.pos) if self.pos < len(self.gains): - print('Setting EM Gain to %d' % self.gains[self.pos]) + logger.debug('Setting EM Gain to %d' % self.gains[self.pos]) self.cam.SetEMGain(self.gains[self.pos]) else: self.finish() diff --git a/PYME/Acquire/Hardware/cobaltLaser.py b/PYME/Acquire/Hardware/cobaltLaser.py index 17a53955d..958f5c4db 100644 --- a/PYME/Acquire/Hardware/cobaltLaser.py +++ b/PYME/Acquire/Hardware/cobaltLaser.py @@ -44,21 +44,21 @@ def IsOn(self): def _TurnOn(self): with serial.Serial(**self.ser_args) as ser: - ser.write('@cobas 0\r\n') - ser.write('l1\r\n') + ser.write(b'@cobas 0\r\n') + ser.write(b'l1\r\n') ser.flush() self.isOn = True def TurnOn(self): with serial.Serial(**self.ser_args) as ser: - ser.write('p %3.2f\r\n' % (self.power*self.maxpower)) + ser.write(b'p %3.2f\r\n' % (self.power*self.maxpower)) ser.flush() self.isOn = True def TurnOff(self): with serial.Serial(**self.ser_args) as ser: - ser.write('l0\r\n') - ser.write('p 0\r\n') + ser.write(b'l0\r\n') + ser.write(b'p 0\r\n') ser.flush() self.isOn = False @@ -72,7 +72,7 @@ def SetPower(self, power): def _getOutputPower(self): with serial.Serial(**self.ser_args) as ser: - ser.write('p?\r') + ser.write(b'p?\r') ser.flush() res = float(ser.readline()) @@ -84,9 +84,9 @@ def GetPower(self): def checkfault(self): with serial.Serial(**self.ser_args) as ser: - ser.write('f?\r\n') + ser.write(b'f?\r\n') ser.flush() - print('fault code: 0-no errors; 1-temperature error; 3-interlock error; 4-constant power time out') + #print('fault code: 0-no errors; 1-temperature error; 3-interlock error; 4-constant power time out') ret = ser.read(50) return @@ -99,10 +99,10 @@ def __init__(self, name,turnOn=False, portname='COM1', minpower=0.001, maxpower= def TurnOn(self): with serial.Serial(**self.ser_args) as ser: - ser.write('@cobas 0\r\n') - ser.write('cf\r\n') - ser.write('l1\r\n') # the Cobolt 405nm laser needs to be restarted if it is powered on before the interlock is closed. - ser.write('p %3.2f\r\n' % (self.power*self.maxpower)) + ser.write(b'@cobas 0\r\n') + ser.write(b'cf\r\n') + ser.write(b'l1\r\n') # the Cobolt 405nm laser needs to be restarted if it is powered on before the interlock is closed. + ser.write(b'p %3.2f\r\n' % (self.power*self.maxpower)) ser.flush() self.isOn = True diff --git a/PYME/Acquire/Hardware/cobaltLaser561.py b/PYME/Acquire/Hardware/cobaltLaser561.py index 6a68de627..cf32bebc7 100644 --- a/PYME/Acquire/Hardware/cobaltLaser561.py +++ b/PYME/Acquire/Hardware/cobaltLaser561.py @@ -88,7 +88,7 @@ def checkfault(self): self.ser_port.flush() return self.ser_port.read(50) - print('fault code: 0-no errors; 1-temperature error; 3-interlock error; 4-constant power time out') + #print('fault code: 0-no errors; 1-temperature error; 3-interlock error; 4-constant power time out') def GetPower(self): return self.power \ No newline at end of file diff --git a/PYME/Acquire/Hardware/driftTrackGUI.py b/PYME/Acquire/Hardware/driftTrackGUI.py index f7a8c20fa..115730c87 100644 --- a/PYME/Acquire/Hardware/driftTrackGUI.py +++ b/PYME/Acquire/Hardware/driftTrackGUI.py @@ -205,16 +205,45 @@ def draw(self): -# add controls for lastAdjustment +from PYME.DSView import overlays +import weakref +class DriftROIOverlay(overlays.Overlay): + def __init__(self, driftTracker): + self.dt = driftTracker + + def __call__(self, view, dc): + if self.dt.sub_roi is not None: + dc.SetPen(wx.Pen(colour=wx.CYAN, width=1)) + dc.SetBrush(wx.TRANSPARENT_BRUSH) + x0, x1, y0, y1 = self.dt.sub_roi + x0c, y0c = view.pixel_to_screen_coordinates(x0, y0) + x1c, y1c = view.pixel_to_screen_coordinates(x1, y1) + sX, sY = x1c-x0c, y1c-y0c + dc.DrawRectangle(int(x0c), int(y0c), int(sX), int(sY)) + dc.SetPen(wx.NullPen) + else: + dc.SetBackground(wx.TRANSPARENT_BRUSH) + dc.Clear() + class DriftTrackingControl(wx.Panel): - def __init__(self, parent, driftTracker, winid=-1, showPlots=True): + def __init__(self, main_frame, driftTracker, winid=-1, showPlots=True): + ''' This class provides a GUI for controlling the drift tracking system. + + It should be initialised with a reference to the PYMEAcquire main frame, which will stand in as a parent while other GUI items are + created. Note that the actual parent will be reassigned once the GUI tool panel is created using a Reparent() call. + ''' # begin wxGlade: MyFrame1.__init__ #kwds["style"] = wx.DEFAULT_FRAME_STYLE - wx.Panel.__init__(self, parent, winid) + wx.Panel.__init__(self, main_frame, winid) self.dt = driftTracker self.plotInterval = 10 self.showPlots = showPlots + # keep a reference to the main frame. Do this as a weakref to avoid circular references. + # we need this to be able to access the view to get the current selection and to add overlays. + self._main_frame = weakref.proxy(main_frame) + self._view_overlay = None # dummy reference to the overlay so we only create it once + sizer_1 = wx.BoxSizer(wx.VERTICAL) hsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -240,6 +269,15 @@ def __init__(self, parent, driftTracker, winid=-1, showPlots=True): #hsizer.Add(self.bSaveCalib, 0, wx.ALL, 2) #self.bSaveCalib.Bind(wx.EVT_BUTTON, self.OnBSaveCalib) sizer_1.Add(hsizer, 0, wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.tbSubROI = wx.ToggleButton(self, -1, 'Restrict to sub-ROI') + hsizer.Add(self.tbSubROI, 0, wx.ALL, 2) + self.tbSubROI.Bind(wx.EVT_TOGGLEBUTTON, self.OnTBToggleSubROI) + #self.bSaveCalib = wx.Button(self, -1, 'Save Cal') + #hsizer.Add(self.bSaveCalib, 0, wx.ALL, 2) + #self.bSaveCalib.Bind(wx.EVT_BUTTON, self.OnBSaveCalib) + sizer_1.Add(hsizer, 0, wx.EXPAND, 0) hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, "Calibration:"), 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) @@ -256,6 +294,24 @@ def __init__(self, parent, driftTracker, winid=-1, showPlots=True): self.bSetTolerance.Bind(wx.EVT_BUTTON, self.OnBSetTolerance) sizer_1.Add(hsizer,0, wx.EXPAND, 0) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, "Z increment [nm]:"), 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.tdeltaZ = wx.TextCtrl(self, -1, '%3.0f'% (1e3*self.dt.get_delta_Z()), size=[30,-1]) + hsizer.Add(self.tdeltaZ, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.bSetdeltaZ = wx.Button(self, -1, 'Set', style=wx.BU_EXACTFIT) + hsizer.Add(self.bSetdeltaZ, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.bSetdeltaZ.Bind(wx.EVT_BUTTON, self.OnBSetdeltaZ) + sizer_1.Add(hsizer,0, wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, "Stack halfsize:"), 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.tHalfsize = wx.TextCtrl(self, -1, '%3.0f'% (self.dt.get_stack_halfsize()), size=[30,-1]) + hsizer.Add(self.tHalfsize, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.bSetHalfsize = wx.Button(self, -1, 'Set', style=wx.BU_EXACTFIT) + hsizer.Add(self.bSetHalfsize, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.bSetHalfsize.Bind(wx.EVT_BUTTON, self.OnBSetHalfsize) + sizer_1.Add(hsizer,0, wx.EXPAND, 0) + # hsizer = wx.BoxSizer(wx.HORIZONTAL) # hsizer.Add(wx.StaticText(self, -1, "Z-factor:"), 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) # self.tZfactor = wx.TextCtrl(self, -1, '%3.1f'% self.dt.Zfactor, size=[30,-1]) @@ -317,6 +373,20 @@ def OnCBTrack(self, event): def OnBSetPostion(self, event): self.dt.reCalibrate() + + def OnTBToggleSubROI(self, event): + self.toggle_subroi(self.tbSubROI.GetValue()) + + def toggle_subroi(self, new_state=True): + ''' Turn sub-ROI tracking on or off, using the current selection in the live image display''' + if new_state: + x0, x1, y0, y1, _, _ = self._main_frame.view.do.sorted_selection + self.dt.set_subroi((x0, x1, y0, y1)) + else: + self.dt.set_subroi(None) + + if self._view_overlay is None: + self._view_overlay = self._main_frame.view.add_overlay(DriftROIOverlay(self.dt), 'Drift tracking Sub-ROI') def OnBSaveCalib(self, event): if not hasattr(self.dt, 'calibState') or (self.dt.calibState < self.dt.NCalibStates): @@ -364,6 +434,12 @@ def OnBSaveHist(self, event): def OnBSetTolerance(self, event): self.dt.set_focus_tolerance(float(self.tTolerance.GetValue())/1e3) + def OnBSetdeltaZ(self, event): + self.dt.set_delta_Z(float(self.tdeltaZ.GetValue())/1e3) + + def OnBSetHalfsize(self, event): + self.dt.set_stack_halfsize(int(self.tHalfsize.GetValue())) + def OnBSetZfactor(self, event): self.dt.Zfactor = float(self.tZfactor.GetValue()) diff --git a/PYME/Acquire/Hardware/driftTracking.py b/PYME/Acquire/Hardware/driftTracking.py index 504599408..dd51a7c37 100644 --- a/PYME/Acquire/Hardware/driftTracking.py +++ b/PYME/Acquire/Hardware/driftTracking.py @@ -19,6 +19,9 @@ import threading from PYME.misc.computerName import GetComputerName +import logging +logger = logging.getLogger(__name__) + def correlateFrames(A, B): A = A.squeeze()/A.mean() - 1 B = B.squeeze()/B.mean() - 1 @@ -62,15 +65,97 @@ def correlateAndCompareFrames(A, B): return (As -B).mean(), dx, dy +from PYME.contrib import dispatch +class StandardFrameSource(object): + '''This is a simple source which emits frames once per polling interval of the frameWrangler + (i.e. corresponding to the onFrameGroup signal of the frameWrangler). + + The intention is to reproduce the historical behaviour of the drift tracking code, whilst + abstracting some of the detailed knowledge of frame handling out of the actual tracking code. + + ''' + def __init__(self, frameWrangler): + self._fw = frameWrangler + self._on_frame = dispatch.Signal(['frameData']) + self._fw.onFrameGroup.connect(self.tick) + + + def tick(self, *args, **kwargs): + self._on_frame.send(sender=self, frameData=self._fw.currentFrame) + + @property + def shape(self): + return self._fw.currentFrame.shape + + def connect(self, callback): + self._on_frame.connect(callback) + + def disconnect(self, callback): + self._on_frame.disconnect(callback) + +class OIDICFrameSource(StandardFrameSource): + """ Emit frames from the camera to the tracking code only for a single OIDIC orientation. + + Currently a straw man / skeleton pending details of OIDIC code. + + TODO - should this reside here, or with the other OIDIC code (which I believe to be in a separate repo)? + """ + + def __init__(self, frameWrangler, oidic_controller, oidic_orientation=0): + #super().__init__(frameWrangler) + self._fw = frameWrangler + self._on_frame = dispatch.Signal(['frameData']) + + # connect to onFrame rather than onFrameGroup so we get the + # current frame which as opposed to an older one. + # this is important as the OIDIC orientation is expected to change between frames. + # NOTE: we still assume that this connection happens before any of the acquisition + # classes connect to onFrame, so that we get the frame data (and microscope state) + # before the acquisition classes have had a chance to modify the state (e.g. by changing the DIC orientation). + # TODO: This should be pretty safe, as the FrameSource is usually created in the init script, + # but can we make this robust agains the assumed ordering (execution of handlers in order of addition is an undocumented feature of dispatch)? + self._fw.onFrame.connect(self.tick) + + self._oidic = oidic_controller + + self._last_tick_time = 0 + # throttle to ensure 50 ms delay between completion of computation on one frame and start of computation on the next. + # the 50ms is emperical, but should (hopefully) be long enough to let anything else which is hooked to onFrame run. + self._tick_throttle = 0.05 + #self._target_orientation = oidic_orientation + + def tick(self, frameData, **kwargs): + # Because we are connected to onFrame rather than onFrameGroup (which is inherently throttled by the GUI loop), + # we need to throttle the signal ourselves to avoid overwhelming things + # when the frameWrangler is running at high speed (i.e. drift computation time + # is slower than or on the order of the camera integration time). + if (time.time() - self._last_tick_time) < self._tick_throttle: + return + + #self._target_orientation = self._oidic.home_channel() + #if self._oidic.current_channel == self._target_orientation: + if self._oidic.current_channel == self._oidic.home_channel(): + # send with the frame data of the frame which triggered the signal, rather than frameWrangler.currentFrame, which lags. + self._on_frame.send(sender=self, frameData=frameData) + self._last_tick_time = time.time() + else: + # clobber all frames coming from camera when not in the correct DIC orientation + pass class Correlator(object): - def __init__(self, scope, piezo=None): - self.scope = scope + def __init__(self, scope, piezo=None, frame_source=None, sub_roi=None, focusTolerance=.05, deltaZ=0.2, stackHalfSize=35): self.piezo = piezo - - self.focusTolerance = .05 #how far focus can drift before we correct - self.deltaZ = 0.2 #z increment used for calibration - self.stackHalfSize = 35 + + if frame_source is None: + self.frame_source = StandardFrameSource(scope.frameWrangler) + else: + self.frame_source = frame_source + + self.sub_roi = sub_roi + + self.focusTolerance = focusTolerance #how far focus can drift before we correct + self.deltaZ = deltaZ #z increment used for calibration + self.stackHalfSize = stackHalfSize self.NCalibStates = 2*self.stackHalfSize + 1 self.calibState = 0 @@ -88,8 +173,8 @@ def __init__(self, scope, piezo=None): self.maxfac = 1.5e3 self.Zfactor = 1.0 - def initialise(self): - d = 1.0*self.scope.frameWrangler.currentFrame.squeeze() + def _initialise(self, frame_data): + d = 1.0*frame_data.squeeze() self.X, self.Y = np.mgrid[0.0:d.shape[0], 0.0:d.shape[1]] # self.X -= d.shape[0]/2 @@ -121,37 +206,34 @@ def initialise(self): self.historyCorrections = [] - # def setRefA(self): - # d = 1.0*self.scope.frameWrangler.currentFrame.squeeze() - # self.refA = d/d.mean() - 1 - # self.FA = ifftn(self.refA) - # self.refA *= self.mask - - # def setRefB(self): - # d = 1.0*self.scope.frameWrangler.currentFrame.squeeze() - # self.refB = d/d.mean() - 1 - # self.refB *= self.mask - - # def setRefC(self): - # d = 1.0*self.scope.frameWrangler.currentFrame.squeeze() - # self.refC = d/d.mean() - 1 - # self.refC *= self.mask - - # self.dz = (self.refC - self.refB).ravel() - # self.dzn = 2./np.dot(self.dz, self.dz) - def setRefN(self, N): - d = 1.0*self.scope.frameWrangler.currentFrame.squeeze() + def _setRefN(self, frame_data, N): + d = 1.0*frame_data.squeeze() ref = d/d.mean() - 1 self.refImages[:,:,N] = ref self.calFTs[:,:,N] = ifftn(ref) self.calImages[:,:,N] = ref*self.mask - #def setRefD(self): - # self.refD = (1.0*self.d).squeeze()/self.d.mean() - 1 - # self.refD *= self.mask - - #self.dz = (self.refC - self.refA).ravel() + def set_subroi(self, bounds): + """ Set the position of the roi to crop + + Parameters + ---------- + + position : tuple + The pixel position (x0, x1, y0, y1) in int + """ + + self.sub_roi = bounds + self.reCalibrate() + + def _crop_frame(self, frame_data): + if self.sub_roi is None: + return frame_data.squeeze() # we may as well do the squeeze here to avoid lots of squeezes elsewhere + else: + x0, x1, y0, y1 = self.sub_roi + return frame_data.squeeze()[x0:x1, y0:y1] + def set_focus_tolerance(self, tolerance): """ Set the tolerance for locking position @@ -168,6 +250,46 @@ def set_focus_tolerance(self, tolerance): def get_focus_tolerance(self): return self.focusTolerance + + def set_delta_Z(self, delta): + """ Set the Z increment for calibration stack + + Parameters + ---------- + + delta : float + The delta in um. This should be the distance over which changes in PSF intensity with depth + can be approximated as being linear, with an upper bound of the Nyquist sampling in Z. + At Nyquist sampling, the linearity assumption is already getting a bit tenuous. Default = 0.2 um, + which is approximately Nyquist sampled at 1.4NA. + """ + + self.deltaZ = delta + + def get_delta_Z(self): + return self.deltaZ + + + def set_stack_halfsize(self, halfsize): + """ Set the calibration stack half size + + This dictates the maximum size of z-stack you can record whilst retaining focus lock. The resulting + calibration range can be calculated as deltaZ*(2*halfsize), and should extend about 1 micron above + and below the size of the the largest z-stack to ensure that lock can be maintained at the edges of + the stack. The default of 35 gives about 12 um of axial range. + + Parameters + ---------- + + halfsize : int + """ + + self.stackHalfSize = halfsize + + def get_stack_halfsize(self): + return self.stackHalfSize + + def set_focus_lock(self, lock=True): """ Set locking on or off @@ -208,8 +330,8 @@ def get_offset(self): def set_offset(self, offset): self.piezo.SetOffset(offset) - def compare(self): - d = 1.0*self.scope.frameWrangler.currentFrame.squeeze() + def compare(self, frame_data): + d = 1.0*frame_data.squeeze() dm = d/d.mean() - 1 #where is the piezo suppposed to be @@ -235,7 +357,7 @@ def compare(self): #what is the offset between our target position and the calibration position posDelta = nomPos - calPos - print('%s' % [nomPos, posInd, calPos, posDelta]) + #print('%s' % [nomPos, posInd, calPos, posDelta]) #find x-y drift C = ifftshift(np.abs(ifftn(fftn(dm)*FA))) @@ -277,11 +399,17 @@ def compare(self): return dx, dy, dz, Cm, dz, nomPos, posInd, calPos, posDelta - def tick(self, **kwargs): + def tick(self, frameData = None, **kwargs): + if frameData is None: + raise ValueError('frameData must be specified') + else: + frameData = self._crop_frame(frameData) + targetZ = self.piezo.GetTargetPos(0) - if not 'mask' in dir(self) or not self.scope.frameWrangler.currentFrame.shape[:2] == self.mask.shape[:2]: - self.initialise() + #if not 'mask' in dir(self) or not self.frame_source.shape[:2] == self.mask.shape[:2]: + if not 'mask' in dir(self) or not frameData.shape[:2] == self.mask.shape[:2]: + self._initialise(frameData) #called on a new frame becoming available if self.calibState == 0: @@ -303,7 +431,7 @@ def tick(self, **kwargs): # print "cal proceed" if (self.calibState % 1) == 0: #full step - record current image and move on to next position - self.setRefN(int(self.calibState - 1)) + self._setRefN(frameData, int(self.calibState - 1)) self.piezo.MoveTo(0, self.calPositions[int(self.calibState)]) @@ -312,7 +440,7 @@ def tick(self, **kwargs): elif (self.calibState == self.NCalibStates): # print "cal finishing" - self.setRefN(int(self.calibState - 1)) + self._setRefN(frameData, int(self.calibState - 1)) #perform final bit of calibration - calcuate gradient between steps #self.dz = (self.refC - self.refB).ravel() @@ -330,7 +458,7 @@ def tick(self, **kwargs): elif (self.calibState > self.NCalibStates) and np.allclose(self._last_target_z, targetZ): # print "fully calibrated" - dx, dy, dz, cCoeff, dzcorr, nomPos, posInd, calPos, posDelta = self.compare() + dx, dy, dz, cCoeff, dzcorr, nomPos, posInd, calPos, posDelta = self.compare(frameData) self.corrRef = max(self.corrRef, cCoeff) @@ -344,7 +472,7 @@ def tick(self, **kwargs): if self.lockActive: if abs(self.piezo.GetOffset()) > 20.0: self.lockFocus = False - print("focus lock released") + logger.info("focus lock released") if abs(dz) > self.focusTolerance and self.lastAdjustment >= self.minDelay: zcorr = self.piezo.GetOffset() - dz if zcorr < - self.maxfac*self.focusTolerance: @@ -373,13 +501,11 @@ def reCalibrate(self): self.lockActive = False def register(self): - #self.scope.frameWrangler.WantFrameGroupNotification.append(self.tick) - self.scope.frameWrangler.onFrameGroup.connect(self.tick) + self.frame_source.connect(self.tick) self.tracking = True def deregister(self): - #self.scope.frameWrangler.WantFrameGroupNotification.remove(self.tick) - self.scope.frameWrangler.onFrameGroup.disconnect(self.tick) + self.frame_source.disconnect(self.tick) self.tracking = False # def setRefs(self, piezo): @@ -455,7 +581,7 @@ def run(self): # daemon.shutdown(True) def cleanup(self): - print('Shutting down drift tracking Server') + logger.info('Shutting down drift tracking Server') self.daemon.shutdown(True) def getClient(compName = GetComputerName()): diff --git a/PYME/Acquire/Hardware/focusKeys.py b/PYME/Acquire/Hardware/focusKeys.py index dd0746cbf..814deae15 100644 --- a/PYME/Acquire/Hardware/focusKeys.py +++ b/PYME/Acquire/Hardware/focusKeys.py @@ -34,10 +34,10 @@ def __init__(self, parent, piezo, keys = ['F1', 'F2', 'F3', 'F4'], scope = None) parent.AddMenuItem('Focus', 'Sensitivity Down\t%s' % keys[2], self.OnSensDown) parent.AddMenuItem('Focus', 'Sensitivity Up\t%s' % keys[3], self.OnSensUp) -# idFocUp = wx.NewId() -# idFocDown = wx.NewId() -# idSensUp = wx.NewId() -# idSensDown = wx.NewId() +# idFocUp = wx.NewIdRef() +# idFocDown = wx.NewIdRef() +# idSensUp = wx.NewIdRef() +# idSensDown = wx.NewIdRef() # self.menu = wx.Menu(title = '') # @@ -124,10 +124,10 @@ def __init__(self, parent, xpiezo, ypiezo, keys = ['F9', 'F10', 'F11', 'F12'], s parent.AddMenuItem('Position', 'Sensitivity Down\tCtrl-N', self.OnSensDown) parent.AddMenuItem('Position', 'Sensitivity Up\tCtrl-M', self.OnSensUp) - # idFocUp = wx.NewId() - # idFocDown = wx.NewId() - # idSensUp = wx.NewId() - # idSensDown = wx.NewId() + # idFocUp = wx.NewIdRef() + # idFocDown = wx.NewIdRef() + # idSensUp = wx.NewIdRef() + # idSensDown = wx.NewIdRef() # self.menu = wx.Menu(title = '') diff --git a/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py b/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py index b2421a41a..e3d09f1d1 100644 --- a/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py +++ b/PYME/Acquire/Hardware/focus_locks/reflection_focus_lock.py @@ -110,12 +110,13 @@ class ReflectedLinePIDFocusLock(PID): """ def __init__(self, scope, piezo, p=1., i=0.1, d=0.05, sample_time=0.01, mode='frame', fit_roi_size=75, min_amp=0, - max_sigma=np.finfo(float).max): + max_sigma=np.finfo(float).max, min_lateral_sigma=0, + trigger_failsafe=True, multiplier=1): """ Parameters ---------- - scope: PYME.Acquire.Hardware.microscope.microscope + scope: PYME.Acquire.Hardware.microscope.Microscope piezo: PYME.Acquire.Hardware.Piezos.offsetPiezoREST.OffsetPiezoClient p: float i: float @@ -134,25 +135,37 @@ def __init__(self, scope, piezo, p=1., i=0.1, d=0.05, sample_time=0.01, max_sigma : float maximum fit result sigma which we are willing to accept as a valid peak measurement we can use to correct the focus. + min_lateral_sigma : float + minimum standard dev along the dimension we sum (squash) to get the + profile. If the lateral sigma is below this we reject the frame for + fitting because we expect a long spread profile. + trigger_failsafe : bool + if True, try to kill lasers if the profile intensity saturates a + majority of the pixels on the focus lock camera """ self.scope = scope self.piezo = piezo - # self._last_offset = self.piezo.GetOffset() + self._piezo_control_lock = threading.Lock() + self._piezo_home = 0.5 * (self.piezo.GetMax() - self.piezo.GetMin()) self._lock_ok = False self._ok_tolerance = 5 self.fit_roi_size = fit_roi_size self._fitter = GaussFitter1D(min_amp=min_amp, max_sigma=max_sigma) + self._min_lateral_sigma = min_lateral_sigma self.peak_position = self.scope.frameWrangler.currentFrame.shape[1] * 0.5 # default to half of the camera size self.subtraction_profile = None + self.use_failsafe = trigger_failsafe and hasattr(scope, 'failsafe') + PID.__init__(self, p, i, d, setpoint=self.peak_position, auto_mode=False, sample_time=sample_time) self._mode = mode self._polling = False + self.multiplier = multiplier @property def mode(self): @@ -190,27 +203,41 @@ def LockEnabled(self): return self.auto_mode @webframework.register_endpoint('/EnableLock', output_is_json=False) - def EnableLock(self): + def EnableLock(self, enable=True): """ Returns offset to last-known offset before enabling the lock. The servo is generally more robust to changing its setpoint when it is running than when you toggle it off, move off the setpoint, and then slam it on again. This just makes the slam small. """ - # make sure piezo is ready - retry = 0 - while not self.piezo.OnTarget() and retry < 3: - logger.debug('waiting for piezo to stop moving') - time.sleep(0.1) - retry += 1 - logger.debug('Enabling focus lock') - self.set_auto_mode(True) + + # enable could be a string + enable = enable and not (enable == 'False') + + if enable: + # make sure piezo is ready + retry = 0 + while not self.piezo.OnTarget() and retry < 3: + logger.debug('waiting for piezo to stop moving') + time.sleep(0.1) + retry += 1 + logger.debug('Enabling focus lock') + self.set_auto_mode(True) + else: # disable + # take out the lock so we can log offset and disable 'at once' + with self._piezo_control_lock: + self.piezo.LogFocusCorrection(self.piezo.GetOffset()) + self.set_auto_mode(False) + logger.debug('Disabling focus lock') + + @webframework.register_endpoint('/EnableLockAndHome', output_is_json=False) + def EnableLockAndHome(self): + self.EnableLock() + self.piezo.MoveTo(0, self._piezo_home) @webframework.register_endpoint('/DisableLock', output_is_json=False) def DisableLock(self): - self.set_auto_mode(False) - logger.debug('Disabling focus lock') - self.piezo.LogFocusCorrection(self.piezo.GetOffset()) + self.EnableLock(False) def register(self): if self.mode == 'time': @@ -234,6 +261,10 @@ def ChangeSetpoint(self, setpoint=None): self.setpoint = self.peak_position else: self.setpoint = float(setpoint) + + @webframework.register_endpoint('/UpdateHome', output_is_json=False) + def UpdateHome(self, home): + self._piezo_home = float(home) @webframework.register_endpoint('/SetSubtractionProfile', output_is_json=False) def SetSubtractionProfile(self): @@ -304,7 +335,7 @@ def on_target(self, tolerance=None): return bool(abs(self.peak_position - self.setpoint) < tolerance) @webframework.register_endpoint('/ReacquireLock', output_is_json=False) - def ReacquireLock(self, step_size=3.): + def ReacquireLock(self, start_at=0.0, step_size=3., pause=0.75, retries=0): """Routine to call if we've lost the lock. The lock is disabled, objective is moved to its lowest position, and we step upwards gradually until we get decent fits on the profile and the profile is sufficiently @@ -313,47 +344,77 @@ def ReacquireLock(self, step_size=3.): Parameters ---------- + start_at: float, optional + offset to start the scan at (10 positions chosen 'spirally' out + before linearly stepping through the range from bottom up) step_size : float, optional number of microns to step the objective position by when searching, by default 3. + pause : float, optional + seconds to pause after each offset change before checking if we can + lock """ + from itertools import zip_longest step_size = float(step_size) - logger.debug('reacquiring lock') + start_at = float(start_at) + pause = float(pause) + logger.info('reacquiring lock') self.DisableLock() + # get our range min_offset = self.piezo.GetMinOffset() max_offset = self.piezo.GetMaxOffset() - - scan_positions = np.arange(min_offset, max_offset + step_size, - step_size) + start_at = max(min_offset, min(max_offset, start_at)) # clip + + # set up scan positions + scan_up = np.arange(start_at, max_offset + step_size, step_size).tolist() + scan_up.append(max_offset) + scan_down = np.arange(start_at - step_size, min_offset, -step_size).tolist() + scan_down.append(min_offset) + scan_positions = [item for sub in zip_longest(scan_up, scan_down) for item in sub] + scan_positions = [pos for pos in scan_positions if pos is not None] assert len(scan_positions) > 0 - - for pos in scan_positions: - logger.debug('looking for focus, offset: %.1f' % pos) - - self.piezo.SetOffset(pos) - - time.sleep(0.3) - if self.lockable(self._ok_tolerance): - logger.debug('found focus, offset %.1f' % pos) - self.EnableLock() - return + cut = min(len(scan_positions), 10) + # try not to be overly mean to the pifoc, step small after first 10 + scan_positions = scan_positions[:cut] + sorted(scan_positions) + + n = -1 + while n < retries: + n += 1 + for pos in scan_positions: + logger.debug('looking for focus, offset: %.1f' % pos) + self.piezo.SetOffset(pos) + + time.sleep(pause) + if self.lockable(self._ok_tolerance): + logger.info('found focus, offset %.1f' % pos) + self.EnableLock() + return logger.debug('failed to find focus, lowering objective') self.piezo.SetOffset(min_offset) @webframework.register_endpoint('/DisableLockAfterAcquiring', output_is_json=False) - def DisableLockAfterAcquiring(self): + def DisableLockAfterAcquiring(self, target_tolerance=1): self.EnableLock() # make sure we have the lock on + if not self.on_target(float(target_tolerance)): + logger.info('not locked to target tolerance, waiting 0.5 s') + time.sleep(0.5) if not self.LockOK(): - import time - logger.debug('lock not OK, pausing for 5 s') + logger.info('lock not OK, pausing for 5 s') time.sleep(5) if not self.LockOK(): - logger.debug('still not OK, starting pause/reacquire sequence') + logger.info('still not OK, starting pause/reacquire sequence') time.sleep(5) - self.ReacquireLock() + if hasattr(self.scope, '_stage_leveler'): + pos = self.scope.GetPos() + offset = self.scope._stage_leveler.lookup_offset(pos['x'], + pos['y']) + self.ReacquireLock(start_at=offset) + else: + self.ReacquireLock() + time.sleep(0.5) else: logger.debug('lock OK') @@ -361,14 +422,19 @@ def DisableLockAfterAcquiring(self): @webframework.register_endpoint('/DisableLockAfterAcquiringIfEnabled', output_is_json=False) - def DisableLockAfterAcquiringIfEnabled(self): + def DisableLockAfterAcquiringIfEnabled(self, target_tolerance=1): """ Helper function to allow protocols used in automated workflows to make sure they have the right focal plane without barring that protocols use for manual imaging without the focus lock on/set up """ if self.LockEnabled(): - self.DisableLockAfterAcquiring() + self.DisableLockAfterAcquiring(float(target_tolerance)) + + @property + def _failsafe_threshold(self): + frame_shape = self.scope.frameWrangler.currentFrame.shape + return 0.5 * frame_shape[0] * frame_shape[1] * self.scope.cam.noise_properties['SaturationThreshold'] def find_peak(self, profile): """ @@ -394,13 +460,21 @@ def find_peak(self, profile): def on_frame(self, **kwargs): # get focus position - profile = self.scope.frameWrangler.currentFrame.squeeze().sum(axis=0).astype(float) - if self.subtraction_profile is not None: - peak_position, success = self.find_peak(profile - self.subtraction_profile) - else: - peak_position, success = self.find_peak(profile) - - if not success: + cf = self.scope.frameWrangler.currentFrame.squeeze() + profile = cf.sum(axis=0).astype(float) + + if self.use_failsafe and profile.sum() > self._failsafe_threshold: + self.scope.failsafe.kill(message='focus lock profile suggests heat danger') + + try: + assert np.std(cf.sum(axis=1)) > self._min_lateral_sigma + + if self.subtraction_profile is not None: + peak_position, success = self.find_peak(profile - self.subtraction_profile) + else: + peak_position, success = self.find_peak(profile) + assert success + except AssertionError: self._lock_ok = False # restart the integration / derivatives so we don't go wild when we # eventually get a good fit again @@ -414,10 +488,11 @@ def on_frame(self, **kwargs): elapsed_time =_current_time() - self._last_time correction = self(self.peak_position) # note that correction only updates if elapsed_time is larger than sample time - don't apply same correction 2x. - if self.auto_mode and elapsed_time > self.sample_time: - # logger.debug('Correction: %.2f' % correction) - # logger.debug('components %s' % (self.components,)) - self.piezo.CorrectOffset(correction) + with self._piezo_control_lock: + if self.auto_mode and elapsed_time > self.sample_time: + # logger.debug('Correction: %.2f' % correction) + # logger.debug('components %s' % (self.components,)) + self.piezo.CorrectOffset(correction*self.multiplier) class RLPIDFocusLockClient(object): @@ -428,24 +503,41 @@ def __init__(self, host='127.0.0.1', port=9798, name='focus_lock'): self.base_url = 'http://%s:%d' % (host, port) self._session = requests.Session() + self._enabled = False @property def lock_enabled(self): - return self.LockEnabled() + """Returns a cached version of the lock state, which while helpful in + some instances (notably whether to accept a correction from the server) + should usually not replace `LockEnabled` + + Returns + ------- + bool + """ + return self._enabled def LockEnabled(self): response = self._session.get(self.base_url + '/LockEnabled') - return bool(response.json()) + self._enabled = bool(response.json()) + return self.lock_enabled def LockOK(self): response = self._session.get(self.base_url + '/LockOK') return bool(response.json()) - def EnableLock(self): - return self._session.get(self.base_url + '/EnableLock') + def EnableLock(self, enable=True): + self._enabled = enable + return self._session.get(self.base_url + '/EnableLock?enable=%s' % enable) + + def EnableLockAndHome(self): + self._enabled = True + return self._session.get(self.base_url + '/EnableLockAndHome') def DisableLock(self): - return self._session.get(self.base_url + '/DisableLock') + self._enabled = False + r = self._session.get(self.base_url + '/DisableLock') + return r def GetPeakPosition(self): response = self._session.get(self.base_url + '/GetPeakPosition') @@ -455,6 +547,9 @@ def ChangeSetpoint(self, setpoint=None): if setpoint is None: setpoint = self.GetPeakPosition() return self._session.get(self.base_url + '/ChangeSetpoint?setpoint=%3.3f' % (setpoint,)) + + def UpdateHome(self, home): + return self._session.get(self.base_url + '/UpdateHome?home=%3.3f' % (home,)) def ToggleLock(self): if self.lock_enabled: @@ -465,15 +560,18 @@ def ToggleLock(self): def SetSubtractionProfile(self): return self._session.get(self.base_url + '/SetSubtractionProfile') - @webframework.register_endpoint('/ReacquireLock', output_is_json=False) - def ReacquireLock(self, step_size=3.): - return self._session.get(self.base_url + '/ReacquireLock?step_size=%3.3f' % (step_size,)) + def ReacquireLock(self, start_at=0, step_size=3): + return self._session.get(self.base_url + '/ReacquireLock?step_size=%3.3f&start_at=%3.3f' % (step_size, start_at)) - def DisableLockAfterAcquiring(self): - return self._session.get(self.base_url + '/DisableLockAfterAcquiring') + def DisableLockAfterAcquiring(self, target_tolerance=1): + self._enabled = False + r = self._session.get(self.base_url + '/DisableLockAfterAcquiring?target_tolerance=%3.3f' % target_tolerance) + return r - def DisableLockAfterAcquiringIfEnabled(self): - return self._session.get(self.base_url + '/DisableLockAfterAcquiringIfEnabled') + def DisableLockAfterAcquiringIfEnabled(self, target_tolerance=1): + self._enabled = False + r = self._session.get(self.base_url + '/DisableLockAfterAcquiringIfEnabled?target_tolerance=%3.3f' % target_tolerance) + return r class RLPIDFocusLockServer(webframework.APIHTTPServer, ReflectedLinePIDFocusLock): diff --git a/PYME/Acquire/Hardware/frZStage.py b/PYME/Acquire/Hardware/frZStage.py index f4ee5c917..a41d44d10 100755 --- a/PYME/Acquire/Hardware/frZStage.py +++ b/PYME/Acquire/Hardware/frZStage.py @@ -1,26 +1,26 @@ -#!/usr/bin/python - -################## -# frZStage.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - +#!/usr/bin/python + +################## +# frZStage.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + #Boa:Frame:frZStepper import wx @@ -31,7 +31,7 @@ def create(parent): [wxID_FRZSTEPPER, wxID_FRZSTEPPERBMINUS1U, wxID_FRZSTEPPERBMINUS200N, wxID_FRZSTEPPERBMINUS50N, wxID_FRZSTEPPERBPLUS1U, wxID_FRZSTEPPERBPLUS200N, wxID_FRZSTEPPERBPLUS50N, wxID_FRZSTEPPERPANEL1, wxID_FRZSTEPPERTCCURPOS, -] = [wx.NewId() for _init_ctrls in range(9)] +] = [wx.NewIdRef() for _init_ctrls in range(9)] class frZStepper(wx.Frame): def _init_ctrls(self, prnt): diff --git a/PYME/Acquire/Hardware/ids_peak_cam.py b/PYME/Acquire/Hardware/ids_peak_cam.py new file mode 100644 index 000000000..24468f672 --- /dev/null +++ b/PYME/Acquire/Hardware/ids_peak_cam.py @@ -0,0 +1,561 @@ + +from PYME.Acquire.Hardware.Camera import Camera +from ids_peak import ids_peak as peak +import logging +import numpy as np +import threading +import time +import ctypes +import queue +from PYME.Acquire import eventLog as event_log +from threading import Lock + +# The IDS Peak API supports uEye+ cameras (U3/GV models) as well as "almost all" uEye (UI models) +# to use this implementation, first install the IDS Peak SDK (our interface developed on version 2.6.2.0) +# You'll have to download the IDS Peak SDK from the IDS website and install it. +# IDS Peak 2.10 and on: install the ids_peak python package from pypi +# https://pypi.org/project/ids-peak/ +# IDS Peak <2.10: install the ids_peak python package from their wheel in your IDS peak installation folder, e.g.: +# C:\Program Files\IDS\ids_peak\sdk\api\binding\python\wheel\x86_[32|64] +# pip install ids_peak--cp-cp[m]-[win32|win_amd64].whl +# You can ignore the IPL / AFL wheels - these are for 'basic image processing' and 'auto features' libraries +# which we don't use. We use the generic SDK (rather than the 'comfort' SDK). +# For my installation, 64 bit windows, python 3.8, I installed ids_peak with: +# python -m pip install "C:\Program Files\IDS\ids_peak\generic_sdk\api\binding\python\wheel\x86_64\ids_peak-1.6.2.0-cp38-cp38-win_amd64.whl" + +logger = logging.getLogger(__name__) + +def find_ids_peak_cameras(): + peak.Library.Initialize() + device_manager = peak.DeviceManager.Instance() + device_manager.Update() + return device_manager.Devices() + +# at the moment, only support 'unpacked' types, i.e. 12 bit in 2 bytes, not 1.5 bytes +PIXEL_FORMATS = { + 8: 'Mono8', + 10: 'Mono10', + 12: 'Mono12', + # 16: 'Mono16', # Only supported by uEye cameras (UI models, not e.g. U3) +} + +class IDS_Camera(Camera): + """ + + Notes + ----- + Camera noise_properties dictionary will be keyed off the gain_mode property, + which corresponds to the bit-depth for this class. See uEye example in + PYME.Acquire.Hardware.camera_noise. + """ + + supports_software_trigger = True + + def __init__(self, device_number=0, nbits=8): + import sys + self.initialized = False + super().__init__() + self.device_number = device_number + self.nbits = nbits + self.n_full = 0 + self._buffer_lock = Lock() # use to avoid race conditions while polling buffers during their destruction + + devices = find_ids_peak_cameras() + if len(devices) == 0: + raise RuntimeError('No IDS peak cameras found') + + # open the device + self.device = devices[device_number].OpenDevice(peak.DeviceAccessType_Control) + self.serial_number = self.device.SerialNumber() + self.model_name = self.device.ModelName() + logger.info(f'IDS peak camera {self.model_name} opened, serial number {self.serial_number}') + + # get the remote device 'node map' + self._node_map = self.device.RemoteDevice().NodeMaps()[0] + + # open a datastream + # self._data_stream = self.device.DataStreams()[0] + self._data_stream = self.device.DataStreams()[0].OpenDataStream() + self._data_stream_node_map = self._data_stream.NodeMaps()[0] + # set to FIFO + self._data_stream_node_map.FindNode("StreamBufferHandlingMode").SetCurrentEntry("OldestFirst") + + # set ROI size to full usable sensor size --------------------- + # get min offsets. Note that some IDS cameras do not use the full chip. + self._offset_x_min = self._node_map.FindNode("OffsetX").Minimum() + self._offset_y_min = self._node_map.FindNode("OffsetY").Minimum() + self._width_min = self._node_map.FindNode("Width").Minimum() + self._height_min = self._node_map.FindNode("Height").Minimum() + + # set the minimum, to remove size restrictions due to current settings + self._node_map.FindNode("OffsetX").SetValue(self._offset_x_min) + self._node_map.FindNode("OffsetY").SetValue(self._offset_y_min) + self._node_map.FindNode("Width").SetValue(self._width_min) + self._node_map.FindNode("Height").SetValue(self._height_min) + + # now query max values + self._offset_x_max = self._node_map.FindNode("OffsetX").Maximum() + self._offset_y_max = self._node_map.FindNode("OffsetY").Maximum() + self._width_max = self._node_map.FindNode("Width").Maximum() + self._height_max = self._node_map.FindNode("Height").Maximum() + # get increments + self._offset_x_incr = self._node_map.FindNode("OffsetX").Increment() + self._offset_y_incr = self._node_map.FindNode("OffsetY").Increment() + self._width_increment = self._node_map.FindNode("Width").Increment() + self._height_increment = self._node_map.FindNode("Height").Increment() + + # set ROI to full sensor size: + self._node_map.FindNode("OffsetX").SetValue(self._offset_x_min) + self._node_map.FindNode("OffsetY").SetValue(self._offset_y_min) + self._node_map.FindNode("Width").SetValue(self._width_max) + self._node_map.FindNode("Height").SetValue(self._height_max) + # ---------------------------------------------------------------- + + # find out if this model supports board/sensor temperature: + try: + self._node_map.FindNode("DeviceTemperature") + self._has_temperature = True + except: + self._has_temperature = False + + self.SetAcquisitionMode(self.MODE_CONTINUOUS) + # allocate buffers + # self.allocate_buffers() + self.full_buffers = None + + self._buffer_poll_wait_time_ms = 5000 # [ms] + + # set pixel format + self._node_map.FindNode("PixelFormat").SetCurrentEntry(PIXEL_FORMATS[nbits]) + # IDS sends all multi-byte little-endian, and we're going to copy in native computer order, + # throw an error unless we're matched. + assert sys.byteorder == 'little' + + self.Init() + self.initialized = True + + def Init(self): + self._poll = False + self.poll_loop_active = True + self.poll_thread = threading.Thread(target=self._poll_loop) + self.poll_thread.start() + + def _poll_loop(self): + while self.poll_loop_active: + if self._poll: # only poll if an acquisition is running + try: + with self._buffer_lock: + self._poll_buffer() + except Exception as e: + logger.exception(str(e)) + else: + time.sleep(.05) + + def ExpReady(self): + return (self.full_buffers is not None) and (self.n_full > 0) + + def ExtractColor(self, ch_slice, mode): + # get nowait to hard-throw an Empty error if we've entered this method + # and we shouldn't have + buf = self.full_buffers.get_nowait() + ch_slice[:] = buf.T + if self.free_buffers is not None: + # recycle buffer + self.free_buffers.put(buf) + self.n_full -= 1 + + def _poll_buffer(self): + try: + buffer = self._data_stream.WaitForFinishedBuffer(self._buffer_poll_wait_time_ms) + # copy over + ctypes.memmove(self.transfer_buffer, int(buffer.BasePtr()), int(buffer.Size())) + # ctypes uses native byteorder, so we are OK if this is little-endian system + arr = np.frombuffer(self.transfer_buffer, dtype=self.transfer_buffer_dtype) + # return camera buffer to queue + self._data_stream.QueueBuffer(buffer) + arr = arr.reshape((self.curr_height, self.curr_width)) + self.full_buffers.put(arr) + self.n_full += 1 + except Exception as e: + logger.error(f'Error polling buffer: {e}') + + + def CamReady(self): + """ + Returns true if the camera is ready (initialized) not really used for + anything, but might still be checked. + + Returns + ------- + bool + Is the camera ready? + """ + + return self.initialized + + def Close(self): + self.StopAq() + # self.DestroyBuffers() # already destroyed in StopAq + peak.Library.Close() + + def DestroyBuffers(self): + with self._buffer_lock: + self.n_full = 0 + + # remove camera-side buffers + for b in self._data_stream.AnnouncedBuffers(): + try: + self._data_stream.RevokeBuffer(b) + except Exception as e: + logger.error(f'Error revoking buffer: {e}') + + # computer RAM: destroy free and full buffer queues + while not self.full_buffers.empty(): + try: + self.full_buffers.get_nowait() + except queue.Empty: + pass + + while not self.free_buffers.empty(): + try: + self.free_buffers.get_nowait() + except queue.Empty: + pass + + def allocate_buffers(self, n_buffers=50): + self._n_cam_buffers = n_buffers + # camera side + try: + self._data_stream.Flush(peak.DataStreamFlushMode_DiscardAll) + for b in self._data_stream.AnnouncedBuffers(): + self._data_stream.RevokeBuffer(b) + # get current payload size + self._payload_size = self._node_map.FindNode('PayloadSize').Value() + # allocate buffers + for ind in range(n_buffers): + b = self._data_stream.AllocAndAnnounceBuffer(self._payload_size) + self._data_stream.QueueBuffer(b) + except Exception as e: + logger.error(f'Error allocating buffers: {e}') + raise e + + # computer RAM + + # transfer buffer + if self.nbits == 8: + bufferdtype = np.uint8 + else: # 10 & 12 bits + bufferdtype = np.uint16 + + self.curr_height, self.curr_width = self.GetPicHeight(), self.GetPicWidth() + self.transfer_buffer_size = self.curr_height * self.curr_width * bufferdtype().itemsize + self.transfer_buffer_dtype = bufferdtype + self.transfer_buffer = ctypes.create_string_buffer(self.transfer_buffer_size) + self.transfer_buffer_memory_v = ctypes.c_char() + self.transfer_buffer_memory = ctypes.pointer(self.transfer_buffer_memory_v) + self.transfer_buffer_id = ctypes.c_int() + # others + self.free_buffers = queue.Queue() + self.full_buffers = queue.Queue() + for ind in range(n_buffers): + # Note that it is essential to use zeros here to ensure + # compatibility with Mono8, Mono10, Mono12 written into uint16 + self.free_buffers.put(np.zeros((self.GetPicHeight(), self.GetPicWidth()), + dtype=np.uint16)) + self._poll = False + + def StopAq(self): + self._poll = False + try: + # tell the camera to stop + self._node_map.FindNode('AcquisitionStop').Execute() + + # stop the datastream + self._data_stream.KillWait() # interrupts 1 WaitForFinishedBuffer call + self._data_stream.StopAcquisition(peak.AcquisitionStopMode_Default) + # flush TODO - do we really want to flush immediately? + self._data_stream.Flush(peak.DataStreamFlushMode_DiscardAll) + + # unlock parameters + self._node_map.FindNode('TLParamsLocked').SetValue(0) + except Exception as e: + logger.error(f'Error stopping acquisition: {e}') + raise e + self.DestroyBuffers() + + def StartExposure(self): + logger.debug('StartAq') + if self._poll: + # stop, we'll allocate buffers and restart + self.StopAq() + # allocate at least 2 seconds of buffers + buffer_size = int(max(2 * self.GetFPS(), 50)) + logger.info('Allocating {} buffers'.format(buffer_size)) + self.allocate_buffers(buffer_size) + + self._log_exposure_start() + try: + + if self._acq_mode == self.MODE_CONTINUOUS: + self._node_map.FindNode('TriggerMode').SetCurrentEntry('Off') + + elif self._acq_mode == self.MODE_SOFTWARE_TRIGGER: + self._node_map.FindNode('TriggerSelector').SetCurrentEntry('ExposureStart') + self._node_map.FindNode('TriggerSource').SetCurrentEntry('Software') + self._node_map.FindNode('TriggerMode').SetCurrentEntry('On') + + + elif self._acq_mode == self.MODE_HARDWARE_TRIGGER: + self._node_map.FindNode('TriggerSelector').SetCurrentEntry('ExposureStart') + # line0 should be (trigger) input with optocoupler for all IDS + # cameras covered in IDS peak 2.10.0 with hardware triggering + self._node_map.FindNode('TriggerSource').SetCurrentEntry('Line0') + self._node_map.FindNode('TriggerMode').SetCurrentEntry('On') + else: + raise NotImplementedError('Single shot mode not implemented') + + self._node_map.FindNode('TLParamsLocked').SetValue(1) + self._data_stream.StartAcquisition(peak.AcquisitionStartMode_Default, + peak.DataStream.INFINITE_NUMBER) + self._node_map.FindNode('AcquisitionStart').Execute() + + except Exception as e: + logger.error(f'Error starting acquisition: {e}') + raise e + self._poll = True + return 0 + + def FireSoftwareTrigger(self): + self._node_map.FindNode('TriggerSoftware').Execute() + self._log_exposure_start() + # self._node_map.FineNode('TriggerSoftware').WaitUntilDone(1000) + + def GetIntegTime(self): + """ + Get the current exposure time. + + Returns + ------- + float + The exposure time in s + + See Also + -------- + SetIntegTime + """ + exposure_time = self._node_map.FindNode("ExposureTime").Value() # [us] + return exposure_time / 1e6 # [s] + + def SetIntegTime(self, exposure_time): + """ + Set the exposure time. Automatically adjusts frame rate + + Parameters + ---------- + exposure_time : float + The exposure time in s + + See Also + -------- + GetIntegTime + """ + # note that setting exposure time will automatically increase decrease + # FPS if necessary, but setting exposure time lower will not + # automatically increase FPS + + # Exposure time min/max will auto adjust given the current frame rate, + # so best to ignore it, and let frame rate adjust to what it can. + # lower = self._node_map.FindNode("ExposureTime").Minimum() # [us] + # upper = self._node_map.FindNode("ExposureTime").Maximum() # [us] + # exp_time = np.clip(exposure_time * 1e6, lower, upper) # [us] + # logger.info(f'Setting exposure time to {exp_time} us') + # self._node_map.FindNode("ExposureTime").SetValue(exp_time) + self._node_map.FindNode("ExposureTime").SetValue(exposure_time * 1e6) + fps_target = 1 / exposure_time # [FPS] + lower = self._node_map.FindNode("AcquisitionFrameRate").Minimum() # [FPS] + upper = self._node_map.FindNode("AcquisitionFrameRate").Maximum() # [FPS] + fps = np.clip(fps_target, lower, upper) + self._node_map.FindNode("AcquisitionFrameRate").SetValue(fps) + + def GetPicWidth(self): + """ + Returns the width (in pixels) of the currently selected ROI. + + Returns + ------- + int + Width of ROI (pixels) + """ + x0, _, x1, _ = self.GetROI() + return x1 - x0 + + def GetPicHeight(self): + """ + Returns the height (in pixels) of the currently selected ROI + + Returns + ------- + int + Height of ROI (pixels) + """ + _, y0, _, y1 = self.GetROI() + return y1 - y0 + + def GetNumImsBuffered(self): + """ + Return the number of images in the buffer. + + Returns + ------- + int + Number of images in buffer + """ + # can enable camera-side buffer queue monitoring, but seems to return total number + # of frames acquired, not number of full frames waiting in the queue. + # self._data_stream_node_map.FindNode("BufferStatusMonitoringEnabled").SetValue(True) + # return self._data_stream_node_map.FindNode("BufferStatusOutputQueueCount").Value() + + return self.n_full + + def GetBufferSize(self): + """ + Return the total size of the buffer (in images). + + Returns + ------- + int + Number of images that can be stored in the buffer. + """ + if self._poll: + return self._n_cam_buffers + else: + # if we aren't polling, spoof infinitely large buffer so we don't + # flag a buffer overflow while we e.g. rebuild the buffers. This + # makes no functional difference for us, but avoids a spurious + # warning from frameWrangler about the buffer overflowing. + return np.iinfo(np.int32).max + + def GetROI(self): + """ + Returns the current ROI as a tuple of (x0, y0, x1, y1). + + Returns + ------- + tuple + (x0, y0, x1, y1) + """ + x0 = self._node_map.FindNode("OffsetX").Value() + y0 = self._node_map.FindNode("OffsetY").Value() + x1 = x0 + self._node_map.FindNode("Width").Value() + y1 = y0 + self._node_map.FindNode("Height").Value() + return (x0, y0, x1, y1) + + def SetROI(self, x1, y1, x2, y2): + """ + Set the ROI via coordinates (as opposed to via an index). + + Parameters + ---------- + x1 : int + Left x-coordinate, zero-indexed + y1 : int + Top y-coordinate, zero-indexed + x2 : int + Right x-coordinate, (excluded from ROI) + y2 : int + Bottom y-coordinate, (excluded from ROI) + + Returns + ------- + None + + + """ + logger.debug('setting ROI: %d, %d, %d, %d' % (x1, y1, x2, y2)) + + # set the minimum, to remove size restrictions due to current settings + self._node_map.FindNode("OffsetX").SetValue(self._offset_x_min) + self._node_map.FindNode("OffsetY").SetValue(self._offset_y_min) + self._node_map.FindNode("Width").SetValue(self._width_min) + self._node_map.FindNode("Height").SetValue(self._height_min) + + # check offset increments + x1 -= x1 % self._offset_x_incr + y1 -= y1 % self._offset_y_incr + # clip to bounds + x1 = int(np.clip(x1, self._offset_x_min, self._offset_x_max)) + y1 = int(np.clip(y1, self._offset_y_min, self._offset_y_max)) + x2 = int(np.clip(x2, x1 + self._width_min, self._width_max)) + y2 = int(np.clip(y2, y1 + self._height_min, self._height_max)) + # double check width/height increments + x2 -= (x2 - x1) % self._width_increment # ROI must be a multiple of increment + y2 -= (y2 - y1) % self._height_increment + logger.debug('adjusted ROI: %d, %d, %d, %d' % (x1, y1, x2, y2)) + + self._node_map.FindNode("OffsetX").SetValue(x1) + self._node_map.FindNode("OffsetY").SetValue(y1) + self._node_map.FindNode("Width").SetValue(x2 - x1) + self._node_map.FindNode("Height").SetValue(y2 - y1) + # using ueye api we used to have to set integration time after adjusting + # ROI. Not sure if we need that here or not. Leave it for not just in case. + self.SetIntegTime(self.GetIntegTime()) + + def SetAcquisitionMode(self, mode): + """Set a flag so next StartExposure can toggle between continuous and single shot mode. + + Parameters + ---------- + mode : int + toggles between continuous and single shot mode + """ + self._acq_mode = mode + + def GetAcquisitionMode(self): + return self._acq_mode + + def GetFPS(self): + """ + Returns the current frame rate (in frames per second). + + Returns + ------- + float + Frame rate (fps) + """ + return self._node_map.FindNode("AcquisitionFrameRate").Value() + + def GetSerialNumber(self): + return self.serial_number + + def GetCCDTemp(self): + if self._has_temperature: + return self._node_map.FindNode("DeviceTemperature").Value() # [degrees C] + else: + return 0 + + def GetCycleTime(self): + return 1 / self.GetFPS() + + def GetCCDHeight(self): + """ + Returns + ------- + int + The sensor height in pixels + + """ + return self._height_max + + def GetCCDWidth(self): + """ + Returns + ------- + int + The sensor width in pixels + + """ + return self._width_max + + @property + def _gain_mode(self): + return '%d-bit' % self.nbits diff --git a/PYME/Acquire/Hardware/ioslave.py b/PYME/Acquire/Hardware/ioslave.py index b68d8090f..050ce6632 100644 --- a/PYME/Acquire/Hardware/ioslave.py +++ b/PYME/Acquire/Hardware/ioslave.py @@ -108,7 +108,7 @@ def GetAnalog(self, chan): with self.lock: with serial.Serial(**self.ser_args) as ser: ser.write(b'QA%d\n' % chan) - res = float(str(ser.readline())) + res = float((ser.readline()).decode().split('\\')[0]) return res @@ -157,7 +157,7 @@ def SetServo(self, chan, value): def GetAnalog(self, chan): with self.lock: self.ser.write(b'QA%d\n' % chan) - res = float(str(self.ser.readline())) + res = float((self.ser.readline()).decode().split('\\')[0]) return res diff --git a/PYME/Acquire/Hardware/meson.build b/PYME/Acquire/Hardware/meson.build new file mode 100644 index 000000000..825828b49 --- /dev/null +++ b/PYME/Acquire/Hardware/meson.build @@ -0,0 +1,225 @@ + +# Boilerplate to make sure things go in the right place - TODO can we do some of this in the top-level meson.build? +#py = import('python').find_installation(pure: false) +#np_include_dir = run_command(py, ['-c', '"import numpy; print(numpy.get_include())"'], check: true).stdout().strip() +install_dir = py.get_install_dir() / 'PYME/Acquire/Hardware' + +py_sources = files( + 'driftTracking.py', + 'lasers.py', + 'NikonTiGUI.py', + 'CameraSkeleton.py', + 'thorlabs_cam.py', + 'thorlabs_elliptec.py', + 'thorlabs_elliptec_serial.py', + 'NikonTi.py', + 'cobaltLaser.py', + 'thorlabsPiezo.py', + 'ccdAdjPanel.py', + 'mpd_picosecond_delayer.py', + 'matchboxLaser.py', + 'focusKeys.py', + 'TiLightCrafter.py', + 'ccdCalibrator.py', + 'multiview.py', + 'camera_noise.py', + 'phoxxLaser.py', + 'OrielCornerstone.py', + 'ueye.py', + 'splitter.py', + '__init__.py', + 'fw102.py', + 'comports.py', + 'toptica_ibeam.py', + 'ExciterWheel.py', + 'driftTrackGUI.py', + 'phoxxLaserOLD.py', + 'priorarclampshutter.py', + 'NikonTE2000.py', + 'FocCorr.py', + 'thorlabs_mff_flipper.py', + 'FilterWheel.py', + 'microscope_adapter.py', + 'Camera.py', + 'setup.py', + 'arclampshutterpanel.py', + 'aotf.py', + 'cobaltLaser561.py', + 'cameraSoftwareBuffer.py', + 'PM100USB.py', + 'fakeShutters.py', + 'spacenav.py', + 'LaserControlFrame.py', + 'EMCCDTheory.py', + 'FocCorrR.py', + 'ioslave.py', + 'FrFilter.py', + 'DMDGui.py', + 'frZStage.py', + 'ids_peak_cam.py', + 'olympusix81.py', + 'priorLumen.py', +) + +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware') + +py_sources = files( + 'AndorIXon/AndorCam.py', + 'AndorIXon/__init__.py', + 'AndorIXon/AndorControlFrame.py', + 'AndorIXon/AndorIXon.py', +) + +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/AndorIXon') + +py_sources = files( + 'AndorNeo/ZylaControlPanel.py', + 'AndorNeo/SDK3Cam.py', + 'AndorNeo/SDK3.py', + 'AndorNeo/AndorNeoControlFrame.py', + 'AndorNeo/AndorZyla.py', + 'AndorNeo/__init__.py', + 'AndorNeo/plotTimings.py', + 'AndorNeo/AndorNeo.py', + 'AndorNeo/parseAtdebug.py', + 'AndorNeo/testNeo.py', +) + +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/AndorNeo') + +py_sources = files( + 'DigiData/RemoteDigiData.py', + 'DigiData/DigiData.py', + 'DigiData/__init__.py', + 'DigiData/DigiDataClient.py', + 'DigiData/axDD132x.py', +) + +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/DigiData') + +subdir('Simulator') + +# py_sources = files( +# 'Old/mc2000.py', +# 'Old/esp300.py', +# 'Old/lpttest.py', +# ) +# py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Old') + +py_sources = files( + 'Piezos/piezo_e816.py', + 'Piezos/piezo_test.py', + 'Piezos/piezo_c867.py', + 'Piezos/piezo_e816_dll.py', + 'Piezos/piezo_pipython_gcs.py', + 'Piezos/__init__.py', + 'Piezos/offsetPiezoREST.py', + 'Piezos/offsetPiezo.py', + 'Piezos/piezo_e709.py', + 'Piezos/piezo_e255.py', + 'Piezos/base_piezo.py', + 'Piezos/piezo_e816b.py', + 'Piezos/piezo_e662.py', + 'Piezos/piezo_e816_corr.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Piezos') + + +# Coherent +py_sources = files( + 'Coherent/OBIS.py', + 'Coherent/Sapphire.py', + 'Coherent/__init__.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Coherent') + +# GCS +py_sources = files( + 'GCS/GCS_DLL.py', + 'GCS/__init__.py', + 'GCS/gcs.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/GCS') + +# HamamatsuDCAM +py_sources = files( + 'HamamatsuDCAM/HamamatsuDCAM.py', + 'HamamatsuDCAM/HamamatsuORCA.py', + 'HamamatsuDCAM/Hamamatsu_control_panel.py', + 'HamamatsuDCAM/__init__.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/HamamatsuDCAM') + +# MPBCommunications +py_sources = files( + 'MPBCommunications/MPBCW.py', + 'MPBCommunications/__init__.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/MPBCommunications') + +# Mercury +py_sources = files( + 'Mercury/Mercury.py', + 'Mercury/PI_Mercury_GCS_DLL.py', + 'Mercury/__init__.py', + 'Mercury/mercuryStepper.py', + 'Mercury/mercuryStepperGCS.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Mercury') + +# Tango +py_sources = files( + 'Tango/__init__.py', + 'Tango/marzhauser_tango.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/Tango') + +# arduino +# Only io_slave/ subdir present, skipping as no .py files at top level + +# focus_locks +py_sources = files( + 'focus_locks/__init__.py', + 'focus_locks/reflection_focus_lock.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/focus_locks') + +# pco +py_sources = files( + 'pco/__init__.py', + 'pco/pco_cam.py', + 'pco/pco_edge_42_lt.py', + 'pco/pco_sdk.py', + 'pco/pco_sdk_cam.py', + 'pco/pco_sdk_cam_control_panel.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/pco') + +# uc480 +py_sources = files( + 'uc480/AndorControlFrame.py', + 'uc480/__init__.py', + 'uc480/uCam480.py', + 'uc480/uc480.py', + 'uc480/uc480Deprecated.py', + 'uc480/uc480_h.py', + 'uc480/uc480_h_gen.py', + 'uc480/ucCamControlFrame.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/uc480') + +# AAOptoelectronics +py_sources = files( + 'AAOptoelectronics/MDS.py', + 'AAOptoelectronics/__init__.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/AAOptoelectronics') + +# ARCoptix +py_sources = files( + 'ARCoptix/__init__.py', + 'ARCoptix/lcdriver.py', + 'ARCoptix/lcserver32.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Hardware/ARCoptix') + diff --git a/PYME/Acquire/Hardware/mpd_picosecond_delayer.py b/PYME/Acquire/Hardware/mpd_picosecond_delayer.py new file mode 100644 index 000000000..7753e195c --- /dev/null +++ b/PYME/Acquire/Hardware/mpd_picosecond_delayer.py @@ -0,0 +1,395 @@ + +## Microphoton devices picosecond delay module + +import serial +import threading +import logging + +logger = logging.getLogger(__name__) + +mpd_psd_errors = { + 1 : 'Command not recognized', + 2 : 'Picosecond Delayer in local mode; cannot set parameters', + 3 : 'Frequency divider value too high (>999); parameter is not set', + 4 : 'Frequency divider value too low (<1); parameter is not set', + 5 : 'Trigger level is higher than maximum 2 V; parameter is not set', + 6 : 'Trigger level is lower than minimum -2 V; parameter is not set', + 7 : 'Delay value is higher than the maximum delay; parameter is not set', + 8 : 'Delay value is less than 0 ps; parameter is not set', + 9 : 'Pulse width value too high (>250 ns); parameter is not set', + 10: 'Pulse width value too low (<1 ns); parameter is not set' +} + +def check_success(resp): + if b'ERR' in resp: + err_no = int((resp.split(b'ERR')[-1]).rstrip(b'#').decode()) + try: + raise RuntimeError(mpd_psd_errors[err_no]) + except KeyError: + raise RuntimeError('Unknown error code; %d' % err_no) + + +class PicosecondDelayer(object): + """ + Microphoton devices picosecond delay module. This implementation uses + serial commands without context managers due to issues experienced with + an arduino previously + (see https://github.com/python-microscopy/python-microscopy/issues/1194) + + However, that would potentially be a cleaner option than opening the port + and leaving it open continually. + + Most properties are implemented as properties with getters returning + cached values, while GetXXXX calls will query the board over serial. + + Echo mode means the board replies back the command it received, the string + terminator '#' and then its response (and anoher #). Somewhat nebulous + message in manual that turning off echo mode can interupt communication + with he board. + + Local mode means most settings (delay, pulse width, trigger level, divider, + edge, or I/O) cannot be set over serial and must be set using the front + panel of the box. + """ + def __init__(self, port='COM4'): + self.lock = threading.Lock() + self.ser = serial.Serial(port=port, baudrate=115200, + bytesize=serial.EIGHTBITS, + parity=serial.PARITY_NONE, + stopbits=serial.STOPBITS_ONE, + rtscts=False, timeout=.1, writeTimeout=2) + + self._echo_mode = True # unit starts up in echo mode + self._delay = self.GetDelay() + self._pulse_width = self.GetPulseWidth() + self._trigger_level = self.GetTriggerLevel() + self._divide_by = self.GetFrequencyDivider() + self._edge = self.GetEdge() + self._enabled = self.GetIO() + self._temperature = self.GetTemperature() + self._max_delay = self.GetMaxDelay() + + # there is no way to query high-speed mode without setting it. + self._high_speed_mode = False # start us off in normal mode + + def GenStartMetadata(self, mdh): + """ + Most of these settings are reasonable to use cached values. + Temperature we'll grab live, and delay we'll leave out as it + will propagate through its state handler. + """ + mdh['PicosecondDelayer.PulseWidth_ns'] = self.pulse_width + mdh['PicosecondDelayer.TriggerLevel_mV'] = self.trigger_level + mdh['PicosecondDelayer.Edge'] = self.edge + mdh['PicosecondDelayer.FrequencyDivider'] = self.frequency_divider + mdh['PicosecondDelayer.Temperature_C'] = self.GetTemperature() + mdh['PicosecondDelayer.HighSpeedMode'] = self.high_speed_mode + + def register(self, scope): + """ + Add start metadata (anything interesting) and add a state handler for the delay itself + so we can change it with scope state updates. + """ + from PYME.IO import MetaDataHandler + MetaDataHandler.provideStartMetadata.append(self.GenStartMetadata) + + scope.state.registerHandler('PicosecondDelayer.Delay_ps', getFcn=lambda : self.delay, + setFcn=lambda y: self.__class__.delay.__set__(self, y)) + + def __del__(self): + # make sure display on box is useful + self.high_speed_mode = False + # close out serial connection + with self.lock: + self.ser.close() + + def send_command(self, cmd): + """Forward command to the unit, check for errors, parse return + + Args: + cmd (bytes): command to send to the unit, complete with b'#' + terminator. + + Returns: + bytes: reply from the unit, with b'#' and original command + (if unit is in echo mode) removed. + """ + base_cmd = cmd.split(b'#')[0] + b'#' + with self.lock: + self.ser.write(cmd + b'\n') + resp = self.ser.readline() + check_success(resp) + if self.echo_mode: + return ((resp.split(base_cmd)[-1]).rstrip(b'#')).decode() + else: + return (resp.rstrip(b'#')).decode() + + @property + def temperature(self): + """ + Returns + ------- + temp: float + Temperature in units of Celsius. Should stabilize to about 55 C + """ + return self._temperature + + @property + def delay(self): + """ + + Returns: + int: delay setting in units of picoseconds + """ + return self._delay + + @delay.setter + def delay(self, delay): + """ + Parameters + ---------- + delay: int + delay setpoint in units of picoseconds. Delay can be varied in 10 ps + steps from 0 to MAX-DELAY. MAX-DELAY is slightly nuanced, but + something like 50 nanoseconds. + """ + self._delay = int(self.send_command(b'SD%d#' % delay)) + + @property + def pulse_width(self): + """ + + Returns: + int: output pulse-width duration in units of nanoseconds + """ + return self._pulse_width + + @pulse_width.setter + def pulse_width(self, pulse_width): + """ + Parameters + ---------- + pulse_width: int + pulse width duration in nanoseconds. Possible values are + non-linearly distributed from 1 ns to 250 ns, and will be rounded to + on the unit. + """ + self._pulse_width = int(self.send_command(b'SP%d#' % pulse_width)) + + @property + def trigger_level(self): + """ + + Returns: + int: trigger level in units of millivolts + """ + return self._trigger_level + + @trigger_level.setter + def trigger_level(self, trigger_level): + """ + Parameters + ---------- + trigger_level: int + threshold voltage in mV to trigger an output pulse. Can be set in + 10 mV steps from -2 V to + 2 V. Level is rounded to the nearest + 10 mV on unit. + """ + self._trigger_level = int(self.send_command(b'SH%d#' % trigger_level)) + + @property + def frequency_divider(self): + """ + + Returns: + int: divide_by value (to allow skipping input triggers) + """ + return self._divide_by + + @frequency_divider.setter + def frequency_divider(self, divide_by): + """ + Parameters + ---------- + divide_by: int + frequency divider factor. Possible values are integers from 1 to + 999. + """ + self._divide_by = int(self.send_command(b'SV%d#' % divide_by)) + + @property + def edge(self): + """ + + Returns: + bool: whether significant edge is rising (True) or falling (False) + """ + return self._edge + + @edge.setter + def edge(self, rising): + """ + Parameters + ---------- + rising: bool + significant edge for trigger input. False: falling edge, True: + rising edge. + """ + self._edge = bool(self.send_command(b'SE%d#' % rising)) + + @property + def io(self): + """ + + Returns: + bool: whether output signal is enable (True) or disabled (False) + """ + return self._enabled + + @io.setter + def io(self, io): + """ + Parameters + ---------- + io: bool + enable (True) or disable (False) output signal + """ + self._enabled = bool(self.send_command(b'EO%d#' % io)) + + def Enable(self): + """ + Turn on output + """ + self.io = True + + def Disable(self): + """ + Turn off output, ignoring triggers + """ + self.io = False + + @property + def echo_mode(self): + """ + Returns: + bool: whether unit is in echo mode (True), where it + replies to any commmand first with the command it received + """ + return self._echo_mode + + @echo_mode.setter + def echo_mode(self, echo_mode): + """ + Parameters + ---------- + echo_mode: bool + enable (True) or disable (False) echo mode + """ + if not echo_mode: + logger.warn('toggling echo-mode during serial operation may interrupt communication') + self._echo_mode = bool(self.send_command(b'EM%d#' % echo_mode)) + + @property + def high_speed_mode(self): + """ + + Returns + ------- + bool: whether unit is in high-speed mode (True) where the unit display does + not update in order to achieve the fastest set-delay update rate + """ + return self._high_speed_mode + + @high_speed_mode.setter + def high_speed_mode(self, high_speed): + """ + high-speed mode stops refreshing the display on the unit in order to + achieve the fastest set-delay update rate. + + Parameters + ---------- + high_speed: bool + enable (True) or disable (False) high-speed mode, + """ + self._high_speed_mode = bool(self.send_command(b'HS%d#' % high_speed)) + + def GetTemperature(self): + """ Query device and return current temperature + Returns + ------- + float: Temperature in units of Celsius. Should stabilize to about 55 C + """ + self._temperature = float(self.send_command(b'RT#')) + return self._temperature + + def GetDelay(self): + """ Query device and return current delay setpoint + Returns + ---------- + int: delay setpoint in units of picoseconds. Delay can be varied in 10 ps + steps from 0 to MAX-DELAY. MAX-DELAY is slightly nuanced, but + something like 50 nanoseconds. + """ + self._delay = int(self.send_command(b'RD#')) + return self._delay + + def GetPulseWidth(self): + self._pulse_width = int(self.send_command(b'RP#')) + return self._pulse_width + + def GetTriggerLevel(self): + """ Query device for trigger level setpoint + + Returns: + int: threshold voltage in mV to trigger an output pulse. Can be set in + 10 mV steps from -2 V to + 2 V. Level is rounded to the nearest + 10 mV on unit. + """ + self._trigger_level = int(self.send_command(b'RH#')) + return self._trigger_level + + def GetEdge(self): + """ Query device for significant edge setting + + Returns + ------- + bool + significant edge for trigger input. False: falling edge, True: + rising edge. + """ + self._edge = bool(self.send_command(b'RE#')) + return self._edge + + def GetIO(self): + """Query device for whether output is enabled/disabled + + Returns + ------- + bool + enabled (True) or disabled (False) + """ + self._io = bool(self.send_command(b'RO#')) + return self._io + + def GetFrequencyDivider(self): + """Query device for divide-by setting + + Returns + ------- + int + frequency divider factor. Possible values are integers from 1 to + 999. + """ + self._divide_by = int(self.send_command(b'RV#')) + return self._divide_by + + def GetMaxDelay(self): + """Query device for maximum possible delay setpoint + + Returns + ------- + int + maximum delay in units of picoseconds + """ + self._max_delay = int(self.send_command(b'RMD#')) + return self._max_delay diff --git a/PYME/Acquire/Hardware/multiview.py b/PYME/Acquire/Hardware/multiview.py new file mode 100644 index 000000000..499424ad0 --- /dev/null +++ b/PYME/Acquire/Hardware/multiview.py @@ -0,0 +1,287 @@ +import imp +from PYME.Acquire.Hardware import Camera +from PYME.Acquire.Hardware.Simulator import fakeCam +import numpy as np +import logging +logger = logging.getLogger(__name__) + +class MultiviewWrapper(object): + def __init__(self, base_camera, multiview_info, default_roi=None): + """ + Used principally for cutting horizontally spaced ROIs out of a vertical band of the sCMOS chip, where there + is dark space between the images and we want to avoid saving and transmitting this dark data. + + This class supports the standard full chip and cropped ROI modes, as well as a new multiview mode. The + multiview mode makes the frames appear to the outside world as though they are just active multiview views + concatenated horizontally. + + The class wraps around an existing camera class, redefining the methods associated + with getting and setting ROIs and image data, other camera methods are passed through to the + base camera class. + + Usage is as follows: + + ``` + #initialise base camera as normal + base_camera = SomeCameraClass(...) + # wrap it + mvcam = MultiviewWrapper(base_camera, multiview_info, default_roi) + ``` + + Parameters + ---------- + base_camera: Camera.Camera (or subclass) instance + the object representing the actual physical camera. + multiview_info : dict + Information about how to crop the image. Can either be a dictionary, or something which behaves like a + dictionary (e.g. a MetaDataHandler). + default_roi: dict, optional + contains the default ROI of the camera. This will usually be automatically read from the base camera + + + Notes + ----- + For now, the 0th multiview ROI should be the upper-left most multiview ROI, in order to properly spoof the + position to match up with the stage. See PYME.IO.MetaDataHandler.get_camera_roi_origin. + """ + self._camera = base_camera # type: Camera.Camera + + if default_roi is None: + + #full field of base camera + default_roi = { + 'xi' : 0, + 'yi' : 0, + 'xf' : self._camera.GetCCDWidth(), + 'yf' : self._camera.GetCCDHeight() + } + + self.multiview_info = multiview_info + self._channel_color = multiview_info['Multiview.ChannelColor'] + + self.n_views = multiview_info['Multiview.NumROIs'] + self.view_origins = [multiview_info['Multiview.ROI%dOrigin' % i] for i in range(self.n_views)] + self.size_x, self.size_y = multiview_info['Multiview.DefaultROISize'] + self.multiview_roi_size_options = multiview_info['Multiview.ROISizeOptions'] + + self.view_centers = [(ox + int(0.5*self.size_x), oy + int(0.5*self.size_y)) for ox, oy in self.view_origins] + + self.multiview_enabled = False + self.active_views = [] + + # set default width and height to return to when multiview is disabled + self._default_chip_width = default_roi['xf'] - default_roi['xi'] + self._default_chip_height = default_roi['yf'] - default_roi['yi'] + self.default_chip_roi = default_roi + self._current_pic_width = self._default_chip_width + self._current_pic_height = self._default_chip_height + + #hack for simulator TODO - move this somewhere more sensible + if isinstance(self._camera, fakeCam.FakeCamera): + vx = self._camera.XVals[1] - self._camera.XVals[0] + self._camera._chan_x_offsets = [vx*x0 for x0, y0 in self.view_origins] + + def ChangeMultiviewROISize(self, x_size, y_size): + """ + Changes the ROI size of the views. Currently they are all the same size + Parameters + ---------- + x_size: int + first dimension size + y_size: int + second dimension size + + Returns + ------- + None + """ + # shift the origins + self.view_origins = [(xc - int(0.5*x_size), yc - int(0.5*y_size)) for xc, yc in self.view_centers] + # store the new sizes + self.size_x, self.size_y = x_size, y_size + + if self.multiview_enabled: + # re-set the slices for frame cropping + self.enable_multiview(self.active_views) + + + def GetPicWidth(self): + """ + This clobbers the inherited self.camera_class GetPicWidth method so that the outside world (FrameWrangler) + only allocates memory/thinks our camera frames are the final concatenated multiview width. + Returns + ------- + pic_width: int + width of the concatenated multiview frame + """ + if self.multiview_enabled: + return self._current_pic_width + else: + return self._camera.GetPicWidth() + + def GetPicHeight(self): + """ + This clobbers the inherited self.camera_class GetPicHeight method so that the outside world (FrameWrangler) + only allocates memory/thinks our camera frames are the final multiview frame height. + Returns + ------- + pic_height: int + height of the multiview frame + """ + if self.multiview_enabled: + return self._current_pic_height + else: + return self._camera.GetPicHeight() + + def set_active_views(self, views): + if len(views) == 0: + self.disable_multiview() + elif sorted(views) == self.active_views: + pass + else: + self.enable_multiview(views) + + + def enable_multiview(self, views): + """ + + Parameters + ---------- + views: list + views to activate. Should be integers which can be used to index self.multiview_info + + Returns + ------- + + Notes + ----- + FrameWrangler must be stopped before this function is called, and "prepared" afterwards before being started + again. This is not special to this function, but rather anytime SetROI gets called. + + """ + views = sorted(list(views)) # tuple(int) isn't iterable, make sure we avoid it + # set the camera FOV to be just large enough so we do most of the cropping where it is already optimized + self.x_origins, self.y_origins = zip(*[self.view_origins[view] for view in views]) + chip_x_min, chip_x_max = min(self.x_origins), max(self.x_origins) + chip_y_min, chip_y_max = min(self.y_origins), max(self.y_origins) + + chip_width = chip_x_max + self.size_x - chip_x_min + chip_height = chip_y_max + self.size_y - chip_y_min + + self.chip_roi = [chip_x_min, chip_y_min, chip_x_min + chip_width, chip_y_min + chip_height] + logger.debug('setting chip ROI') + self._camera.SetROI(*self.chip_roi) + actual = self._camera.GetROI() + try: + assert actual == tuple(self.chip_roi) + except AssertionError: + raise(AssertionError('Error setting camera ROI. Check that ROI is feasible for camera, target: %s, actual: %s' + % (tuple(self.chip_roi), actual))) + + # hold an array for temporarily writing the roughly cropped chip + self.chip_data = np.empty((chip_width, chip_height), dtype='uint16', order='F') + + # precalculate slices for each view + self.view_slices, self.output_slices = [], [] + for x_ind, view in enumerate(views): + ox, oy = self.view_origins[view] + # calculate the offset from the chip origin + oxp, oyp = ox - chip_x_min, oy - chip_y_min + # calculate the slices to pull out of roi on chip + self.view_slices.append(np.s_[oxp:oxp + self.size_x, oyp: oyp + self.size_y]) + # calculate slices to write into out array + self.output_slices.append(np.s_[self.size_x * x_ind:self.size_x * (x_ind + 1), 0:self.size_y]) + + # update our apparent height and widths, concatenating along 'x' or the 0th dim + self._current_pic_width = len(views) * self.size_x + self._current_pic_height = self.size_y + # tell the world what we've accomplished here today + self.multiview_enabled = True + self.active_views = views + + def disable_multiview(self): + """ + Disables multiview mode and returns camera to the default ROI (e.g. full chip) + Returns + ------- + + """ + self.multiview_enabled = False + self.active_views = [] + self._camera.SetROI(self.default_chip_roi['xi'], self.default_chip_roi['yi'], + self.default_chip_roi['xf'], self.default_chip_roi['yf'],) + + + def ExtractColor(self, output_frame, mode): + """ + Override camera get-frame function, but with multiview cropping. + + Parameters + ---------- + output_frame: np.array + array sized for the final multiview frame + mode: int + camera acquisition mode. + + Returns + ------- + + """ + if self.multiview_enabled: + # logger.debug('pulling frame') + # pull data off the roughly cropped frame + self._camera.ExtractColor(self.chip_data, mode) + # extract the multiview frames from the cropped chip into our output + for out_slice, view_slice in zip(self.output_slices, self.view_slices): + output_frame[out_slice] = self.chip_data[view_slice] + + else: + # skip extra cropping, extract the full chip directly into the output frame + self._camera.ExtractColor(output_frame, mode) + + def GenStartMetadata(self, mdh): + """ + Light shim to record multiview metadata, when appropriate + + Parameters + ---------- + mdh : MetaDataHandler + MetaDataHandler object for Camera. + + Returns + ------- + None + """ + self._camera.GenStartMetadata(mdh) + # add in multiview info + if self.multiview_enabled: + mdh.setEntry('Multiview.NumROIs', self.n_views) + mdh.setEntry('Multiview.ROISize', [self.size_x, self.size_y]) + mdh.setEntry('Multiview.ChannelColor', self._channel_color) + mdh.setEntry('Multiview.ActiveViews', self.active_views) + for ind in range(self.n_views): + mdh.setEntry('Multiview.ROI%dOrigin' % ind, self.view_origins[ind]) + + def register_state_handlers(self, state_manager): + """ Allow key multiview settings to be updated easily through + the microscope state handler + + Parameters + ---------- + state_manager : PYME.Acquire.microscope.State + """ + logger.debug('registering multiview camera state handlers') + + state_manager.registerHandler('Multiview.ActiveViews', + lambda : self.active_views, + self.set_active_views, True) + state_manager.registerHandler('Multiview.ROISize', + lambda : [self.size_x, self.size_y], + lambda p : self.ChangeMultiviewROISize(p[0], p[1]), + True) + + def __getattr__(self, key): + """ + Proxy any methods we don't implement here by passing through to the base camera class + """ + return getattr(self._camera, key) diff --git a/PYME/Acquire/Hardware/olympusix81.py b/PYME/Acquire/Hardware/olympusix81.py new file mode 100644 index 000000000..4f3156924 --- /dev/null +++ b/PYME/Acquire/Hardware/olympusix81.py @@ -0,0 +1,59 @@ +""" +Control the halogen lamp (transmitted light) on an Olympus IX81 stand. + +Uses the serial command set described at https://madhadron.com/science/olympus_ix81_commands.html + +TODO - move logic to an IX81 class which also allows control of other stand features and support more of the command set. +TODO - some form of error handling +TODO - consider re-writing the query function to try and match responses to commands (these are not guaranteed to be synchronous) + +""" +import serial +import time +import threading +from PYME.Acquire.Hardware.lasers import Laser + +class OlympusIX81HalogenLamp(Laser): + def __init__(self, name, portname='COM17', turn_on=False, **kwargs): + self.ser_port = serial.Serial(portname, 19200, parity='E', + timeout=2, writeTimeout=2) + self.lock = threading.Lock() + self.name = name + self.powerControlable = False + self.isOn = False + #self.TurnOn() + Laser.__init__(self, name, turn_on, **kwargs) + + def _query(self, command, lines_expected=1): + with self.lock: + self.ser_port.reset_input_buffer() + self.ser_port.write(command) + reply = [self.ser_port.readline() for line in range(lines_expected)] + self.ser_port.reset_input_buffer() + return reply + + def IsOn(self): + return self.isOn + + def TurnOn(self): + # make sure serial is open + try: + self.ser_port.open() + except serial.SerialException: + pass + + self.isOn = True + + # turn on the laser + self._query(b'1LOG IN\r\n', lines_expected=1) + self._query(b'1LMPSW ON\r\n', lines_expected=1) + self._query(b'1LOG OUT\r\n', lines_expected=1) + #self.ser_port.flush() + + def TurnOff(self): + self._query(b'1LOG IN\r\n', lines_expected=1) + self._query(b'1LMPSW OFF\r\n', lines_expected=1) + self._query(b'1LOG OUT\r\n', lines_expected=1) + #self.ser_port.flush() + self.isOn = False + diff --git a/PYME/Acquire/Hardware/pco/pco_cam.py b/PYME/Acquire/Hardware/pco/pco_cam.py index fc55fbb11..52c257092 100644 --- a/PYME/Acquire/Hardware/pco/pco_cam.py +++ b/PYME/Acquire/Hardware/pco/pco_cam.py @@ -51,8 +51,13 @@ def Init(self): self.initalized = True self._roi = None self.SetROI(1, 1, self.GetCCDWidth(), self.GetCCDHeight()) - self._ccd_temp = 0 # Store the sensor temperature + self._ccd_temp = 0 + self._ccd_temp_set_point = 0 + self._electr_temp = 0 self._cycle_time = 0 + self._integ_time = 0 + self._binning_x = 0 + self._binning_y = 0 self.recording = False @property @@ -63,7 +68,7 @@ def SetDescription(self): self.desc = self.cam.sdk.get_camera_description() def ExpReady(self): - return self.GetNumImsBuffered() >= 1 + return self.recording and (self.GetNumImsBuffered() >= 1) def GetName(self): return self.cam.sdk.get_camera_name()['camera name'] @@ -109,10 +114,11 @@ def SetIntegTime(self, time): time = np.clip(time, lb, ub) self.cam.set_exposure_time(time) + d = self.cam.sdk.get_delay_exposure_time() + self._integ_time = d['exposure']*timebase[d['exposure timebase']] def GetIntegTime(self): - d = self.cam.sdk.get_delay_exposure_time() - return d['exposure']*timebase[d['exposure timebase']] + return self._integ_time def GetCycleTime(self): return self._cycle_time @@ -141,9 +147,10 @@ def SetHorizontalBin(self, value): value = np.clip(value, 1, b_max) self.cam.sdk.set_binning(value, self.GetVerticalBin()) + self._binning_x = self.cam.sdk.get_binning()['binning x'] def GetHorizontalBin(self): - return self.cam.sdk.get_binning()['binning x'] + return self._binning_x def SetVerticalBin(self, value): b_max, step_type = self.desc['max. binning vert'], self.desc['binning vert stepping'] @@ -157,9 +164,10 @@ def SetVerticalBin(self, value): value = np.clip(value, 1, b_max) self.cam.sdk.set_binning(self.GetHorizontalBin(), value) + self._binning_y = self.cam.sdk.get_binning()['binning y'] def GetVerticalBin(self): - return self.cam.sdk.get_binning()['binning y'] + return self._binning_y def GetSupportedBinning(self): import itertools @@ -239,14 +247,13 @@ def GetROI(self): return self._roi def GetElectrTemp(self): - # FIXME: should this be 'power temperature'? - return self.cam.sdk.get_temperature()['camera temperature'] + return self._electr_temp def GetCCDTemp(self): - return self.cam.sdk.get_temperature()['sensor temperature'] + return self._ccd_temp def GetCCDTempSetPoint(self): - return self.cam.sdk.get_cooling_setpoint_temperature()['cooling setpoint temperature'] + return self._ccd_temp_set_point def SetCCDTemp(self, temp): lb = self.desc['Min Cool Set DESC'] @@ -254,6 +261,16 @@ def SetCCDTemp(self, temp): temp = np.clip(temp, lb, ub) self.cam.sdk.set_cooling_setpoint_temperature(temp) + self._ccd_temp_set_point = temp + self._get_temps() + + def _get_temps(self): + # NOTE: temperature only gets probed when acquisition starts/stops (which can be fairly + # irregularly - to the point of not being useful). + # TODO - find a way to safely call this while the camera is running + d = self.cam.sdk.get_temperature() + self._ccd_temp = d['sensor temperature'] + self._electr_temp = d['camera temperature'] # FIXME: should this be 'power temperature'? def GetAcquisitionMode(self): return self._mode @@ -265,10 +282,12 @@ def SetAcquisitionMode(self, mode): raise RuntimeError('Mode %d not supported' % mode) def StartExposure(self): - # self.StopAq() - self.n_read = 0 + self.StopAq() + self._get_temps() + d = self.cam.sdk.get_delay_exposure_time() - self._cycle_time = d['exposure']*timebase[d['exposure timebase']] \ + self._integ_time = d['exposure']*timebase[d['exposure timebase']] + self._cycle_time = self._integ_time \ + d['delay']*timebase[d['delay timebase']] if self._mode == self.MODE_SINGLE_SHOT: self.cam.record(number_of_images=1, mode='sequence') @@ -278,13 +297,27 @@ def StartExposure(self): self.cam.record(number_of_images=self.buffer_size, mode='ring buffer') self.recording = True - eventLog.logEvent('StartAq', '') + self._log_exposure_start() return 0 def StopAq(self): self.recording = False - self.cam.stop() + if self.cam.rec.recorder_handle.value is not None: + # NOTE: probably don't need this if statement thanks to the self.recording flag, + # but better to be safe + self.cam.rec.stop_record() + self.cam.rec.delete() + + self._integ_time = 0 + self._cycle_time = 0 + self.buffer_size = 0 + self.n_read = 0 + + # read the temp in StopAq too, as this has the side effect of making sure we have + # a reasonably correct temperature in acquisition metadata (as we + # stop acquisition, record metadata, and then restart) + self._get_temps() def GetNumImsBuffered(self): if self.recording: diff --git a/PYME/Acquire/Hardware/pco/pco_edge_42_lt.py b/PYME/Acquire/Hardware/pco/pco_edge_42_lt.py index c5285bda1..661a6e3da 100644 --- a/PYME/Acquire/Hardware/pco/pco_edge_42_lt.py +++ b/PYME/Acquire/Hardware/pco/pco_edge_42_lt.py @@ -6,7 +6,8 @@ @author: zacsimile """ -from PYME.Acquire.Hardware.pco import pco_cam +# from PYME.Acquire.Hardware.pco import pco_cam +from PYME.Acquire.Hardware.pco import pco_sdk_cam noiseProperties = { '61003940' : { @@ -19,10 +20,15 @@ }, } -class PcoEdge42LT(pco_cam.PcoCam): +class PcoEdge42LT(pco_sdk_cam.PcoSdkCam): def __init__(self, camNum, debuglevel='off'): - pco_cam.PcoCam.__init__(self, camNum, debuglevel) + pco_sdk_cam.PcoSdkCam.__init__(self, camNum, debuglevel) def Init(self): - pco_cam.PcoCam.Init(self) - self.noiseProps = noiseProperties[self.GetSerialNumber()] \ No newline at end of file + pco_sdk_cam.PcoSdkCam.Init(self) + self.noiseProps = noiseProperties[self.GetSerialNumber()] + + def GenStartMetadata(self, mdh): + pco_sdk_cam.PcoSdkCam.GenStartMetadata(self, mdh) + if self.active: + mdh.setEntry('Camera.ADOffset', self.noiseProps['ADOffset']) diff --git a/PYME/Acquire/Hardware/pco/pco_sdk.py b/PYME/Acquire/Hardware/pco/pco_sdk.py new file mode 100644 index 000000000..ef88b7267 --- /dev/null +++ b/PYME/Acquire/Hardware/pco/pco_sdk.py @@ -0,0 +1,1645 @@ +# -*- coding: utf-8 -*- + +""" +Created on Fri May 14 2021 + +@author: zacsimile + +To run, install the pco. sdk, available at +https://www.pco.de/software/development-tools/pcosdk/, as admin. + +The goal of this file is to provide Python access to pco.sdk as closely to how it +is implemented in pco.sdk as possible. There's no mapping of flags to readable +strings such as "on"/"off". The idea is to have raw access to the DLL. For a +friendlier--but less comprehensive--version, see sdk.py in https://pypi.org/project/pco/. + +For more information, see the pco.sdk manual, +https://www.pco.de/fileadmin/fileadmin/user_upload/pco-manuals/pco.sdk_manual.pdf + +NOTE: I cheated a little off https://pypi.org/project/pco/. +""" + +import ctypes +import ctypes.wintypes +import platform +import os +import sys + +os_desc = platform.system() +if os_desc != 'Windows': + raise Exception("Operating system is not supported.") + +dll = 'SC2_Cam.dll' +dll_path = "C:\\Program Files (x86)\\PCO Digital Camera Toolbox\\pco.sdk\\bin64\\" + dll +try: + sc2_cam = ctypes.windll.LoadLibrary(dll_path) +except: + raise + +# --------------------------------------------------------------------- +# Structures and types +# --------------------------------------------------------------------- +class PCO_Description(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("wSize", ctypes.wintypes.WORD), + ("wSensorTypeDESC", ctypes.wintypes.WORD), + ("wSensorSubTypeDESC", ctypes.wintypes.WORD), + ("wMaxHorzResStdDESC", ctypes.wintypes.WORD), + ("wMaxVertResStdDESC", ctypes.wintypes.WORD), + ("wMaxHorzResExtDESC", ctypes.wintypes.WORD), + ("wMaxVertResExtDESC", ctypes.wintypes.WORD), + ("wDynResDESC", ctypes.wintypes.WORD), + ("wMaxBinHorzDESC", ctypes.wintypes.WORD), + ("wBinHorzSteppingDESC", ctypes.wintypes.WORD), + ("wMaxBinVertDESC", ctypes.wintypes.WORD), + ("wBinVertSteppingDESC", ctypes.wintypes.WORD), + ("wRoiHorStepsDESC", ctypes.wintypes.WORD), + ("wRoiVertStepsDESC", ctypes.wintypes.WORD), + ("wNumADCsDESC", ctypes.wintypes.WORD), + ("wMinSizeHorzDESC", ctypes.wintypes.WORD), + ("dwPixelRateDESC", ctypes.wintypes.DWORD * 4), + ("ZzdwDummypr", ctypes.wintypes.DWORD * 20), + ("wConvFactDESC", ctypes.wintypes.WORD * 4), + ("sCoolingSetpoints", ctypes.c_short * 10), + ("ZZdwDummycv", ctypes.wintypes.WORD * 8), + ("wSoftRoiHorStepsDESC", ctypes.wintypes.WORD), + ("wSoftRoiVertStepsDESC", ctypes.wintypes.WORD), + ("wIRDESC", ctypes.wintypes.WORD), + ("wMinSizeVertDESC", ctypes.wintypes.WORD), + ("dwMinDelayDESC", ctypes.wintypes.DWORD), + ("dwMaxDelayDESC", ctypes.wintypes.DWORD), + ("dwMinDelayStepDESC", ctypes.wintypes.DWORD), + ("dwMinExposDESC", ctypes.wintypes.DWORD), + ("dwMaxExposDESC", ctypes.wintypes.DWORD), + ("dwMinExposStepDESC", ctypes.wintypes.DWORD), + ("dwMinDelayIRDESC", ctypes.wintypes.DWORD), + ("dwMaxDelayIRDESC", ctypes.wintypes.DWORD), + ("dwMinExposIRDESC", ctypes.wintypes.DWORD), + ("dwMaxExposIRDESC", ctypes.wintypes.DWORD), + ("wTimeTableDESC", ctypes.wintypes.WORD), + ("wDoubleImageDESC", ctypes.wintypes.WORD), + ("sMinCoolSetDESC", ctypes.c_short), + ("sMaxCoolSetDESC", ctypes.c_short), + ("sDefaultCoolSetDESC", ctypes.c_short), + ("wPowerDownModeDESC", ctypes.wintypes.WORD), + ("wOffsetRegulationDESC", ctypes.wintypes.WORD), + ("wColorPatternDESC", ctypes.wintypes.WORD), + ("wPatternTypeDESC", ctypes.wintypes.WORD), + ("wDummy1", ctypes.wintypes.WORD), + ("wDummy2", ctypes.wintypes.WORD), + ("wNumCoolingSetpoints", ctypes.wintypes.WORD), + ("dwGeneralCapsDESC1", ctypes.wintypes.DWORD), + ("dwGeneralCapsDESC2", ctypes.wintypes.DWORD), + ("dwExtSyncFrequency", ctypes.wintypes.DWORD * 4), + ("dwGeneralCapsDESC3", ctypes.wintypes.DWORD), + ("dwGeneralCapsDESC4", ctypes.wintypes.DWORD), + ("ZzdwDummy", 40*ctypes.wintypes.DWORD)] +class PCO_SC2_Hardware_DESC(ctypes.Structure): + # _pack_ = 1 + _fields_ = [ + ("szName", ctypes.c_char * 16), + ("wBatchNo", ctypes.wintypes.WORD), + ("wRevision", ctypes.wintypes.WORD), + ("wVariant", ctypes.wintypes.WORD), + ("ZZwDummy", ctypes.wintypes.WORD * 20)] + +class PCO_SC2_Firmware_DESC(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("szName", ctypes.c_char * 16), + ("bMinorRev", ctypes.c_byte), + ("bMajorRev", ctypes.c_byte), + ("wVariant", ctypes.wintypes.WORD), + ("ZZwDummy", ctypes.wintypes.WORD * 22)] + +class PCO_HW_Vers(ctypes.Structure): + #_pack_ = 1 + _fields_ = [ + ("wBoardNum", ctypes.wintypes.WORD), + ("Board", PCO_SC2_Hardware_DESC * 10)] + +class PCO_FW_Vers(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("wDeviceNum", ctypes.wintypes.WORD), + ("Device", PCO_SC2_Firmware_DESC * 10)] + +class PCO_CameraType(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("wSize", ctypes.wintypes.WORD), + ("wCamType", ctypes.wintypes.WORD), + ("wCamSubType", ctypes.wintypes.WORD), + ("ZZwAlignDummy1", ctypes.wintypes.WORD), + ("dwSerialNumber", ctypes.wintypes.DWORD), + ("dwHWVersion", ctypes.wintypes.DWORD), + ("dwFWVersion", ctypes.wintypes.DWORD), + ("wInterfaceType", ctypes.wintypes.WORD), + ("strHardwareVersion", PCO_HW_Vers), + ("strFirmwareVersion", PCO_FW_Vers), + ("ZZwDummy", ctypes.wintypes.PWORD)] # ("ZZwDummy", ctypes.wintypes.WORD * 39) + +class PCO_ImageTiming(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("wSize", ctypes.wintypes.WORD), + ("wDummy", ctypes.wintypes.WORD), + ("FrameTime_ns", ctypes.wintypes.DWORD), + ("FrameTime_s", ctypes.wintypes.DWORD), + ("ExposureTime_ns", ctypes.wintypes.DWORD), + ("ExposureTime_s", ctypes.wintypes.DWORD), + ("TriggerSystemDelay_ns", ctypes.wintypes.DWORD), + ("TriggerSystemJitter_ns", ctypes.wintypes.DWORD), + ("TriggerDelay_ns", ctypes.wintypes.DWORD), + ("TriggerDelay_s", ctypes.wintypes.DWORD), + ("ZZdwDummy", ctypes.wintypes.DWORD)] + +class PCO_Recording(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("wSize", ctypes.wintypes.WORD), + ("wStorageMode", ctypes.wintypes.WORD), + ("wRecSubmode", ctypes.wintypes.WORD), + ("wRecState", ctypes.wintypes.WORD), + ("wAcquMode", ctypes.wintypes.WORD), + ("wAcquEnableStatus", ctypes.wintypes.WORD), + ("ucDay", ctypes.c_byte), + ("ucMonth", ctypes.c_byte), + ("wYear", ctypes.wintypes.WORD), + ("wHour", ctypes.wintypes.WORD), + ("ucMin", ctypes.c_byte), + ("ucSec", ctypes.c_byte), + ("wTimeStampMode", ctypes.wintypes.WORD), + ("wRecordStopEventMode", ctypes.wintypes.WORD), + ("dwRecordStopDelayImages", ctypes.wintypes.DWORD), + ("wMetaDataMode", ctypes.wintypes.WORD), + ("wMetaDataSize", ctypes.wintypes.WORD), + ("wMetaDataVersion", ctypes.wintypes.WORD), + ("ZZwDummy1", ctypes.wintypes.WORD), + ("dwAcquModeExNumberImages", ctypes.wintypes.DWORD), + ("dwAcquModeExReserved", ctypes.wintypes.DWORD * 4), + ("ZZwDummy", ctypes.wintypes.WORD * 22)] + +class PCO_Metadata_Struct(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("wSize", ctypes.wintypes.WORD), + ("wVersion", ctypes.wintypes.WORD), + ("bIMAGE_COUNTER_BCD", ctypes.c_byte * 4), + ("bIMAGE_TIME_US_BCD", ctypes.c_byte * 3), + ("bIMAGE_TIME_SEC_BCD", ctypes.c_byte), + ("bIMAGE_TIME_MIN_BCD", ctypes.c_byte), + ("bIMAGE_TIME_HOUR_BCD", ctypes.c_byte), + ("bIMAGE_TIME_DAY_BCD", ctypes.c_byte), + ("bIMAGE_TIME_MON_BCD", ctypes.c_byte), + ("bIMAGE_TIME_YEAR_BCD", ctypes.c_byte), + ("bIMAGE_TIME_STATUS", ctypes.c_byte), + ("wEXPOSURE_TIME_BASE", ctypes.wintypes.WORD), + ("dwEXPOSURE_TIME", ctypes.wintypes.DWORD), + ("dwFRAMERATE_MILLIHZ", ctypes.wintypes.DWORD), + ("sSENSOR_TEMPERATURE", ctypes.c_short), + ("wIMAGE_SIZE_X", ctypes.wintypes.WORD), + ("wIMAGE_SIZE_Y", ctypes.wintypes.WORD), + ("bBINNING_X", ctypes.c_byte), + ("bBINNING_Y", ctypes.c_byte), + ("dwSENSOR_READOUT_FREQUENCY", ctypes.wintypes.DWORD), + ("wSENSOR_CONV_FACTOR", ctypes.wintypes.WORD), + ("dwCAMERA_SERIAL_NO", ctypes.wintypes.DWORD), + ("wCAMERA_TYPE", ctypes.wintypes.WORD), + ("bBIT_RESOLUTION", ctypes.c_byte), + ("bSYNC_STATUS", ctypes.c_byte), + ("wDARK_OFFSET", ctypes.wintypes.WORD), + ("bTRIGGER_MODE", ctypes.c_byte), + ("bDOUBLE_IMAGE_MODE", ctypes.c_byte), + ("bCAMERA_SYNC_MODE", ctypes.c_byte), + ("bIMAGE_TYPE", ctypes.c_byte), + ("wCOLOR_PATTERN", ctypes.wintypes.WORD)] + +PCO_NOERROR = 0x00000000 +class PCO_Buflist(ctypes.Structure): + # _pack_ = 1 + _fields_ = [("SBufNr", ctypes.c_short), + ("reserved", ctypes.wintypes.WORD), + ("dwStatusDll", ctypes.wintypes.DWORD), # PCO_BUFFER_ALLOCATED, PCO_BUFFER_CREATED, PCO_BUFFER_EXTERNAL, PCO_BUFFER_SET + ("dwStatusDrv", ctypes.wintypes.DWORD)] # PCO_NOERROR or see pco.sdk for error codes + +PCO_CAMERA_TYPES = { + 0x1300 : 'pco.edge 5.5 CL', + 0x1302 : 'pco.edge 4.2 CL', + 0x1310 : 'pco.edge GL', + 0x1320 : 'pco.edge USB3', + 0x1340 : 'pco.edge CLHS', + 0x1304 : 'pco.edge MT', + 0x1000 : 'pco.dimax', + 0x1010 : 'pco.dimax_TV', + 0x1020 : 'pco.dimax CS', + 0x1400 : 'pco.flim', + 0x1500 : 'pco.panda', + 0x0800 : 'pco.pixelfly usb', + 0x0100 : 'pco.1200HS', + 0x0200 : 'pco.1300', + 0x0220 : 'pco.1600', + 0x0240 : 'pco.2000', + 0x0260 : 'pco.4000', + 0x0830 : 'pco.1400' +} + +PCO_INTERFACE_TYPES = { + 0x0001 : 'FireWire', + 0x0002 : 'Camera Link', + 0x0003 : 'USB 2.0', + 0x0004 : 'GigE', + 0x0005 : 'Serial Interface', + 0x0006 : 'USB 3.0', + 0x0007 : 'CLHS' +} + +PCO_TRIGGER_MODES = { + 0x0000 : 'auto sequence', + 0x0001 : 'software trigger', + 0x0002 : 'external exposure start & software trigger', + 0x0003 : 'external exposure control', + 0x0004 : 'external synchronized', + 0x0005 : 'fast external exposure control', + 0x0006 : 'external CDS control', + 0x0007 : 'slow external exposure control', + 0x0102 : 'external synchronized HDSDI' +} + +# --------------------------------------------------------------------- +# Error handling +# --------------------------------------------------------------------- +class PcoSdkException(Exception): + def __init__(self, message): + Exception.__init__(self, message) + +# TODO: Instead of wrapping everything with check_status, move to ctypes +# errcheck https://docs.python.org/3/library/ctypes.html#ctypes._FuncPtr.errcheck +def check_status(err, function_name=None): + if err: + if function_name is None: + function_name = sys._getframe(1).f_code.co_name # who called me? + err_desc = get_error_text(err) + raise PcoSdkException(f"Error during {function_name}: {err_desc}") + +# --------------------------------------------------------------------- +# 2.1.1 PCO_OpenCamera +# --------------------------------------------------------------------- +sc2_cam.PCO_OpenCamera.argtypes = [ctypes.POINTER(ctypes.wintypes.HANDLE), + ctypes.wintypes.WORD] +def open_camera(): + """ + Open a pco. camera, return the handle to that camera. + This uses a scan process, so call this multiple times + to open multiple cameras. Call get_camera_type() to + figure out which camera was grabbed. + + To open a specific camera, use open_camera_ex(). + + Returns + ------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + """ + handle = ctypes.c_void_p(0) + cam_num = ctypes.wintypes.WORD() # unused + check_status(sc2_cam.PCO_OpenCamera(handle, cam_num)) + return handle +# --------------------------------------------------------------------- +# 2.1.2 PCO_OpenCameraEx +# --------------------------------------------------------------------- +def open_camera_ex(**kwargs): + """ + Choose which camera to open, rather than using a scan mode. + """ + raise NotImplementedError("Not implemented, but shouldn't be too hard. Check pco.sdk for details.") + +# --------------------------------------------------------------------- +# 2.1.3 PCO_CloseCamera +# --------------------------------------------------------------------- +sc2_cam.PCO_CloseCamera.argtypes = [ctypes.wintypes.HANDLE] +def close_camera(handle): + """ + Close a pco. camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + """ + check_status(sc2_cam.PCO_CloseCamera(handle)) + +# --------------------------------------------------------------------- +# 2.1.4 PCO_ResetLib +# --------------------------------------------------------------------- +def reset_lib(): + """ + Reset the pco.sdk to its initial state. Can only be called when + no cameras are open. + """ + check_status(sc2_cam.PCO_ResetLib()) + +# --------------------------------------------------------------------- +# 2.2.1 PCO_GetCameraDescription +# --------------------------------------------------------------------- +sc2_cam.PCO_GetCameraDescription.argtypes = [ctypes.wintypes.HANDLE, + ctypes.POINTER(PCO_Description)] +def get_camera_description(handle): + """ + Get camera description. Includes properties such as image size, pixel rate, etctypes., + but not camera names, serial numbers, etctypes. For the latter, call get_camera_type(). + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + PCO_Description + Struct of camera properties. + """ + desc = PCO_Description() + desc.wSize = ctypes.sizeof(PCO_Description) + check_status(sc2_cam.PCO_GetCameraDescription(handle, desc)) + return desc + +# --------------------------------------------------------------------- +# 2.3.2 PCO_GetCameraType +# --------------------------------------------------------------------- +sc2_cam.PCO_GetCameraType.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(PCO_CameraType)] +def get_camera_type(handle): + """ + Get camera name, serial number, etc. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + PCO_CameraType + Struct containing hardware/firmware version, serial number, etc. + """ + camera_type = PCO_CameraType() + camera_type.wSize = ctypes.sizeof(PCO_CameraType) + check_status(sc2_cam.PCO_GetCameraType(handle, camera_type)) + return camera_type + +# --------------------------------------------------------------------- +# 2.3.3 PCO_GetCameraHealthStatus +# --------------------------------------------------------------------- +sc2_cam.PCO_GetCameraHealthStatus.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PDWORD, + ctypes.wintypes.PDWORD, + ctypes.wintypes.PDWORD] +def get_camera_health_status(handle): + """ + Get information about how well the camera is doing. See pco.sdk + manual for bit definitions. Generally, things are OK as long as + all the bits are 0, and bad otherwise. Except a status bit of 1 + is OK. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + warn : int + Warning bit + err : int + Error bit + status : int + Status bit + """ + warn = ctypes.wintypes.DWORD() + err = ctypes.wintypes.DWORD() + status = ctypes.wintypes.DWORD() + check_status(sc2_cam.PCO_GetCameraHealthStatus(handle, warn, err, status)) + return warn.value, err.value, status.value + +# --------------------------------------------------------------------- +# 2.3.4 PCO_GetTemperature +# --------------------------------------------------------------------- +sc2_cam.PCO_GetTemperature.argtypes = [ctypes.wintypes.HANDLE, + ctypes.POINTER(ctypes.c_short), + ctypes.POINTER(ctypes.c_short), + ctypes.POINTER(ctypes.c_short)] +def get_temperature(handle): + """ + Get camera temperatures. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + ccd_temp : int + Image sensor temp in 1/10 degree i.e. 100 = 10.0 C + cam_temp : int + Internal camera temp in C + pow_temp : int + Temp of additional devices (e.g. power supply) + """ + ccd_temp = ctypes.c_short() + cam_temp = ctypes.c_short() + pow_temp = ctypes.c_short() + check_status(sc2_cam.PCO_GetTemperature(handle, ccd_temp, cam_temp, pow_temp)) + return (ccd_temp.value/10.0), cam_temp.value, pow_temp.value + + +# --------------------------------------------------------------------- +# 2.3.6 PCO_GetCameraName +# --------------------------------------------------------------------- +sc2_cam.PCO_GetCameraName.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_char_p, + ctypes.wintypes.WORD] +def get_camera_name(handle): + """ + Get the camera name + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + string + Camera name. + """ + c_buf_len = 40 + c_buf = ctypes.create_string_buffer(c_buf_len) + + check_status(sc2_cam.PCO_GetCameraName(handle, c_buf, ctypes.wintypes.WORD(c_buf_len))) + + return str(c_buf.value.decode('ascii')) + +# --------------------------------------------------------------------- +# 2.4.1 PCO_ArmCamera +# --------------------------------------------------------------------- +sc2_cam.PCO_ArmCamera.argtypes = [ctypes.wintypes.HANDLE] +def arm_camera(handle): + """ + Prepare the camera for recording. Must be called after the change of + any camera parameter except for delay and exposure time. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + """ + check_status(sc2_cam.PCO_ArmCamera(handle)) + +# --------------------------------------------------------------------- +# 2.4.3 PCO_SetImageParameters +# --------------------------------------------------------------------- +sc2_cam.PCO_SetImageParameters.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD, + ctypes.wintypes.DWORD, + ctypes.c_void_p, + ctypes.c_int] +PCO_IMAGEPARAMETERS_READ_WHILE_RECORDING = 0x00000001 +PCO_IMAGEPARAMETERS_READ_FROM_SEGMENTS = 0x00000002 +def set_image_parameters(handle, lx, ly, image_flag): + """ + Set image parameters for internal allocated resources before image transfer. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + lx : int + Image width + ly : int + Image height + image_flag : int + PCO_IMAGEPARAMETERS_READ_WHILE_RECORDING or PCO_IMAGEPARAMETERS_READ_FROM_SEGMENTS + """ + param = ctypes.c_void_p(0) + ilen = ctypes.c_int(0) + check_status(sc2_cam.PCO_SetImageParameters(handle, ctypes.wintypes.WORD(lx), + ctypes.wintypes.WORD(ly), + ctypes.wintypes.DWORD(image_flag), + param, ilen)) + +# --------------------------------------------------------------------- +# 2.4.4 PCO_ResetSettingsToDefault +# --------------------------------------------------------------------- +sc2_cam.PCO_ResetSettingsToDefault.argtypes = [ctypes.wintypes.HANDLE] +def reset_settings_to_default(handle): + """ + Reset the camera settings to default. Executed by default during + camera power-up. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + """ + check_status(sc2_cam.PCO_ResetSettingsToDefault(handle)) + + +# --------------------------------------------------------------------- +# 2.4.10 PCO_GetFanControlParameters +# --------------------------------------------------------------------- +sc2_cam.PCO_GetFanControlParameters.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.WORD] +PCO_FAN_CONTROL_MODE_AUTO = 0x0000 +PCO_FAN_CONTROL_MODE_USER = 0x0001 +def get_fan_control_parameters(handle): + """ + Get fan speed + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + mode : int + FAN_CONTROL_MODE_AUTO or FAN_CONTROL_MODE_USER + speed : int + Fan speed in range (slowest) 0..100 (fastest) + """ + mode = ctypes.wintypes.WORD() + speed = ctypes.wintypes.WORD() + reserved = ctypes.wintypes.WORD() + num_reserved = ctypes.wintypes.WORD(0) + check_status(sc2_cam.PCO_GetFanControlParameters(handle, mode, speed, + reserved, num_reserved)) + return mode.value, speed.value + +# --------------------------------------------------------------------- +# 2.4.11 PCO_SetFanControlParameters +# --------------------------------------------------------------------- +sc2_cam.PCO_SetFanControlParameters.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD] +def set_fan_control_parameters(handle, mode, speed): + """ + Set image parameters for internal allocated resources before image transfer. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + mode : int + FAN_CONTROL_MODE_AUTO or FAN_CONTROL_MODE_USER + speed : int + Fan speed in range (slowest) 0..100 (fastest) + """ + reserved = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_SetFanControlParameters(handle, ctypes.wintypes.WORD(mode), + ctypes.wintypes.WORD(speed), + reserved)) + +# --------------------------------------------------------------------- +# 2.5.3 PCO_GetSizes +# --------------------------------------------------------------------- +sc2_cam.PCO_GetSizes.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD] +def get_sizes(handle): + """ + Get the current image size of the camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + lx : int + Image width + ly : int + Image height + lx_max : int + Max image width + ly_max : int + Max image height + """ + lx = ctypes.wintypes.WORD() + ly = ctypes.wintypes.WORD() + lx_max = ctypes.wintypes.WORD() + ly_max = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetSizes(handle, lx, ly, lx_max, ly_max)) + return lx.value, ly.value, lx_max.value, ly_max.value + +# --------------------------------------------------------------------- +# 2.5.6 PCO_GetROI +# --------------------------------------------------------------------- +sc2_cam.PCO_GetROI.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD] +def get_roi(handle): + """ + Get the current region of interest as (horiz. start coordinate, vert. + start coordinate, horiz. end coordinate, vert. end coordinate). + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + x0 : int + Horizontal (column) start coordinate + y0 : int + Vertical (row) start coordinate + x1 : int + Horizontal end coordinate + y1 : int + Vertical end coordinate + """ + x0 = ctypes.wintypes.WORD() + x1 = ctypes.wintypes.WORD() + y0 = ctypes.wintypes.WORD() + y1 = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetROI(handle, x0, y0, x1, y1)) + return x0.value, x1.value, y0.value, y1.value + +# --------------------------------------------------------------------- +# 2.5.7 PCO_SetROI +# --------------------------------------------------------------------- +sc2_cam.PCO_SetROI.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD] +def set_roi(handle, x0, y0, x1, y1): + """ + Set the current region of interest as (horiz. start coordinate, vert. + start coordinate, horiz. end coordinate, vert. end coordinate). + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + x0 : int + Horizontal (column) start coordinate + y0 : int + Vertical (row) start coordinate + x1 : int + Horizontal end coordinate + y1 : int + Vertical end coordinate + """ + check_status(sc2_cam.PCO_SetROI(handle, ctypes.wintypes.WORD(x0), + ctypes.wintypes.WORD(y0), + ctypes.wintypes.WORD(x1), + ctypes.wintypes.WORD(y1))) + +# --------------------------------------------------------------------- +# 2.5.8 PCO_GetBinning +# --------------------------------------------------------------------- +sc2_cam.PCO_GetBinning.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD] +def get_binning(handle): + """ + Get horizontal and vertical binning on the camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + bin_horiz : int + Horizontal (column) binning in pixels + bin_vert : int + Vertical (row) binning in pixels + """ + bin_horiz = ctypes.wintypes.WORD() + bin_vert = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetBinning(handle, bin_horiz, bin_vert)) + return bin_horiz.value, bin_vert.value + +# --------------------------------------------------------------------- +# 2.5.9 PCO_SetBinning +# --------------------------------------------------------------------- +sc2_cam.PCO_SetBinning.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD] +def set_binning(handle, bin_horiz, bin_vert): + """ + Set the horizonal and vertical binning on the camera. Possible + values can be calculate from the binning parameters returned in + get_camera_description(). set_roi() must be called after set_binning() + and before arm_camera(). + + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + bin_horiz : int + Horizontal (column) binning in pixels + bin_vert : int + Vertical (row) binning in pixels + """ + check_status(sc2_cam.PCO_SetBinning(handle, ctypes.wintypes.WORD(bin_horiz), + ctypes.wintypes.WORD(bin_vert))) + +# --------------------------------------------------------------------- +# 2.5.10 PCO_GetPixelRate +# --------------------------------------------------------------------- +sc2_cam.PCO_GetPixelRate.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.PDWORD] +def get_pixel_rate(handle): + """ + Get the current pixel rate (sensor readout speed) of the camera in Hz. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + pixel_rate : int + Pixel rate of the camera in Hz + """ + pixel_rate = ctypes.wintypes.DWORD() + check_status(sc2_cam.GetPixelRate(handle, pixel_rate)) + return pixel_rate.value + +# --------------------------------------------------------------------- +# 2.5.10 PCO_SetPixelRate +# --------------------------------------------------------------------- +sc2_cam.PCO_SetPixelRate.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.DWORD] +def set_pixel_rate(handle, pixel_rate): + """ + Set the current pixel rate (sensor readout speed) of the camera in Hz. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + pixel_rate : int + Pixel rate of the camera in Hz + """ + check_status(sc2_cam.SetPixelRate(handle, ctypes.wintypes.DWORD(pixel_rate))) + +# --------------------------------------------------------------------- +# 2.5.20 PCO_GetCoolingSetpointTemperature +# --------------------------------------------------------------------- +sc2_cam.PCO_GetCoolingSetpointTemperature.argtypes = [ctypes.wintypes.HANDLE, + ctypes.POINTER(ctypes.c_short)] +def get_cooling_setpoint_temperature(handle): + """ + Get temperature set point for image sensor. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + temp : int + Setpoint in degrees C + """ + temp = ctypes.c_short() + check_status(sc2_cam.PCO_GetCoolingSetpointTemperature(handle,temp)) + return temp.value + +# --------------------------------------------------------------------- +# 2.5.21 PCO_SetCoolingSetpointTemperature +# --------------------------------------------------------------------- +sc2_cam.PCO_SetCoolingSetpointTemperature.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_short] +def set_cooling_setpoint_temperature(handle, temp): + """ + Set temperature set point for image sensor. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + temp : int + Setpoint in degrees C. Must be in between sMinCoolSetDESC and + sMaxCoolSetDESC (see output of get_camera_description()) + """ + check_status(sc2_cam.PCO_SetCoolingSetpointTemperature(handle, ctypes.c_short(temp))) +# --------------------------------------------------------------------- +# 2.6.4 PCO_GetDelayExposureTime +# --------------------------------------------------------------------- +sc2_cam.PCO_GetDelayExposureTime.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PDWORD, + ctypes.wintypes.PDWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD] +PCO_TIMEBASE_NS = 0x0000 # nanoseconds +PCO_TIMEBASE_US = 0x0001 # microseconds +PCO_TIMEBASE_MS = 0x0002 # milliseconds +def get_delay_exposure_time(handle): + """ + Get delay and exposure times for camera sensor. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + delay : unsigned long int + Delay time in units of timebase_delay + exposure : unsigned long int + Exposure time in units of timebase_exposure + timebase_delay : unsigned short int + Unit for delay time. One of PCO_TIMEBASE_NS, PCO_TIMEBASE_US, PCO_TIMEBASE_MS. + timebase_exposure : unsigned short int + Unit for exposure time. One of PCO_TIMEBASE_NS, PCO_TIMEBASE_US, PCO_TIMEBASE_MS. + """ + delay = ctypes.wintypes.DWORD() + exposure = ctypes.wintypes.DWORD() + timebase_delay = ctypes.wintypes.WORD() + timebase_exposure = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetDelayExposureTime(handle, delay, exposure, + timebase_delay, timebase_exposure)) + return delay.value, exposure.value, timebase_delay.value, timebase_exposure.value + +# --------------------------------------------------------------------- +# 2.6.5 PCO_SetDelayExposureTime +# --------------------------------------------------------------------- +sc2_cam.PCO_SetDelayExposureTime.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD] +def set_delay_exposure_time(handle, delay, exposure, timebase_delay, timebase_exposure): + """ + Set delay and exposure times for camera sensor. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + delay : int + Delay time in units of timebase_delay. In range + dwMinDelayDESC .. dwMinDelayStepDESC .. dwMaxDelayDESC + (see output of get_camera_description()). + exposure : int + Exposure time in units of timebase_exposure. In range + dwMinExposDESC .. dwMinExposStepDESC .. dwMaxExposDESC + (see output of get_camera_description()). + timebase_delay : int + Unit for delay time. One of PCO_TIMEBASE_NS, PCO_TIMEBASE_US, PCO_TIMEBASE_MS. + timebase_exposure : int + Unit for exposure time. One of PCO_TIMEBASE_NS, PCO_TIMEBASE_US, PCO_TIMEBASE_MS. + """ + check_status(sc2_cam.PCO_SetDelayExposureTime(handle, ctypes.wintypes.DWORD(delay), + ctypes.wintypes.DWORD(exposure), + ctypes.wintypes.WORD(timebase_delay), + ctypes.wintypes.WORD(timebase_exposure))) + +# --------------------------------------------------------------------- +# 2.6.12 PCO_GetTriggerMode +# --------------------------------------------------------------------- +sc2_cam.PCO_GetTriggerMode.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD] +def get_trigger_mode(handle, mode): + """ + Get the camera trigger mode. See pco.sdk for details. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + mode : unsigned short int + Trigger mode. One of PCO_TRIGGER_MODES. + """ + mode = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetTriggerMode(handle, mode)) + + return mode.value +# --------------------------------------------------------------------- +# 2.6.13 PCO_SetTriggerMode +# --------------------------------------------------------------------- +sc2_cam.PCO_SetTriggerMode.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD] +def set_trigger_mode(handle, mode): + """ + Set the camera trigger mode. See pco.sdk for details. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + mode : int + Trigger mode. One of PCO_TRIGGER_MODES. + """ + check_status(sc2_cam.PCO_SetTriggerMode(handle, ctypes.wintypes.WORD(mode))) + +# --------------------------------------------------------------------- +# 2.6.14 PCO_ForceTrigger +# --------------------------------------------------------------------- +sc2_cam.PCO_ForceTrigger.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD] +TRIGGER_FAIL = 0x0000 +TRIGGER_SUCCESS = 0x0001 # NOTE: This is counter to every other function + # where a 0 indicates success +def force_trigger(handle): + """ + Trigger image acquisition in software triggered mode. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + triggered : unsigned short int + Sucess of trigger call, one of TRIGGER_FAIL (0), TRIGGER_SUCCESS (1) + """ + triggered = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_ForceTrigger(handle, triggered)) + return triggered.value + +# --------------------------------------------------------------------- +# 2.6.15 PCO_GetCameraBusyStatus +# --------------------------------------------------------------------- +sc2_cam.PCO_GetCameraBusyStatus.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD] +PCO_CAMERA_NOT_BUSY = 0x0000 +PCO_CAMERA_BUSY = 0x0001 +def get_camera_busy_status(handle): + """ + Return the busy status of the camera. Ideally checked before + a force_trigger() call. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + state : unsigned short int + Camera busy status, one of PCO_CAMERA_NOT_BUSY (0), PCO_CAMERA_BUSY (1) + """ + state = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetCameraBusyStatus(handle, state)) + return state.value + +# --------------------------------------------------------------------- +# 2.6.26 PCO_GetImageTiming +# --------------------------------------------------------------------- +sc2_cam.PCO_GetImageTiming.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(PCO_ImageTiming)] +def get_image_timing(handle): + """ + Get image timing with nanosecond resolution, plus additional trigger + system information. Necessary for accurate timing information on pco.edge. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + image_timing : PCO_ImageTiming + Struct of precise image timing and trigger information. + """ + image_timing = PCO_ImageTiming() + check_status(sc2_cam.PCO_GetImageTiming(handle, image_timing)) + return image_timing + +# --------------------------------------------------------------------- +# 2.7.3 PCO_GetRecordingState +# --------------------------------------------------------------------- +sc2_cam.PCO_GetRecordingState.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.PWORD] +PCO_CAMERA_STOPPED = 0x0000 +PCO_CAMERA_RUNNING = 0x0001 +def get_recording_state(handle): + """ + Get the current recording state of the camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + state : int + Recording state, one of PCO_CAMERA_STOPPED (0), PCO_CAMERA_RUNNING (1). + """ + state = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetRecordingState(handle, state)) + return state.value + +# --------------------------------------------------------------------- +# 2.7.4 PCO_SetRecordingState +# --------------------------------------------------------------------- +sc2_cam.PCO_SetRecordingState.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD] +def set_recording_state(handle, state): + """ + Set the current recording state of the camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + state : int + Recording state, one of PCO_CAMERA_STOPPED (0), PCO_CAMERA_RUNNING (1). + """ + check_status(sc2_cam.PCO_SetRecordingState(handle, ctypes.wintypes.WORD(state))) + +# --------------------------------------------------------------------- +# 2.7.5 PCO_GetStorageMode +# --------------------------------------------------------------------- +sc2_cam.PCO_GetStorageMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.PWORD] +PCO_STORAGE_RECORDER = 0x0000 +PCO_STORAGE_FIFO = 0x0001 +def get_storage_mode(handle): + """ + Get camera image storage mode. One of "recorder" or "FIFO buffer". + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + mode : int + Camera image storage mode. One of PCO_STORAGE_RECORDER (0) or PCO_STORAGE_FIFO (1). + """ + mode = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetStorageMode(handle, mode)) + return mode.value + +# --------------------------------------------------------------------- +# 2.7.6 PCO_SetStorageMode +# --------------------------------------------------------------------- +sc2_cam.PCO_SetStorageMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD] +def set_storage_mode(handle, mode): + """ + Get camera image storage mode. One of "recorder" or "FIFO buffer". + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + mode : unsigned short int + Camera image storage mode. One of PCO_STORAGE_RECORDER (0) or PCO_STORAGE_FIFO (1). + """ + check_status(sc2_cam.PCO_SetStorageMode(handle, ctypes.wintypes.WORD(mode))) + +# --------------------------------------------------------------------- +# 2.7.9 PCO_GetAcquireMode +# --------------------------------------------------------------------- +sc2_cam.PCO_GetAcquireMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.PWORD] +PCO_ACQUIRE_AUTO = 0x0000 +PCO_ACQUIRE_EXTERNAL = 0x0000 +PCO_ACQUIRE_EXTERNAL_MODULATE = 0x0000 +def get_acquire_mode(handle): + """ + Get the current acquisition mode of the camera. One of "auto", + "external" or "external modulate". + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + mode : unsigned short int + Camera acquisition mode. One of PCO_ACQUIRE_AUTO (0), PCO_ACQUIRE_EXTERNAL (1), + PCO_ACQUIRE_EXTERNAL_MODULATE (2). + """ + mode = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetAcquireMode(handle, mode)) + return mode.value + +# --------------------------------------------------------------------- +# 2.7.10 PCO_SetAcquireMode +# --------------------------------------------------------------------- +sc2_cam.PCO_SetAcquireMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD] +def set_acquire_mode(handle, mode): + """ + Set the current acquisition mode of the camera. One of "auto", + "external" or "external modulate". + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + mode : unsigned short int + Camera acquisition mode. One of PCO_ACQUIRE_AUTO (0), PCO_ACQUIRE_EXTERNAL (1), + PCO_ACQUIRE_EXTERNAL_MODULATE (2). + """ + check_status(sc2_cam.PCO_GetAcquireMode(handle, ctypes.wintypes.WORD(mode))) + +# --------------------------------------------------------------------- +# 2.7.14 PCO_GetMetaDataMode +# --------------------------------------------------------------------- +sc2_cam.PCO_GetMetaDataMode.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD] +PCO_METADATA_OFF = 0x0000 +PCO_METADATA_ON = 0x0000 +def get_metadata_mode(handle): + """ + Get the metadata mode of the camera and information about the size + and version of the metadata block. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + mode : unsigned short int + Metadata mode. One of PCO_METADATA_OFF (0) or PCO_METADATA_ON (1). + size : unsigned short int + Size of the matadata block added to the image. + version : unsigned short int + Version number of the metadata mode + """ + mode = ctypes.wintypes.WORD() + size = ctypes.wintypes.WORD() + version = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetMetaDataMode(handle, mode, size, version)) + return mode.value, size.value, version.value + +# --------------------------------------------------------------------- +# 2.7.15 PCO_SetMetaDataMode +# --------------------------------------------------------------------- +sc2_cam.PCO_SetMetaDataMode.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.PWORD, + ctypes.wintypes.PWORD] +def set_metadata_mode(handle, mode): + """ + Set the metadata mode of the camera and get information about the size + and version of the metadata block. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + mode : unsigned short int + Metadata mode. One of PCO_METADATA_OFF (0) or PCO_METADATA_ON (1). + + Returns + ------- + size : unsigned short int + Size of the matadata block added to the image. + version : unsigned short int + Version number of the metadata mode + """ + size = ctypes.wintypes.WORD() + version = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_SetMetaDataMode(handle, ctypes.wintypes.WORD(mode), + size, version)) + return size.value, version.value + +# --------------------------------------------------------------------- +# 2.9.5 PCO_GetBitAlignment +# --------------------------------------------------------------------- +sc2_cam.PCO_GetBitAlignment.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.PWORD] +PCO_ALIGNMENT_MSB = 0x0000 # align to most significant bit +PCO_ALIGNMENT_LSB = 0x0001 # align to least significant bit +def get_bit_alignment(handle): + """ + Get the bit alignment of the transferred image data. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + alignment : unsigned short int + Bit alignment of the transferred image data. One of PCO_ALIGNMENT_MSB (0) + or PCO_ALIGNMENT_LSB (1). + """ + alignment = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetBitAlignment(handle, alignment)) + return alignment.value + +# --------------------------------------------------------------------- +# 2.9.6 PCO_SetBitAlignment +# --------------------------------------------------------------------- +sc2_cam.PCO_SetBitAlignment.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD] +def set_bit_alignment(handle, alignment): + """ + Set the bit alignment of the transferred image data. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + alignment : unsigned short int + Bit alignment of the transferred image data. One of PCO_ALIGNMENT_MSB (0) + or PCO_ALIGNMENT_LSB (1). + """ + check_status(sc2_cam.PCO_SetBitAlignment(handle, ctypes.wintypes.WORD(alignment))) + +# --------------------------------------------------------------------- +# 2.9.7 PCO_GetHotPixelCorrectionMode +# --------------------------------------------------------------------- +sc2_cam.PCO_GetHotPixelCorrectionMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.PWORD] +PCO_HOTPIXELCORRECTION_OFF = 0x0000 +PCO_HOTPIXELCORRECTION_ON = 0x0001 +def get_hot_pixel_correction_mode(handle): + """ + Get camera chip hot pixel correction mode. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + + Returns + ------- + mode : unsigned short int + Hot pixel correction mode. One of PCO_HOTPIXELCORRECTION_OFF (0) or + PCO_HOTPIXELCORRECTION_ON (1). + """ + mode = ctypes.wintypes.WORD() + check_status(sc2_cam.PCO_GetHotPixelCorrectionMode(handle, mode)) + return mode.value + +# --------------------------------------------------------------------- +# 2.9.8 PCO_SetHotPixelCorrectionMode +# --------------------------------------------------------------------- +sc2_cam.PCO_SetHotPixelCorrectionMode.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.WORD] +def set_hot_pixel_correction_mode(handle, mode): + """ + Set camera chip hot pixel correction mode. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + mode : unsigned short int + Hot pixel correction mode. One of PCO_HOTPIXELCORRECTION_OFF (0) or + PCO_HOTPIXELCORRECTION_ON (1). + """ + check_status(sc2_cam.PCO_SetHotPixelCorrectionMode(handle, ctypes.wintypes.WORD(mode))) + +# --------------------------------------------------------------------- +# 2.10.1 PCO_AllocateBuffer +# --------------------------------------------------------------------- +sc2_cam.PCO_AllocateBuffer.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(ctypes.c_short), + ctypes.wintypes.DWORD, + ctypes.POINTER(ctypes.wintypes.PWORD), + ctypes.POINTER(ctypes.wintypes.HANDLE)] + +def allocate_buffer(handle, index, size, buffer=None, event=None): + """ + Allocate a buffer + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + index : int + Buffer index to access from a previous call + or -1 to create a new buffer. + size : int + Buffer size in bytes + buffer : double pointer + Pointer to a memory block, default None (gets allocated internally) + event : pointer + Pointer to an event handle, default None (gets allocated internally) + + Returns + ------- + index : int + Buffer index + buffer : double pointer + Pointer to a memory block + event : pointer + Pointer to an event handle + """ + if buffer is None: + buffer = ctypes.c_void_p(0) + if event is None: + event = ctypes.c_void_p(0) + check_status(sc2_cam.PCO_AllocateBuffer(handle, ctypes.c_short(index), ctypes.wintypes.DWORD(size), + buffer, event)) + if buffer is None and event is None: + return index.contents, buffer, event + +# --------------------------------------------------------------------- +# 2.10.2 PCO_FreeBuffer +# --------------------------------------------------------------------- +sc2_cam.PCO_FreeBuffer.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_short] +def free_buffer(handle, index): + """ + Release a previously-allocated buffer with the given index. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + index : int + Buffer index to free + """ + check_status(sc2_cam.PCO_FreeBuffer(handle, ctypes.c_short(index))) + +# --------------------------------------------------------------------- +# 2.10.3 PCO_GetBufferStatus +# --------------------------------------------------------------------- +sc2_cam.PCO_GetBufferStatus.argtypes = [ctypes.wintypes.HANDLE, + ctypes.c_short, + ctypes.wintypes.PDWORD, + ctypes.wintypes.PDWORD] +PCO_BUFFER_ALLOCATED = 0x80000000 # Buffer is allocated +PCO_BUFFER_CREATED = 0x40000000 # Buffer event created inside the SDK DLL +PCO_BUFFER_EXTERNAL = 0x20000000 # Buffer is allocated externally +PCO_BUFFER_SET = 0x00008000 # Buffer event is set +def get_buffer_status(handle, index): + """ + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + index : int + Buffer index to get status + + Returns + ------- + status_dll : unsigned long int + Status of buffer inside the DLL. One of PCO_BUFFER_ALLOCATED, PCO_BUFFER_CREATED, + PCO_BUFFER_EXTERNAL, PCO_BUFFER_SET. + status_drv : unsigned long int + Status of the image transfer. One of PCO_NOERROR or see pco.sdk for error + codes. + """ + status_dll = ctypes.wintypes.DWORD() + status_drv = ctypes.wintypes.DWORD() + check_status(sc2_cam.PCO_GetBufferStatus(handle, ctypes.c_short(index), + status_dll, status_drv)) + return status_dll.value, status_drv.value + +# --------------------------------------------------------------------- +# 2.10.4 PCO_GetBuffer +# --------------------------------------------------------------------- +sc2_cam.PCO_GetBuffer.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_short, + ctypes.POINTER(ctypes.wintypes.PWORD), + ctypes.POINTER(ctypes.wintypes.HANDLE)] +def get_buffer(handle, index): + """ + Get the buffer at index. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + index : int + Buffer index to get + + Returns + ------- + buffer : double pointer + Double pointer to a memory region + event: pointer + Pointer to event handle + """ + buffer = ctypes.c_void_p(0) + event = ctypes.c_void_p(0) + check_status(sc2_cam.PCO_GetBuffer(handle, ctypes.c_short(index), buffer, event)) + return buffer, event + +# --------------------------------------------------------------------- +# 2.11.1 PCO_GetImageEx +# --------------------------------------------------------------------- +sc2_cam.PCO_GetImageEx.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.c_short, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD] +def get_image_ex(handle, segment, first_image, buffer_index, lx, ly, + bits_per_pixel): + """ + Get a single image from the camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + segment : int + Index of memory segment. 1 is the default memory segment + first_image : int + Image number. 1 if PCO_GetRecordingState is PCO_CAMERA_STOPPED, 0 if it is PCO_CAMERA_RUNNING + buffer_index : int + Buffer index. + lx : int + Image width + ly : int + Image height + bits_per_pixel : int + Bit resolution of the transferred image (16 is the common choice, see pco.sdk manual) + """ + image_index = ctypes.wintypes.DWORD(first_image) + check_status(sc2_cam.PCO_GetImageEx(handle, ctypes.wintypes.WORD(segment), image_index, + image_index, ctypes.c_short(buffer_index), + ctypes.wintypes.WORD(lx), ctypes.wintypes.WORD(ly), + ctypes.wintypes.WORD(bits_per_pixel))) +# --------------------------------------------------------------------- +# 2.11.3 PCO_AddBufferEx +# --------------------------------------------------------------------- +sc2_cam.PCO_AddBufferEx.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.c_short, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD, + ctypes.wintypes.WORD] +def add_buffer_ex(handle, first_image, buffer_index, lx, ly, + bits_per_pixel): + """ + Set up buffer for single image transfer from the camera. Can add multiple + buffers for fast live recording. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + first_image : int + Image number. 1 if PCO_GetRecordingState is PCO_CAMERA_STOPPED, 0 if it is PCO_CAMERA_RUNNING + buffer_index : int + Buffer index. + lx : int + Image width + ly : int + Image height + bits_per_pixel : int + Bit resolution of the transferred image (16 is the common choice, see pco.sdk manual) + """ + image_index = ctypes.wintypes.DWORD(first_image) + check_status(sc2_cam.PCO_AddBufferEx(handle, image_index, image_index, + ctypes.c_short(buffer_index), + ctypes.wintypes.WORD(lx), ctypes.wintypes.WORD(ly), + ctypes.wintypes.WORD(bits_per_pixel))) + +# --------------------------------------------------------------------- +# 2.11.5 PCO_AddBufferExtern +# --------------------------------------------------------------------- +sc2_cam.PCO_AddBufferExtern.argtypes = [ctypes.wintypes.HANDLE, + ctypes.wintypes.HANDLE, + ctypes.wintypes.WORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.wintypes.DWORD, + ctypes.c_void_p, # alternatively numpy.ctypeslib.ndpointer + ctypes.wintypes.DWORD, + ctypes.wintypes.PDWORD] +def add_buffer_extern(handle, event, segment, first_image, buf, n_bytes, status): + """ + Set up buffer for single image transfer from the camera. Can add multiple + buffers for fast live recording. This function lets us use buffers we + create externally from the pco.sdk DLL. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + event : ctypes.wintypes.HANDLE + Event handle + segment : int + Camera internal memory segment. Most of the time this will have a value + of 1. In a default state, all memory is distributed to segement 1 and + segment 1 is the active segment. Camera internal memory is arranged as + an array with 4 segments. + first_image : int + Image number. 1 if PCO_GetRecordingState is PCO_CAMERA_STOPPED, 0 if it + is PCO_CAMERA_RUNNING + buf : ctypes.c_void_p + Pointer to a 2D image buffer, probably a numpy array. See + https://numpy.org/doc/stable/reference/routines.ctypeslib.html + n_bytes : int + Number of bytes in the 2D image buffer, buf + status : ctypes.wintypes.PDWORD + Pointer to buffer status bit + """ + image_index = ctypes.wintypes.DWORD(first_image) + synch = ctypes.wintypes.DWORD(0) + check_status(sc2_cam.PCO_AddBufferExtern(handle, event, ctypes.wintypes.WORD(segment), + image_index, image_index, synch, buf, + ctypes.wintypes.DWORD(n_bytes), + status)) + +# --------------------------------------------------------------------- +# 2.11.6 PCO_CancelImages +# --------------------------------------------------------------------- +sc2_cam.PCO_CancelImages.argtypes = [ctypes.wintypes.HANDLE] +def cancel_images(handle): + """ + + Remove all remaining buffers from the internal queue, reset the internal queue + and also reset the transfer state machine in the camera. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + """ + check_status(sc2_cam.PCO_CancelImages(handle)) + +# --------------------------------------------------------------------- +# 2.11.9 PCO_WaitforBuffer +# --------------------------------------------------------------------- +sc2_cam.PCO_WaitforBuffer.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_int, + ctypes.POINTER(PCO_Buflist), + ctypes.c_int] +def wait_for_buffer(handle, num_buffers, buffer_list, timeout): + """ + Wait for one or buffers, which are in the requeest queue of the driver. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + num_buffers : int + How many buffers in buffer_list? + buffer_list : list + List of PCO_Buflist containing all allocated buffers + we are waiting on. + timeout : int + Timeout in milliseconds. + """ + check_status(sc2_cam.PCO_WaitForBuffer(handle, ctypes.c_int(num_buffers), + buffer_list, ctypes.c_int(timeout))) + +# --------------------------------------------------------------------- +# 2.11.9 PCO_GetMetaData +# --------------------------------------------------------------------- +sc2_cam.PCO_GetMetaData.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_short, PCO_Metadata_Struct, + ctypes.wintypes.DWORD, ctypes.wintypes.DWORD] +def get_metadata(handle, index): + """ + Get metadata associated with image in the buffer index. + + Parameters + ---------- + handle : ctypes.wintypes.HANDLE + Unique pco. camera handle (pointer). + index : int + Buffer index to get metadata + + Returns + ------- + metadata : PCO_Metadata_Struct + Metadata from the buffer at index + """ + reserved1 = ctypes.wintypes.DWORD() + reserved2 = ctypes.wintypes.DWORD() + metadata = PCO_Metadata_Struct() + check_status(sc2_cam.PCO_GetMetaData(handle, ctypes.c_short(index), metadata, + reserved1, reserved2)) + return metadata + +# --------------------------------------------------------------------- +# 5.1 PCO_GetErrorText +# --------------------------------------------------------------------- +sc2_cam.PCO_GetErrorText.argtypes = [ctypes.wintypes.DWORD, + ctypes.c_char_p, + ctypes.wintypes.DWORD] +def get_error_text(err): + """ + Get detailed description for a pco.sdk error. + + Parameters + ---------- + err : int + Error number + + Returns + ------- + string + Error description. + """ + c_buf_len = 512 + c_buf = ctypes.create_string_buffer(c_buf_len) + sc2_cam.PCO_GetErrorText(ctypes.wintypes.DWORD(err), c_buf, c_buf_len) + + return str(c_buf.value.decode('ascii')) diff --git a/PYME/Acquire/Hardware/pco/pco_sdk_cam.py b/PYME/Acquire/Hardware/pco/pco_sdk_cam.py new file mode 100644 index 000000000..61dc1fbea --- /dev/null +++ b/PYME/Acquire/Hardware/pco/pco_sdk_cam.py @@ -0,0 +1,518 @@ +# -*- coding: utf-8 -*- + +""" +Created on Sun May 16 2021 + +@author: zacsimile +""" + +from PYME.Acquire.Hardware.Camera import Camera +from PYME.Acquire.Hardware.pco import pco_sdk +from PYME.Acquire import eventLog + +import numpy as np +import logging +logger = logging.getLogger(__name__) + +import ctypes +import ctypes.wintypes + +import queue +import threading +import time + +k32_dll = ctypes.windll.kernel32 # lets us use the recommended WaitForSingleObject call (see pco.sdk) + # instead of the not-recommended-for-polling pco_sdk.get_buffer_status() + +# Define event handle type (needed for pco_sdk.add_buffer_extern()) +# Generally we will want to use k32_dll.CreateEventA(None, 1, 0, None) +# See https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa +class _SECURITY_ATTRIBUTES(ctypes.Structure): + pass +LPSECURITY_ATTRIBUTES = ctypes.POINTER(_SECURITY_ATTRIBUTES) +k32_dll.CreateEventA.restype = ctypes.wintypes.HANDLE +k32_dll.CreateEventA.argtypes = [LPSECURITY_ATTRIBUTES, + ctypes.wintypes.BOOL, + ctypes.wintypes.BOOL, + ctypes.wintypes.LPCSTR] + +timebase = {pco_sdk.PCO_TIMEBASE_NS : 1e-9, + pco_sdk.PCO_TIMEBASE_US : 1e-6, + pco_sdk.PCO_TIMEBASE_MS : 1e-3} # Conversions + +class camReg(object): + """ + Keep track of the number of cameras initialised so we can initialise and + finalise the library. + """ + numCameras = -1 + + @classmethod + def regCamera(cls): + if cls.numCameras == -1: + pco_sdk.reset_lib() + + cls.numCameras += 1 + + @classmethod + def unregCamera(cls): + cls.numCameras -= 1 + if cls.numCameras == 0: + # There appears to be no pco.sdk uninitialization + pass + +camReg.regCamera() # initialize/reset the sdk + +MAX_BUFFERS = 100 +MAX_TIMEOUTS = 100 +MAX_QUEUED_BUFFERS = 16 # pco. has a hard limit on attaching no + # more than 16 buffers at a time to the camera + +class PcoSdkCam(Camera): + def __init__(self, camNum, debuglevel='off'): + Camera.__init__(self) + self._initalized = False + self.noiseProps = None + + def Init(self): + self._handle = pco_sdk.open_camera() + camReg.regCamera() + self._desc = pco_sdk.get_camera_description(self._handle) + + if pco_sdk.get_recording_state(self._handle) == pco_sdk.PCO_CAMERA_RUNNING: + pco_sdk.set_recording_state(self._handle, pco_sdk.PCO_CAMERA_STOPPED) + pco_sdk.reset_settings_to_default(self._handle) + _, err, _ = pco_sdk.get_camera_health_status(self._handle) + if err != 0: + self.Shutdown() + raise pco_sdk.PcoSdkException(f"Camera shutdown with error status {err}.") + + self._integ_time = 0 + self._delay_time = 0 + self._electr_temp = 0 + self._ccd_temp = 0 + self.__default_n_buffers = 16 + self._n_buffers = 0 + self._buffer = None + self._bufsize = 0 + self._buffer_status = None + self._buf_event = [] + self._buf_addr = [] + self._buf_status_addr = [] + self._recording = False + self._roi = None + #self._timeout = 1000 # ms + self._n_buffered = 0 + self._n_queued = 0 + self._binning_x = 1 + self._binning_y = 1 + self._n_timeouts = 0 + self._i = 0 + self._buffers_to_queue = queue.Queue() + self._queued_buffers = queue.Queue() + self._full_buffers = queue.Queue() + self.SetROI(1, 1, self.GetCCDWidth(), self.GetCCDHeight()) + self.SetIntegTime(0.025) + self.SetAcquisitionMode(self.MODE_CONTINUOUS) + self._cam_type = pco_sdk.get_camera_type(self._handle) + self.SetHotPixelCorrectionMode(pco_sdk.PCO_HOTPIXELCORRECTION_OFF) + + self._buffer_lock = threading.RLock() + + self._polling = True + self._poll_thread = threading.Thread(target=self._poll_loop) + self._poll_thread.start() + + self._initalized = True + + @property + def noise_properties(self): + return self.noiseProps + + def ExpReady(self): + if not self._recording: + return False + + return (self._n_buffered > 0) + + def _poll_loop(self): + while self._polling: + sleep_time = 0.0001 # FIXME - this seems very short! + with self._buffer_lock: + if self._recording: + # lock to prevent race condition when stopping acquisition and emptying buffers + while not self._buffers_to_queue.empty() and (self._n_queued < MAX_QUEUED_BUFFERS): + i = self._buffers_to_queue.get() + pco_sdk.add_buffer_extern(self._handle, self._buf_event[i], + 1, 0, self._buf_addr[i], self._bufsize, + self._buf_status_addr[i]) + self._queued_buffers.put(i) + self._n_queued += 1 + + if self._n_queued > 0: + _curr_buf = self._queued_buffers.get() + self._n_queued -= 1 + + # wait for the buffer + wait_status = k32_dll.WaitForSingleObject(self._buf_event[_curr_buf], self._timeout) + if wait_status: + logger.warning(f"Waited too long for buffer ({self._timeout} ms).") + + #TODO: we currently continue as if we got the buffer - is this the right thing to do? + # Presumably the status will be non-zero and we will drop the buffer?, but then what + # happens to those buffers? do they just dissapear? + + k32_dll.ResetEvent(self._buf_event[_curr_buf]) + # make sure this buffer is safe to use + status = self._buffer_status[_curr_buf] + if status: + logger.warning(f"Error {status} during check of buffer {_curr_buf}.") + #DB: Do you see a lot of these warnings? IE - do we get one every time we have a timeout? + # drop this buffer + else: + # use it + self._full_buffers.put(_curr_buf) + self._n_buffered += 1 + else: + # sleep for a bit longer if there were no buffers queued + sleep_time = 0.01 + + else: + #sleep for longer if we are not recording + sleep_time = 0.01 + + time.sleep(sleep_time) + + + def CamReady(self): + return self._initalized + + def ExtractColor(self, chSlice, mode): + if self._recording: + _curr_buf = self._full_buffers.get() + # update buffer index + self._n_buffered -= 1 + + # copy image from _buf_addr + ctypes.cdll.msvcrt.memcpy(chSlice.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)), + self._buf_addr[_curr_buf], + chSlice.nbytes) + + if self._mode == self.MODE_CONTINUOUS: + # auto-recycle the buffer + self._buffers_to_queue.put(_curr_buf) + + def GetName(self): + return pco_sdk.get_camera_name(self._handle) + + def GetHeadModel(self): + return pco_sdk.PCO_CAMERA_TYPES.get(self._cam_type.wCamType) + + def GetSerialNumber(self): + return str(self._cam_type.dwSerialNumber) + + def SetIntegTime(self, time): + lb = float(self._desc.dwMinExposDESC)*1e-9 + ub = float(self._desc.dwMaxExposDESC)*1e-3 + step = float(self._desc.dwMinExposStepDESC)*1e-9 + + # Round to a multiple of time step + time = np.round(time/step)*step + + # Don't let this go out of bounds + time = np.clip(time, lb, ub) + + pco_sdk.set_delay_exposure_time(self._handle, int(self._delay_time*1e3), int(time*1e3), + pco_sdk.PCO_TIMEBASE_MS, pco_sdk.PCO_TIMEBASE_MS) + _, exposure, _, timebase_exposure = pco_sdk.get_delay_exposure_time(self._handle) + self._integ_time = exposure*timebase.get(timebase_exposure) + + def GetIntegTime(self): + return self._integ_time + + def GetCycleTime(self): + return (self._integ_time + self._delay_time) + + def GetCCDWidth(self): + return self._desc.wMaxHorzResStdDESC + + def GetCCDHeight(self): + return self._desc.wMaxVertResStdDESC + + def GetPicWidth(self): + return self.GetROI()[2] - self.GetROI()[0] + 1 + + def GetPicHeight(self): + return self.GetROI()[3] - self.GetROI()[1] + 1 + + def SetHorizontalBin(self, value): + b_max, step_type = self._desc.wMaxBinHorzDESC, self._desc.wBinHorzSteppingDESC + + if step_type == 0: + # binary binning, round to powers of two + value = 1<<(value-1).bit_length() + else: + value = int(np.round(value)) # integer + + value = np.clip(value, 1, b_max) + + pco_sdk.set_binning(self._handle, value, self.GetVerticalBin()) + self._binning_x, _ = pco_sdk.get_binning(self._handle) + + def GetHorizontalBin(self): + return self._binning_x + + def SetVerticalBin(self, value): + b_max, step_type = self._desc.wMaxBinVertDESC, self._desc.wBinVertSteppingDESC + + if step_type == 0: + # binary binning, round to powers of two + value = 1<<(value-1).bit_length() + else: + value = int(np.round(value)) + + value = np.clip(value, 1, b_max) + + pco_sdk.set_binning(self._handle, self.GetHorizontalBin(), value) + _, self._binning_y = pco_sdk.get_binning(self._handle) + + def GetVerticalBin(self): + return self._binning_y + + def GetSupportedBinning(self): + import itertools + + bx_max, bx_step_type = self._desc.wMaxBinHorzDESC, self._desc.wBinHorzSteppingDESC + by_max, by_step_type = self._desc.wMaxBinVertDESC, self._desc.wBinVertSteppingDESC + + if bx_step_type == 0: + # binary step type + x = [2**j for j in np.arange((bx_max).bit_length())] + else: + x = [j for j in np.arange(bx_max)] + + if by_step_type == 0: + y = [2**j for j in np.arange((by_max).bit_length())] + else: + y = [j for j in np.arange(by_max)] + + return list(itertools.product(x,y)) + + def SetROI(self, x0, y0, x1, y1): + # Stepping (n pixels) + dx = self._desc.wRoiHorStepsDESC + dy = self._desc.wRoiVertStepsDESC + + # Chip size + lx_max = self.GetCCDWidth() + ly_max = self.GetCCDHeight() + + # Minimum ROI size + lx_min = self._desc.wMinSizeHorzDESC + ly_min = self._desc.wMinSizeVertDESC + + # Make sure bounds are ordered + if x1 < x0: + x0, x1 = x1, x0 + if y1 < y0: + y0, y1 = y1, y0 + + # Don't let the ROI go out of bounds + # See pco.sdk manual chapter 3: IMAGE AREA SELECTION (ROI) + x0 = np.clip(x0, 1, lx_max-dx+1) + y0 = np.clip(y0, 1, ly_max-dy+1) + x1 = np.clip(x1, 1+dx, lx_max) + y1 = np.clip(y1, 1+dy, ly_max) + + # Don't let us choose too small an ROI + if (x1-x0+1) < lx_min: + logger.debug('Selected ROI width is too small, automatically adjusting to {}.'.format(lx_min)) + guess_pos = x0+lx_min + # Deal with boundaries + if guess_pos <= lx_max: + x1 = guess_pos + else: + x0 = x1-lx_min-1 + if (y1-y0+1) < ly_min: + logger.debug('Selected ROI height is too small, automatically adjusting to {}.'.format(ly_min)) + guess_pos = y0+ly_min + if guess_pos <= ly_max: + y1 = guess_pos + else: + y0 = y1-ly_min-1 + + # Round to a multiple of dx, dy + # TODO: Why do I need the 1 correction only on x0, y0??? + x0 = 1+int(np.floor((x0-1)/dx)*dx) + y0 = 1+int(np.floor((y0-1)/dy)*dy) + x1 = int(np.floor(x1/dx)*dx) + y1 = int(np.floor(y1/dy)*dy) + + pco_sdk.set_roi(self._handle, x0, y0, x1, y1) + self._roi = [x0, y0, x1, y1] + + logger.debug('ROI set: x0 %3.1f, y0 %3.1f, w %3.1f, h %3.1f' % (x0, y0, x1-x0+1, y1-y0+1)) + + def GetROI(self): + return self._roi + + def GetElectrTemp(self): + return self._electr_temp + + def GetCCDTemp(self): + return self._ccd_temp + + def GetCCDTempSetPoint(self): + return self._ccd_temp_set_point + + def SetCCDTemp(self, temp): + lb = self._desc.sMinCoolSetDESC + ub = self._desc.sMaxCoolSetDESC + temp = np.clip(temp, lb, ub) + + pco_sdk.set_cooling_setpoint_temperature(self._handle, temp) + self._ccd_temp_set_point = temp + self._get_temps() + + def _get_fan_mode_speed(self): + mode, speed = pco_sdk.get_fan_control_parameters(self._handle) + return mode, speed + + def _set_fan_mode_speed(self, mode, speed): + # TODO - unified fan control interface for all cameras (see also AndorIxon `SetFan()` and comments on PR1135) + pco_sdk.set_fan_control_parameters(self._handle, mode, speed) + + def _get_temps(self): + # NOTE: temperature only gets probed when acquisition starts/stops (which can be fairly + # irregularly - to the point of not being useful). + # TODO - find a way to safely call this while the camera is running + # FIXME: should _electr_tmp be power temperature rather than cam temp? + self._ccd_temp, self._electr_temp, _ = pco_sdk.get_temperature(self._handle) + + def GetAcquisitionMode(self): + return self._mode + + def SetAcquisitionMode(self, mode): + if mode in [self.MODE_SINGLE_SHOT, self.MODE_CONTINUOUS, self.MODE_SOFTWARE_TRIGGER]: + self._mode = mode + else: + raise RuntimeError(f"Mode {mode} not supported") + + # Number of buffers may change depending on mode + self._n_buffers = self.__default_n_buffers + + # Set trigger mode every time in case we were previously + # in a triggered mode + trigger = 0x0000 + if (mode == self.MODE_SINGLE_SHOT) or (mode == self.MODE_SOFTWARE_TRIGGER): + trigger = 0x0001 + self._i = 0 + pco_sdk.set_trigger_mode(self._handle, trigger) + + def _init_buffers(self): + with self._buffer_lock: + # Establish buffers + lx, ly = self.GetPicWidth(), self.GetPicHeight() + self.SetBufferSize(int(max(int(2.0*self.GetFPS()), 1))) + self._buffer = np.zeros((lx, ly, self._n_buffers), dtype=np.uint16) + __buffer = self._buffer.ctypes.data_as(ctypes.c_void_p) + self._buffer_status = np.zeros(self._n_buffers, dtype=np.uint16) + __buffer_status = self._buffer_status.ctypes.data_as(ctypes.c_void_p) + self._bufsize = self._buffer[:,:,0].nbytes # how many words is this image worth? + for i in np.arange(self._n_buffers): + self._buf_event.append(k32_dll.CreateEventA(None, 1, 0, None)) + self._buf_addr.append(ctypes.c_void_p(__buffer.value + int(i*self._bufsize))) + self._buf_status_addr.append(ctypes.cast(ctypes.c_void_p(__buffer_status.value+int(i*np.dtype(np.uint16).itemsize)),ctypes.wintypes.PDWORD)) + if self._mode == self.MODE_CONTINUOUS: + self._buffers_to_queue.put(i) + + #self._timeout = int(max(2*100*self.GetCycleTime(), 100)) + pco_sdk.set_image_parameters(self._handle, lx, ly, pco_sdk.PCO_IMAGEPARAMETERS_READ_WHILE_RECORDING) + pco_sdk.arm_camera(self._handle) + pco_sdk.set_recording_state(self._handle, pco_sdk.PCO_CAMERA_RUNNING) + + @property + def _timeout(self): + return int(max(2*100*self.GetCycleTime(), 100)) + + def StartExposure(self): + #logger.debug(f'PcoSdkCam: StartExposure called from thread {threading.current_thread().name} at {time.time()}') + self._get_temps() + + with self._buffer_lock: + # we need to lock around checking and setting the _recording flag + # use the _buffer_lock (I've made this re-entrant so that it can still be called + # from within _init_buffers, but we could just as well bring the lock out here + # as this is the only place we call _init_buffers) + + if self._recording == False: + self._init_buffers() + self._recording = True + + self._log_exposure_start() + + + if (self._mode == self.MODE_SINGLE_SHOT) or (self._mode == self.MODE_SOFTWARE_TRIGGER): + self.TriggerAq() + + return 0 + + def StopAq(self): + with self._buffer_lock: + if self._recording: + self._recording = False + pco_sdk.set_recording_state(self._handle, pco_sdk.PCO_CAMERA_STOPPED) + pco_sdk.cancel_images(self._handle) + while not self._buffers_to_queue.empty(): + self._buffers_to_queue.get() + while not self._queued_buffers.empty(): + self._queued_buffers.get() + while not self._full_buffers.empty(): + self._full_buffers.get() + self._n_buffered = 0 + self._n_queued = 0 + self._n_timeouts = 0 + self._buf_event = [] + self._buf_addr = [] + self._buf_status_addr = [] + self._buffer = None + self._buffer_status = None + self._get_temps() + + def TriggerAq(self): + if (self._mode == self.MODE_SINGLE_SHOT) or (self._mode == self.MODE_SOFTWARE_TRIGGER): + res = pco_sdk.force_trigger(self._handle) + # FIFO queue the queable buffers so we don't + # grab images before a trigger in _poll_loop + self._buffers_to_queue.put(self._i) + self._i += 1 + if self._i >= self._n_buffers: + self._i = 0 + return res + return 0 + + def GetNumImsBuffered(self): + return self._n_buffered + + def GetBufferSize(self): + return self._n_buffers + + def SetBufferSize(self, n_buffers): + if n_buffers > MAX_BUFFERS: + logger.debug(f"{n_buffers} is greater than the maximum number of buffers, {MAX_BUFFERS}. Defaulting to {MAX_BUFFERS}.") + n_buffers = MAX_BUFFERS + self.__default_n_buffers = n_buffers + self._n_buffers = n_buffers + + def GetFPS(self): + if self.GetCycleTime() == 0: + return 0 + return 1.0/self.GetCycleTime() + + def SetHotPixelCorrectionMode(self, mode): + pco_sdk.set_hot_pixel_correction_mode(self._handle, mode) + + def Shutdown(self): + self._polling = False + pco_sdk.close_camera(self._handle) + camReg.unregCamera() diff --git a/PYME/Acquire/Hardware/pco/pco_sdk_cam_control_panel.py b/PYME/Acquire/Hardware/pco/pco_sdk_cam_control_panel.py new file mode 100644 index 000000000..cdb3be77e --- /dev/null +++ b/PYME/Acquire/Hardware/pco/pco_sdk_cam_control_panel.py @@ -0,0 +1,54 @@ +import wx + +# Mostly copied from ZylaControlPanel +class ModeControl(wx.Panel): + def __init__(self, parent, cam): + wx.Panel.__init__(self, parent) + self.scope = parent.scope + self.parent = parent + self.cam = cam + self.options = ["Single shot", "Continuous", "Software trigger", "Hardware trigger"] + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, "Mode : "), 1, wx.ALL, 2) + + self.choice = wx.Choice(self, -1, size = [100,-1], choices=self.options) + self.choice.Bind(wx.EVT_CHOICE, self.on_change) + hsizer.Add(self.choice, 0, wx.ALL, 2) + + self.update() + + self.SetSizerAndFit(hsizer) + + def on_change(self, event=None): + self.scope.frameWrangler.stop() + self.cam.SetAcquisitionMode(int(self.choice.GetSelection())) + self.scope.frameWrangler.start() + self.parent.update() + + def update(self): + self.choice.SetSelection(self.cam.GetAcquisitionMode()) + +class PcoSdkCamControl(wx.Panel): + def __init__(self, parent, cam, scope): + wx.Panel.__init__(self, parent) + + self.cam = cam + self.scope = scope + + self.ctrls = [ModeControl(self, cam)] + + self._init_ctrls() + + def _init_ctrls(self): + vsizer = wx.BoxSizer(wx.VERTICAL) + + for c in self.ctrls: + vsizer.Add(c, 0, wx.EXPAND|wx.ALL, 2) + + self.SetSizerAndFit(vsizer) + + def update(self): + for c in self.ctrls: + c.update() + \ No newline at end of file diff --git a/PYME/Acquire/Hardware/splitter.py b/PYME/Acquire/Hardware/splitter.py index 411b3aff1..3d87de3dd 100644 --- a/PYME/Acquire/Hardware/splitter.py +++ b/PYME/Acquire/Hardware/splitter.py @@ -23,170 +23,16 @@ ################## import wx -from PYME.DSView.arrayViewPanel import ArrayViewPanel, OptionsPanel +from PYME.DSView.arrayViewPanel import ArrayViewPanel +from PYME.DSView.DisplayOptionsPanel import OptionsPanel import numpy import os import wx.lib.agw.aui as aui from PYME.IO import MetaDataHandler from PYME.IO.FileUtils import nameUtils +from PYME.Analysis import splitting -def LoadShiftField(filename = None): - if not filename: - fdialog = wx.FileDialog(None, 'Select shift field', - wildcard='*.sf;*.h5;*.h5r', style=wx.FD_OPEN) - succ = fdialog.ShowModal() - if (succ == wx.ID_OK): - filename = fdialog.GetPath() - else: - return None - - ext = os.path.splitext(filename)[1] - - if ext in ['sf']: - return numpy.load(filename) - else: - #try and extract shiftfield from h5 / h5r file - try: - import tables - from PYME.IO.MetaDataHandler import HDFMDHandler - - h5file = tables.open_file(filename) - mdh = HDFMDHandler(h5file) - - dx = mdh.getEntry('chroma.dx') - dy = mdh.getEntry('chroma.dy') - - return [dx,dy] - except: - return None - - - - -class Unmixer: - def __init__(self, shiftfield=None, pixelsize=70., flip=True, axis='up_down'): - self.pixelsize = pixelsize - self.flip = flip - self.axis = axis - if shiftfield: - self.SetShiftField(shiftfield) - - def SetShiftField(self, shiftField, scope): - #self.shiftField = shiftField - #self.shiftFieldname = sfname - - if self.axis == 'up_down': - X, Y = numpy.ogrid[:512, :256] - else: - X, Y = numpy.ogrid[:scope.cam.GetPicWidth()/2, :scope.cam.GetPicHeight()] - - self.X2 = numpy.round(X - shiftField[0](X*70., Y*70.)/70.).astype('i') - self.Y2 = numpy.round(Y - shiftField[1](X*70., Y*70.)/70.).astype('i') - - def _deshift(self, red_chan, ROI=[0,0,512, 512]): - if 'X2' in dir(self): - x1, y1, x2, y2 = ROI - - #print self.X2.shape - - if self.axis == 'up_down': - Xn = self.X2[x1:x2, y1:(y1 + red_chan.shape[1])] - x1 - Yn = self.Y2[x1:x2, y1:(y1 + red_chan.shape[1])] - y1 - else: - Xn = self.X2[x1:(x1 + red_chan.shape[0]), y1:y2-1] - x1 - Yn = self.Y2[x1:(x1 + red_chan.shape[0]), y1:y2-1] - y1 - - #print Xn.shape - - Xn = numpy.maximum(numpy.minimum(Xn, red_chan.shape[0]-1), 0) - Yn = numpy.maximum(numpy.minimum(Yn, red_chan.shape[1]-1), 0) - - return red_chan[Xn, Yn] - - else: - return red_chan - - - def Unmix_(self, data, mixMatrix, offset, ROI=[0,0,512, 512]): - import scipy.linalg - from PYME.localisation import splitting - #from pylab import * - #from PYME.DSView.dsviewer_npy import View3D - - umm = scipy.linalg.inv(mixMatrix) - - dsa = data.squeeze() - offset - g_, r_ = [dsa[roi[0]:roi[2], roi[1]:roi[3]] for roi in rois] - - if self.flip: - if self.axis == 'up_down': - r_ = numpy.fliplr(r_) - else: - r_ = numpy.flipud(r_) - - r_ = self._deshift(r_, ROI) - - #print g_.shape, r_.shape - - g = umm[0,0]*g_ + umm[0,1]*r_ - r = umm[1,0]*g_ + umm[1,1]*r_ - - g = g*(g > 0) - r = r*(r > 0) - -# figure() -# subplot(211) -# imshow(g.T, cmap=cm.hot) -# -# subplot(212) -# imshow(r.T, cmap=cm.hot) - - #View3D([r.reshape(r.shape + (1,)),g.reshape(r.shape + (1,))]) - return [r.reshape(r.shape + (1,)),g.reshape(r.shape + (1,))] - - def Unmix(self, data, mixMatrix, offset, ROI=[0,0,512, 512]): - import scipy.linalg - - #from pylab import * - #from PYME.DSView.dsviewer_npy import View3D - - umm = scipy.linalg.inv(mixMatrix) - - dsa = data.squeeze() - offset - - if self.axis == 'up_down': - g_ = dsa[:, :int(dsa.shape[1]/2)] - r_ = dsa[:, int(dsa.shape[1]/2):] - if self.flip: - r_ = numpy.fliplr(r_) - r_ = self._deshift(r_, ROI) - else: - g_ = dsa[:int(dsa.shape[0]/2), :] - r_ = dsa[int(dsa.shape[0]/2):, :] - if self.flip: - r_ = numpy.flipud(r_) - r_ = self._deshift(r_, ROI) - - #print g_.shape, r_.shape - - g = umm[0,0]*g_ + umm[0,1]*r_ - r = umm[1,0]*g_ + umm[1,1]*r_ - - g = g*(g > 0) - r = r*(r > 0) - -# figure() -# subplot(211) -# imshow(g.T, cmap=cm.hot) -# -# subplot(212) -# imshow(r.T, cmap=cm.hot) - - #View3D([r.reshape(r.shape + (1,)),g.reshape(r.shape + (1,))]) - return [r.reshape(r.shape + (1,)),g.reshape(r.shape + (1,))] - - -class Splitter: +class Splitter(object): def __init__(self, parent, scope, cam, dir='up_down', flipChan=1, dichroic = 'Unspecified', transLocOnCamera = 'Top', constrain=True, flip = True, cam_name='', rois=None): self.dir = dir @@ -194,9 +40,11 @@ def __init__(self, parent, scope, cam, dir='up_down', flipChan=1, dichroic = 'Un self.cam = cam self.flipChan=flipChan self.parent = parent - self.unmixer = Unmixer(flip=flip, axis = dir) self.flip = flip self._rois=rois + pixelsize_nm = 1e3*(scope.GetPixelSize(cam)[0]) + self.unmixer = splitting.Unmixer(flip=flip, axis = dir, chanROIs=self.rois, pixelsize=pixelsize_nm) + #which dichroic mirror is installed self.dichroic = dichroic @@ -226,10 +74,10 @@ def __init__(self, parent, scope, cam, dir='up_down', flipChan=1, dichroic = 'Un parent.AddMenuItem('Splitter', 'Unmix%s\tF7' % suff, self.OnUnmix) parent.AddMenuItem('Splitter', 'SetShiftField%s' % suff, self.OnSetShiftField) -# idConstROI = wx.NewId() -# idFlipView = wx.NewId() -# idUnmix = wx.NewId() -# idShiftfield = wx.NewId() +# idConstROI = wx.NewIdRef() +# idFlipView = wx.NewIdRef() +# idUnmix = wx.NewIdRef() +# idShiftfield = wx.NewIdRef() # # self.menu = wx.Menu(title = '') @@ -333,7 +181,8 @@ def OnSetShiftField(self, event): self.SetShiftField(sfname) def SetShiftField(self, sfname): - self.shiftField = numpy.load(sfname) + from PYME.IO.compatibility import np_load_legacy + self.shiftField = np_load_legacy(sfname) self.shiftFieldName = sfname self.unmixer.SetShiftField(self.shiftField, self.scope) @@ -341,7 +190,7 @@ def SetShiftField(self, sfname): def Unmix(self): dsa = self.scope.frameWrangler.currentFrame.squeeze() - return self.unmixer.Unmix(dsa, self.mixMatrix, self.offset, ROI=[self.scope.cam.GetROIX1(),self.scope.cam.GetROIY1(),self.scope.cam.GetROIX2(), self.scope.cam.GetROIY2()]) + return self.unmixer.Unmix(dsa, self.mixMatrix, self.offset, ROI=self.scope.cam.GetROI()) class UnMixSettingsPanel(wx.Panel): @@ -473,7 +322,7 @@ def __init__(self, parent=None, title='Unmixing', splitter = None, size=(-1, -1) # # sizer.Add(pan, 0, 0, 0) - self.vp = ArrayViewPanel(self, self.ds) + self.vp = ArrayViewPanel(self, self.ds, initial_overlays=[]) sizer.Add(self.vp, 1,wx.EXPAND,0) #self.SetAutoLayout(1) self.SetSizer(sizer) diff --git a/PYME/Acquire/Hardware/thorlabs_elliptec.py b/PYME/Acquire/Hardware/thorlabs_elliptec.py new file mode 100644 index 000000000..b7de02d77 --- /dev/null +++ b/PYME/Acquire/Hardware/thorlabs_elliptec.py @@ -0,0 +1,109 @@ + + +# Windows only Thorlabs Elliptec stage control via Thorlabs provided dll +# requires https://pythonnet.github.io/ + + +dll = r"C:\Program Files\Thorlabs\Elliptec\Thorlabs.Elliptec.ELLO_DLL.dll" +import clr +clr.AddReference(dll) +from Thorlabs.Elliptec.ELLO_DLL import ELLDevices, ELLDevicePort +from System import Decimal +import logging +from PYME.Acquire.Hardware.Piezos.base_piezo import PiezoBase +from PYME.Acquire.Hardware.FilterWheel import FilterWheelBase + + +logger = logging.getLogger(__name__) + +class ElliptecBase(object): + """ + base class for Thorlabs elliptec devices. + See below for stage vs multiposition slider classes + """ + def __init__(self, com_port='COM4', device_number=0, home_on_init=False): + """ + Parameters + ---------- + com_port : str + The COM port to which the Elliptec controller is connected + device_number : int + index of the device to control (if daisy chaining) + home_on_init : bool + whether to home the device during initialization. Some devices + require homing before use, while others may not have home functionality. + """ + ELLDevicePort.Connect(com_port) + elldevices = ELLDevices() + logger.info("Scanning for devices...") + devices = elldevices.ScanAddresses('0', 'F') # returns list of strings + device = devices[device_number] # something like 0IN0E1140132620251801016800023000 + logger.info(f"configuring: {device}") + elldevices.Configure(device) + self.device = elldevices.AddressedDevice(device[0]) # index [0], only wants channel as char + if home_on_init: + logger.info("Homing...") + self.device.Home() + + self.dinfo = self.device.DeviceInfo.Description() + for line in self.dinfo: + logger.info(line) + + +class ElliptecStage(ElliptecBase, PiezoBase): + """ + Controls Thorlabs Elliptec single axes stages, or rotation stages. + Tested on ELL14 rotation stage. + """ + def __init__(self, com_port='COM4', device_number=0, home_on_init=False): + """ + Parameters + ---------- + com_port : str + The COM port to which the Elliptec controller is connected + device_number : int + index of the device to control (if daisy chaining) + home_on_init : bool + whether to home the device during initialization. Some devices + require homing before use, while others may not have home functionality. + """ + ElliptecBase.__init__(self, com_port, device_number, home_on_init) + + def MoveTo(self, iChannel, fPos, bTimeOut=True): + self.device.MoveAbsolute(Decimal(fPos)) + + def MoveRel(self, iChannel, incr, bTimeOut=True): + self.device.MoveRelative(Decimal(incr)) + + def GetPos(self, iChannel=0): + self.device.GetPosition() + pos = Decimal.ToDouble(self.device.Position) + return pos + + def GetMin(self, iChan=1): + return 0.0 + + def GetMax(self, iChan=1): + return Decimal.ToDouble(self.device.DeviceInfo.Travel) + + def GetFirmwareVersion(self): + for line in self.dinfo: + if 'firmware' in line.lower(): + return line.split(': ')[-1] + + @property + def units_um(self): + if self.device.DeviceInfo.Units == 'deg': + # rotation stage. Pretend degrees are microns + return 1 + elif self.device.DeviceInfo.Units == 'mm': + return 1000 + elif self.device.DeviceInfo.Units == 'inches': + return 25400 + + +class ElliptecMultiPositionSlider(ElliptecBase, FilterWheelBase): + def __init__(self, com_port='COM4', device_number=0): + super().__init__(com_port, device_number, home_on_init=False) + raise NotImplementedError("ElliptecMultiPositionSlider not yet implemented") + diff --git a/PYME/Acquire/Hardware/thorlabs_elliptec_serial.py b/PYME/Acquire/Hardware/thorlabs_elliptec_serial.py new file mode 100644 index 000000000..ab9be45fc --- /dev/null +++ b/PYME/Acquire/Hardware/thorlabs_elliptec_serial.py @@ -0,0 +1,166 @@ +# thorlabs_elliptec_serial.py +# Cross-platform Thorlabs Elliptec stage control via serial communication. +# Requires the elliptec package: https://github.com/roesel/elliptec +# To instead use the Thorlabs-provided DLL, see thorlabs_elliptec_dll.py + +import logging +from elliptec import Controller, Rotator, Linear, Slider +from elliptec.cmd import get_ +from PYME.Acquire.Hardware.Piezos.base_piezo import PiezoBase +from PYME.Acquire.Hardware.FilterWheel import FilterWheelBase + +logger = logging.getLogger(__name__) + +ROTATION_TYPES = frozenset({14, 18}) # ELL14, ELL18 +LINEAR_TYPES = frozenset({20}) # ELL20 +SLIDER_TYPES = frozenset({6, 9}) # ELL6, ELL9 + + +def _probe_motor_type(controller, address): + """Query device info and return the integer motor type (e.g. 14 for ELL14).""" + status = controller.send_instruction(get_['info'], address=address) + if isinstance(status, dict): + return int(status['Motor Type']) + raise RuntimeError(f"Could not get Elliptec device info at address {address!r}") + + +def _create_motor(controller, address): + """Create and return the appropriate Motor subclass for the device at address.""" + motor_type = _probe_motor_type(controller, address) + if motor_type in ROTATION_TYPES: + return Rotator(controller, address=address) + elif motor_type in LINEAR_TYPES: + return Linear(controller, address=address) + elif motor_type in SLIDER_TYPES: + return Slider(controller, address=address) + else: + raise ValueError(f"Unsupported Elliptec device type: ELL{motor_type}") + + +class ElliptecBase(object): + """Base class for Thorlabs Elliptec devices via serial.""" + + def __init__(self, com_port, address='0', home_on_init=False): + """ + Parameters + ---------- + com_port : str + Serial port. + address : str + Hex address of the device (0-F). When daisy-chaining use '0', '1', etc. + home_on_init : bool + Whether to home the device during initialization. + """ + self.controller = Controller(port=com_port) + self.motor = _create_motor(self.controller, address) + logger.info("Elliptec device connected: ELL%s (S/N: %s)", + self.motor.motor_type, self.motor.serial_no) + logger.info(str(self.motor)) + if home_on_init: + logger.info("Homing...") + self.motor.home() + + def close(self): + self.controller.close_connection() + + +class ElliptecStage(ElliptecBase, PiezoBase): + """Controls Thorlabs Elliptec linear or rotation stages via serial. + Units follow device type: degrees for rotation stages (ELL14/ELL18), + mm for linear stages (ELL20). + """ + + def __init__(self, com_port, address='0', home_on_init=False, soft_limits=None): + """ + Parameters + ---------- + soft_limits : tuple or None + Optional (min, max) movement limits in device units (degrees or mm). + Overrides hardware travel limits. E.g. (15.0, 45.0) for a linear stage. + """ + ElliptecBase.__init__(self, com_port, address, home_on_init) + if not isinstance(self.motor, (Rotator, Linear)): + raise ValueError( + f"ElliptecStage requires a rotation or linear device; " + f"got ELL{self.motor.motor_type}" + ) + self._soft_min = None + self._soft_max = None + if soft_limits is not None: + self.SetSoftLimits(0, soft_limits) + + def SetSoftLimits(self, iChannel, lims): + """Set software movement limits. + + Parameters + ---------- + iChannel : int + Channel index (unused, single-axis stage). + lims : tuple + (min, max) in device units (degrees for rotation, mm for linear). + """ + hw_min = 0.0 + hw_max = float(self.motor.range) + self._soft_min = max(float(lims[0]), hw_min) + self._soft_max = min(float(lims[1]), hw_max) + logger.info("Soft limits set to [%g, %g]", self._soft_min, self._soft_max) + + def _clamp(self, pos): + lo = self._soft_min if self._soft_min is not None else 0.0 + hi = self._soft_max if self._soft_max is not None else float(self.motor.range) + return max(lo, min(hi, pos)) + + def MoveTo(self, iChannel, fPos, bTimeOut=True): + self.motor._set_unit(self._clamp(fPos)) + + def MoveRel(self, iChannel, incr, bTimeOut=True): + current = self.motor._get_unit() or 0.0 + self.motor._set_unit(self._clamp(current + incr)) + + def GetPos(self, iChannel=0): + return self.motor._get_unit() + + def GetMin(self, iChan=1): + return self._soft_min if self._soft_min is not None else 0.0 + + def GetMax(self, iChan=1): + return self._soft_max if self._soft_max is not None else float(self.motor.range) + + def GetFirmwareVersion(self): + return self.motor.info.get('Firmware', None) + + @property + def units_um(self): + if self.motor.motor_type in ROTATION_TYPES: + # Rotation stage: treat degrees as microns (matches DLL convention) + return 1 + elif self.motor.motor_type in LINEAR_TYPES: + # Linear stage: mm -> um + return 1000 + return 1 + + +class ElliptecMultiPositionSlider(ElliptecBase, FilterWheelBase): + """Controls Thorlabs Elliptec multi-position sliders (ELL6, ELL9) via serial. + + installedFilters should be a list of WFilter objects whose .pos values are + 1-indexed slot numbers matching the physical slider positions. + """ + + def __init__(self, com_port, address='0', installedFilters=None): + ElliptecBase.__init__(self, com_port, address, home_on_init=False) + if not isinstance(self.motor, Slider): + raise ValueError( + f"ElliptecMultiPositionSlider requires a slider device (ELL6/ELL9); " + f"got ELL{self.motor.motor_type}" + ) + FilterWheelBase.__init__(self, installedFilters or []) + + def _set_physical_position(self, pos): + """Move to slot. pos is a 1-indexed slot number matching WFilter.pos values.""" + self.motor.set_slot(pos) + + def _get_physical_position(self): + """Returns the current 1-indexed slot number.""" + return self.motor.get_slot() + diff --git a/PYME/Acquire/Hardware/thorlabs_mff_flipper.py b/PYME/Acquire/Hardware/thorlabs_mff_flipper.py new file mode 100644 index 000000000..66108be14 --- /dev/null +++ b/PYME/Acquire/Hardware/thorlabs_mff_flipper.py @@ -0,0 +1,396 @@ + +# Ctypes thorlabs kinesis interface for MFF101, thorlab's motorized flip mirror. +# Kinesis interface by Zach Marin (zacsimile) +# see https://github.com/Thorlabs/Motion_Control_Examples/pull/14 + + +import os +import sys +import ctypes +import ctypes.wintypes +from enum import IntEnum +from PYME.Acquire.Hardware.FilterWheel import FilterWheelBase, WFilter + +CODING = 'ascii' + +if sys.version_info < (3, 8): + os.chdir(r"C:\Program Files\Thorlabs\Kinesis") +else: + os.add_dll_directory(r"C:\Program Files\Thorlabs\Kinesis") +__dll = ctypes.WinDLL("C:\Program Files\Thorlabs\Kinesis\Thorlabs.MotionControl.FilterFlipper.dll") + + +class TLFTDICommunicationError(Exception): + """Exception for Thorlabs FTDI communications module or supporting code.""" + + +class TLDLLError(Exception): + """Exception for general Thorlabs DLL control errors.""" + + +class TLMotorDLLError(Exception): + """Exception for motor-specific Thorlabs DLL errors.""" + + +class FT_Status(IntEnum): + """The following errors are generated from the FTDI communications module, supporting code or device libraries.""" + FT_OK = 0 # No error + FT_InvalidHandle = 1 # The FTDI functions have not been initialized. + FT_DeviceNotFound = 2 # The Device could not be found. This can be generated if the function TLI_BuildDeviceList() has not been called. + FT_DeviceNotOpened = 3 # The Device must be opened before it can be accessed + FT_IOError = 4 # An I/O Error has occured in the FTDI chip. + FT_InsufficientResources = 5 # There are Insufficient resources to run this application. + FT_InvalidParameter = 6 # An invalid parameter has been supplied to the device. + FT_DeviceNotPresent = 7 # The Device is no longer present. The device may have been disconnected since the last TLI_BuildDeviceList() call. + FT_IncorrectDevice = 8 # The device detected does not match that expected + FT_NoDLLLoaded = 16 # The library for this device could not be found + FT_NoFunctionsAvailable = 17 # No functions available for this device + FT_FunctionNotAvailable = 18 # The function is not available for this device. + FT_BadFunctionPointer = 19 # Bad function pointer detected + FT_GenericFunctionFail = 20 # The function failed to complete successfully. + FT_SpecificFunctionFail = 21 # The function failed to complete successfully. + + +FT_Status_Description = { + FT_Status.FT_OK : "No error", + FT_Status.FT_InvalidHandle : "The FTDI functions have not been initialized.", + FT_Status.FT_DeviceNotFound : "The Device could not be found. This can be generated if the function TLI_" + "BuildDeviceList() has not been called.", + FT_Status.FT_DeviceNotOpened : "The Device must be opened before it can be accessed.", + FT_Status.FT_IOError : "An I/O Error has occured in the FTDI chip.", + FT_Status.FT_InsufficientResources : "There are Insufficient resources to run this application.", + FT_Status.FT_InvalidParameter : "An invalid parameter has been supplied to the device.", + FT_Status.FT_DeviceNotPresent : "The Device is no longer present. The device may have been disconnected " + "since the last TLI_BuildDeviceList() call.", + FT_Status.FT_IncorrectDevice : "The device detected does not match that expected.", + FT_Status.FT_NoDLLLoaded : "The library for this device could not be found.", + FT_Status.FT_FunctionNotAvailable : "The function is not available for this device.", + FT_Status.FT_NoFunctionsAvailable : "No functions available for this device.", + FT_Status.FT_BadFunctionPointer : "Bad function pointer detected.", + FT_Status.FT_GenericFunctionFail : "The function failed to complete successfully.", + FT_Status.FT_SpecificFunctionFail : "The function failed to complete successfully." +} + + +class TL_DLL_Error(IntEnum): + """The following errors are general errors generated by all DLLs. """ + TL_ALREADY_OPEN = 32 # Attempt to open a device that was already open. + TL_NO_RESPONSE = 33 # The device has stopped responding. + TL_NOT_IMPLEMENTED = 34 # This function has not been implemented. + TL_FAULT_REPORTED = 35 # The device has reported a fault. + TL_INVALID_OPERATION = 36 # The function could not be completed at this time. + TL_DISCONNECTING = 40 # The function could not be completed because the device is disconnected. + TL_FIRMWARE_BUG = 41 # The firmware has thrown an error. + TL_INITIALIZATION_FAILURE = 42 # The device has failed to initialize + TL_INVALID_CHANNEL = 43 # An Invalid channel address was supplied + + +TL_DLL_Error_Description = { + TL_DLL_Error.TL_ALREADY_OPEN : "Attempt to open a device that was already open.", + TL_DLL_Error.TL_NO_RESPONSE : "The device has stopped responding.", + TL_DLL_Error.TL_NOT_IMPLEMENTED : "This function has not been implemented.", + TL_DLL_Error.TL_FAULT_REPORTED : "The device has reported a fault.", + TL_DLL_Error.TL_INVALID_OPERATION : "The function could not be completed at this time.", + TL_DLL_Error.TL_DISCONNECTING : "The function could not be completed because the device is " + "disconnected.", + TL_DLL_Error.TL_FIRMWARE_BUG : "The firmware has thrown an error.", + TL_DLL_Error.TL_INITIALIZATION_FAILURE : "The device has failed to initialize.", + TL_DLL_Error.TL_INVALID_CHANNEL : "An Invalid channel address was supplied.", +} + + +class Motor_DLL_Error(IntEnum): + """The following errors are motor specific errors generated by the Motor DLLs.""" + TL_UNHOMED = 37 # The device cannot perform this function until it has been Homed. + TL_INVALID_POSITION = 38 # The function cannot be performed as it would result in an illegal position. + TL_INVALID_VELOCITY_PARAMETER = 39 # An invalid velocity parameter was supplied. The velocity must be greater than zero. + TL_CANNOT_HOME_DEVICE = 44 # This device does not support Homing. Check the Limit switch parameters are correct. + TL_JOG_CONTINOUS_MODE = 45 # An invalid jog mode was supplied for the jog function. + TL_NO_MOTOR_INFO = 46 # There is no Motor Parameters available to convert Real World Units. + TL_CMD_TEMP_UNAVAILABLE = 47 # Command temporarily unavailable, Device may be busy. + + +Motor_DLL_Error_Description = { + Motor_DLL_Error.TL_UNHOMED : "The device cannot perform this function until it has been" + "Homed.", + Motor_DLL_Error.TL_INVALID_POSITION : "The function cannot be performed as it would result in an" + "illegal position.", + Motor_DLL_Error.TL_INVALID_VELOCITY_PARAMETER : "An invalid velocity parameter was supplied. The velocity must " + "be greater than zero.", + Motor_DLL_Error.TL_CANNOT_HOME_DEVICE : "This device does not support Homing. Check the Limit switch " + "parameters are correct.", + Motor_DLL_Error.TL_JOG_CONTINOUS_MODE : "An invalid jog mode was supplied for the jog function.", + Motor_DLL_Error.TL_NO_MOTOR_INFO : "There is no Motor Parameters available to convert Real World " + "Units.", + Motor_DLL_Error.TL_CMD_TEMP_UNAVAILABLE : "Command temporarily unavailable, Device may be busy." +} + + +def in_enum(value, enum): + values = set(item.value for item in enum) + return value in values + + +def errcheck(result, func, args): + """ + Wraps the call to DLL functions. + Parameters + ---------- + result : ctypes.c_short + Error code or 0 if successful. + func : function + DLL function + args : tuple + Arguments passed to the DLL function, defined in argtypes + Returns + ------- + result : int + Error code or 0 if successful. + """ + if result: + # returned a non-zero value + if in_enum(result, FT_Status): + raise TLFTDICommunicationError(FT_Status_Description[result]) + elif in_enum(result, TL_DLL_Error): + raise TLDLLError(TL_DLL_Error_Description[result]) + elif in_enum(result, Motor_DLL_Error): + raise TLMotorDLLError(Motor_DLL_Error_Description[result]) + else: + raise Exception(f"Unknown error {result} in Thorlabs device.") + return int(result) + + +__dll.TLI_BuildDeviceList.restype = ctypes.c_short +__dll.TLI_BuildDeviceList.errcheck = errcheck + + +def TLI_BuildDeviceList(): + """This function builds an internal collection of all devices found on the USB that are not currently open. + Returns + ------- + int + The error code or 0 if successful. + """ + return __dll.TLI_BuildDeviceList() + + +__dll.TLI_GetDeviceListSize.restype = ctypes.c_short + + +def TLI_GetDeviceListSize(): + """ + Gets the device list size. + Returns + ------- + int + Number of devices in device list. + """ + return __dll.TLI_GetDeviceListSize() + + +__dll.TLI_GetDeviceListExt.argtypes = [ctypes.c_char_p, ctypes.wintypes.DWORD] +__dll.TLI_GetDeviceListExt.restype = ctypes.c_short +__dll.TLI_GetDeviceListExt.errcheck = errcheck + + +def TLI_GetDeviceListExt(): + """Get the entire contents of the device list. + Returns + ------- + List + List of device serial numbers as strings. + """ + + c_buf_len = 256 + c_buf = ctypes.create_string_buffer(c_buf_len) + __dll.TLI_GetDeviceListExt(c_buf, c_buf_len) + + return str(c_buf.value.decode(CODING)).split(',')[:-1] + + +__dll.TLI_GetDeviceListByTypeExt.argtypes = [ctypes.c_char_p, ctypes.wintypes.DWORD, ctypes.c_int] +__dll.TLI_GetDeviceListByTypeExt.restype = ctypes.c_short +__dll.TLI_GetDeviceListByTypeExt.errcheck = errcheck + + +def TLI_GetDeviceListByTypeExt(type_id): + """Get the contents of the device list which match the supplied type_id. + Parameters + ---------- + type_id : int + Device type. + Returns + ------- + List + List of device serial numbers as strings. + """ + + c_buf_len = 256 + c_buf = ctypes.create_string_buffer(c_buf_len) + __dll.TLI_GetDeviceListByTypeExt(c_buf, c_buf_len, type_id) + + return str(c_buf.value.decode(CODING)).split(',')[:-1] + + +class MOT_MotorTypes(IntEnum): + """Values that represent THORLABSDEVICE_API.""" + MOT_NotMotor = 0 + MOT_DCMotor = 1 + MOT_StepperMotor = 2 + MOT_BrushlessMotor = 3 + MOT_CustomMotor = 100 + + +class TLI_DeviceInfo(ctypes.Structure): + """Information about the device generated from serial number.""" + _pack_ = 1 + _fields_ = [ + ("typeID", ctypes.wintypes.DWORD), # The device Type ID + ("description", ctypes.c_char * 65), # The device description. + ("serialNo", ctypes.c_char * 16), # The device serial number. + ("PID", ctypes.wintypes.DWORD), # The USB PID number. + ("isKnownType", ctypes.wintypes.BOOL), # True if this object is a type known to the Motion Control software. + ("motorType", ctypes.c_int), # The motor type (if a motor) + ("isPiezoDevice", ctypes.wintypes.BOOL), # True if the device is a piezo device. + ("isLaser", ctypes.wintypes.BOOL), # True if the device is a laser. + ("isCustomType", ctypes.wintypes.BOOL), # True if the device is a custom type. + ("isRack", ctypes.wintypes.BOOL), # True if the device is a rack. + ("maxChannels", ctypes.c_short) # Defines the number of channels available in this device. + ] + + +__dll.TLI_GetDeviceInfo.argtypes = [ctypes.c_char_p, ctypes.POINTER(TLI_DeviceInfo)] +__dll.TLI_GetDeviceInfo.restype = ctypes.c_short + + +def TLI_GetDeviceInfo(serial_no): + """Get the device information from the USB port. + The Device Info is read from the USB port not from the device itself. + Parmeters + --------- + serial_number : str + Serial number of Thorlabs Kinesis Inertial Motor (KIM) device. + Returns + ------- + TLI_DeviceInfo + The device information. + """ + + info = TLI_DeviceInfo() + + __dll.TLI_GetDeviceInfo(serial_no.encode(CODING), ctypes.byref(info)) # 1 if successful, 0 if not + + return info + +# ---------------------------------- + +class FF_Positions(IntEnum): + FF_PositionError = 0 # 0: - self.ROIlimits = matches[0] - else: - self.ROIlimits = self.ROIlimitsDefault + self.ROIlimits = self.ROIlimitlist.get(self.sensor_type,self.ROIlimitlist['UI324x']) # work out the camera base parameters for this sensortype - matches = [self.BaseProps[st] for st in self.BaseProps.keys() - if self.sensortype.startswith(st)] - if len(matches) > 0: - self.baseProps = matches[0] - else: - self.baseProps= self.BaseProps['default'] + self.baseProps = self.BaseProps.get(self.sensor_type,self.BaseProps['default']) #------------------- #Do initial setup with a whole bunch of settings I've arbitrarily decided are @@ -274,24 +245,7 @@ def __init__(self, boardNum=0, nbits = 8, isDeviceID = False): self.binning=False #binning flag - binning is off self.binX=1 #1x1 self.binY=1 - - self.background = None - self.flatfield = None - self.flat = None - self.dark = None - - #load flatfield (if present) - calpath = nameUtils.getCalibrationDir(self.serialNum) - ffname = os.path.join(calpath, 'flatfield.npy') - if os.path.exists(ffname): - self.flatfield = np.load(ffname).squeeze() - self.flat = self.flatfield - - darkname = os.path.join(calpath, 'dark.npy') - if os.path.exists(darkname): - self.dark = np.load(darkname).squeeze() - self.background = self.dark - + self.SetROI(0,0, self.CCDSize[0],self.CCDSize[1]) #self.ROIx=(1,self.CCDSize[0]) #self.ROIy=(1,self.CCDSize[1]) @@ -308,6 +262,14 @@ def __init__(self, boardNum=0, nbits = 8, isDeviceID = False): self.Init() self.SetIntegTime(.1) + + + @property + def noise_properties(self): + return {'ElectronsPerCount': self.baseProps['ElectronsPerCount']/self.GetGainFactor(), + 'ReadNoise': self.baseProps['ReadNoise'], + 'ADOffset': self.baseProps['ADOffset'], + 'SaturationThreshold': 2 ** self.nbits - 1} def errcheck(self,value,msg,fatal=True): if not value == uc480.IS_SUCCESS: @@ -460,6 +422,16 @@ def SetIntegTime(self, iTime): def GetIntegTime(self): return self.expTime + def GetCycleTime(self): + """ + Get camera cycle time (1/fps) in seconds (float) + + Returns + ------- + float + Camera cycle time (seconds) + """ + return 1 / self.GetFPS() def GetCCDWidth(self): return self.CCDSize[0] @@ -599,12 +571,6 @@ def SetROI(self, x1,y1,x2,y2): if not ret == 0: raise RuntimeError('Error setting ROI: %d: %s' % GetError(self.boardHandle)) - if not self.flatfield is None: - self.flat = self.flatfield[x1:x2, y1:y2] - - if not self.dark is None: - self.background = self.dark[x1:x2, y1:y2] - # we apparently have to set the integration time explicitly after a call to change the AOI # not sure if this is only for some IDS cameras or applies to all of them if self.GetIntegTime() is not None: @@ -626,7 +592,7 @@ def StartExposure(self): - eventLog.logEvent('StartAq', '') + self._log_exposure_start() if self._cont_mode: ret = uc480.CALL('CaptureVideo', self.boardHandle, uc480.IS_DONT_WAIT) else: @@ -657,12 +623,6 @@ def ExtractColor(self, chSlice, mode): #chSlice[:] = self.transferBuffer[:].T #.reshape(chSlice.shape) chSlice[:] = buf.T - if (not self.background is None) and self.background.shape == chSlice.shape: - chSlice[:] = (chSlice - np.minimum(chSlice, self.background))[:] - - if (not self.flat is None) and self.flat.shape == chSlice.shape: - chSlice[:] = (chSlice*self.flat).astype('uint16')[:] - #ret = uc480.CALL('UnlockSeqBuf', self.boardHandle, uc480.IS_IGNORE_PARAMETER, pData) if not self.freeBuffers is None: @@ -677,12 +637,6 @@ def StopAq(self): def SetCCDTemp(self, temp): pass - def SetEMGain(self, gain): - raise NotImplementedError() - - def GetEMGain(self): - return self.EMGain - def SetGainBoost(self, on): if on: uc480.CALL('SetGainBoost', self.boardHandle, uc480.IS_SET_GAINBOOST_ON) @@ -735,60 +689,7 @@ def GetSerialNumber(self): return self.serialNum def GetHeadModel(self): - return self.sensortype - - def GetElectronsPerCount(self): - return (self.baseProps['ElectronsPerCount']/self.GetGainFactor()) - - def GetReadNoise(self): # readnoise in e- - return (self.baseProps['ReadNoise']) - - def GetADOffset(self): - return self.baseProps['ADOffset'] - - @property - def noise_properties(self): - return {'ElectronsPerCount': self.GetElectronsPerCount(), - 'ReadNoise': self.GetReadNoise(), - 'ADOffset': self.GetADOffset()} - - def GenStartMetadata(self, mdh): - if self.active: #we are active -> write metadata - self.GetStatus() - - mdh.setEntry('Camera.Name', 'UC480-UEYE') - mdh.setEntry('Camera.Model', self.GetHeadModel()) - mdh.setEntry('Camera.SerialNumber', self.GetSerialNumber()) - - mdh.setEntry('Camera.IntegrationTime', self.GetIntegTime()) - mdh.setEntry('Camera.CycleTime', 1./self.GetFPS()) - - mdh.setEntry('Camera.HardwareGain', self.GetGain()) - mdh.setEntry('Camera.HardwareGainFactor', self.GetGainFactor()) - mdh.setEntry('Camera.ElectronsPerCount', self.GetElectronsPerCount()) - mdh.setEntry('Camera.ADOffset', self.GetADOffset()) - mdh.setEntry('Camera.ReadNoise',self.GetReadNoise()) # in units of e- - mdh.setEntry('Camera.NoiseFactor', 1.0) - - mdh.setEntry('Camera.SensorWidth',self.GetCCDWidth()) - mdh.setEntry('Camera.SensorHeight',self.GetCCDHeight()) - mdh.setEntry('Camera.TrueEMGain', 1) - - #mdh.setEntry('Camera.ROIPosX', self.GetROIX1()) - #mdh.setEntry('Camera.ROIPosY', self.GetROIY1()) - - x1, y1, x2, y2 = self.GetROI() - mdh.setEntry('Camera.ROIOriginX', x1) - mdh.setEntry('Camera.ROIOriginY', y1) - mdh.setEntry('Camera.ROIWidth', x2 - x1) - mdh.setEntry('Camera.ROIHeight', y2 - y1) - #mdh.setEntry('Camera.StartCCDTemp', self.GetCCDTemp()) - - check_mapexists(mdh,type='dark') - check_mapexists(mdh,type='variance') - check_mapexists(mdh,type='flatfield') - - def __del__(self): - if self.initialised: - self.Shutdown() + return self._sensor_name + def GetName(self): + return 'uc480-ueye' diff --git a/PYME/Acquire/Hardware/uc480/uc480.py b/PYME/Acquire/Hardware/uc480/uc480.py index 58084330d..7e2a68cfa 100644 --- a/PYME/Acquire/Hardware/uc480/uc480.py +++ b/PYME/Acquire/Hardware/uc480/uc480.py @@ -140,10 +140,22 @@ def loadLibrary(cameratype='uc480'): #libuc480 = ctypes.cdll.LoadLibrary(lib) if plat.startswith('Windows'): if cameratype=='uc480': + try: libuc480 = ctypes.WinDLL('uc480_64') - print("loading uc480_64") + except OSError: + # see https://stackoverflow.com/questions/59330863/cant-import-dll-module-in-python + # winmode=0 enforces windows default dll search mechanism including searching the path set + # necessary since python 3.8.x + libuc480 = ctypes.WinDLL('uc480_64',winmode=0) + print("loading uc480_64") elif cameratype=='ueye': + try: libuc480 = ctypes.WinDLL('ueye_api_64') + except OSError: + # see https://stackoverflow.com/questions/59330863/cant-import-dll-module-in-python + # winmode=0 enforces windows default dll search mechanism including searching the path set + # necessary since python 3.8.x + libuc480 = ctypes.WinDLL('ueye_api_64',winmode=0) print("loading ueye_api_64") else: raise RuntimeError("unknown camera type") diff --git a/PYME/Acquire/Hardware/uc480/ucCamControlFrame.py b/PYME/Acquire/Hardware/uc480/ucCamControlFrame.py index 683fba6ae..313b7c92f 100644 --- a/PYME/Acquire/Hardware/uc480/ucCamControlFrame.py +++ b/PYME/Acquire/Hardware/uc480/ucCamControlFrame.py @@ -41,15 +41,15 @@ def _init_ctrls(self, prnt): self.l = wx.StaticText(self, -1, '%3.1f'%(100.0*self.cam.GetGain()/100)+'%') #ideally this should be self.cam.GetGain()/self.cam.MaxGain, where MaxGain is set to 100 for the specific camera. hsizer.Add(self.l, 0, wx.ALL, 2) self.sl = wx.Slider(self, -1, 100.0*self.cam.GetGain()/100, 0, 100, size=wx.Size(150,-1),style=wx.SL_HORIZONTAL | wx.SL_HORIZONTAL | wx.SL_AUTOTICKS ) - self.sl.SetTickFreq(10,1) - wx.EVT_SCROLL(self,self.onSlide) + self.sl.SetTickFreq(10) + self.Bind(wx.EVT_SCROLL,self.onSlide) hsizer.Add(self.sl, 1, wx.ALL|wx.EXPAND, 2) ucGain.Add(hsizer, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL, 0) self.stGainFactor = wx.StaticText(self, -1, 'Gain Factor = %3.2f' % self.cam.GetGainFactor()) ucGain.Add(self.stGainFactor, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL, 0) - self.stepc = wx.StaticText(self, -1, 'Electrons/Count = %3.2f' % (7.97/self.cam.GetGainFactor())) + self.stepc = wx.StaticText(self, -1, 'Electrons/Count = %3.2f' % (self.cam.noise_properties['ElectronsPerCount'])) ucGain.Add(self.stepc, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL, 0) @@ -70,7 +70,7 @@ def onSlide(self, event): self.cam.SetGain(int(round(sl.GetValue()/100.0*100))) self.l.SetLabel('%3.1f'%(100.0*self.cam.GetGain()/100)+'%') self.stGainFactor.SetLabel('Gain Factor = %3.2f' % self.cam.GetGainFactor()) - self.stepc.SetLabel('Electrons/Count = %3.2f' % self.cam.GetElectronsPerCount()) + self.stepc.SetLabel('Electrons/Count = %3.2f' % self.cam.noise_properties['ElectronsPerCount']) finally: self.sliding = False @@ -79,5 +79,5 @@ def update(self): # only needed if we want automatic update for the panel. self.sl.SetValue(round(self.cam.GetGain()/100)) self.l.SetLabel(str(self.cam.GetGain()/100)+'%%') self.stGainFactor.SetLabel('Gain Factor = %3.2f' % self.cam.GetGainFactor()) - self.stepc.SetLabel('Electrons/Count = %3.2f' % self.cam.GetElectronsPerCount()) + self.stepc.SetLabel('Electrons/Count = %3.2f' % self.cam.noise_properties['ElectronsPerCount']) diff --git a/PYME/Acquire/Hardware/ueye.py b/PYME/Acquire/Hardware/ueye.py new file mode 100644 index 000000000..4c612415d --- /dev/null +++ b/PYME/Acquire/Hardware/ueye.py @@ -0,0 +1,702 @@ +''' +Alternative bindings for IDS ueye cameras using the pyueye module. + +See also the uc480 module which talks directly to the ueye DLLs without an +intermediate python shim. The uc480 implementation has been widely used with +previous ueye driver versions (e.g. <4.93). At present, there might be issues +using uc480 with the most recent versions of the ueye SDK, hence this module. +This module works with ueye driver 4.96.1, and pyueye 4.96.952. Pyueye handles +some boiler plate ctypes code for us, but in the future we may call the dll +directly as in uc480. + +# NOTE to developers: pyueye typing is somewhat obnoxious. Some oddities with +# ctypes pointers (testing for equality of a factory-produced class) makes it +# safer to use their pointer cast method, and their 'extra functionality' ctypes + +Application notes for specific cameras from the SDK: +327x: + Triggering: The internal sensor delay is about 2-3 lines when triggering. + The line period depends on the selected pixel clock. The higher the + pixel clock, the smaller the line period is. In overlapping trigger + mode, the camera timestamp may be overwritten if the maximum exposure + time is used. In this case, reduce the exposure time by approx. 1% + +''' +from PYME.Acquire.Hardware.Camera import Camera, MultiviewCameraMixin +from pyueye import ueye +import ctypes +import threading +import logging +import queue +import numpy as np +import time +from PYME.Acquire import eventLog as event_log + + +logger = logging.getLogger(__name__) + +def GetError(camera_handle): + error = ctypes.c_int() + error_message = ctypes.c_char_p() + ueye.is_GetError(camera_handle, error, error_message) + return error.value, error_message.value + +ROI_LIMITS = { + 'UI306x' : { + 'xmin' : 96, + 'xstep' : 8, + 'ymin' : 32, + 'ystep' : 2 + }, + 'UI327x' : { + 'xmin' : 256, + 'xstep' : 8, + 'ymin' : 2, + 'ystep' : 2 + }, + 'UI324x' : { + 'xmin' : 16, + 'xstep' : 4, + 'ymin' : 4, + 'ystep' : 2 + }, + 'UI124x' : { + 'xmin' : 16, + 'xstep' : 4, + 'ymin' : 4, + 'ystep' : 2 + } +} + +# this info is partly from the IDS datasheets that one can request for each camera model +BaseProps = { + 'UI306x' : { + # from Steve Hearn (IDS) + # The default gain of that camera is the absolute minimum gain the camera can deliver. + # All other gain factors are higher than that. This means, the system gain of 0.125 DN + # per electron, as specified in the camera test sheet, is the smallest possible value. + 'ElectronsPerCount' : 7.97, + 'ReadNoise' : 6.0, + 'ADOffset' : 10 + }, + 'UI327x' : { # calibrated by AESB 2022/04 on S/N 4103211322 running in 12 bit mode, 100 ms integration time. + 'ElectronsPerCount': 2.706, # fitted from Var [ADU^2] vs Mean [ADU] plot (1/slope) + 'ReadNoise' : 2.425, # median of 100 ms varmap from gen_sCMOS_maps.py is 5.883 e-^2. ReadNoise is sigma, i.e. sqrt of that + 'ADOffset' : 7.67, # median of 100 ms dark map from gen_sCMOS_maps.py + }, + 'default' : { # fairly arbitrary values + 'ElectronsPerCount' : 10, + 'ReadNoise' : 20, + 'ADOffset' : 10 + } +} + + +class UEyeCamera(Camera): + def __init__(self, device_number=0, nbits=8): + Camera.__init__(self) + + self.initialized = False + + if nbits not in (8, 10, 12): + raise RuntimeError('Supporting only 8, 10 or 12 bit depth, requested %d bit' % (nbits)) + self.nbits = nbits + + self.h = ueye.HIDS(device_number) + + self.check_success(ueye.is_InitCamera(self.h, None)) + self.initialized = True + + # get serial number + cam_info = ueye.CAMINFO() + self.check_success(ueye.is_GetCameraInfo(self.h, cam_info)) + self._serno = cam_info.SerNo.decode() + + # get chip size + sensor_info = ueye.SENSORINFO() + self.check_success(ueye.is_GetSensorInfo(self.h, sensor_info)) + + self._chip_size = (int(sensor_info.nMaxWidth), int(sensor_info.nMaxHeight)) # convert from c_uint, otherwise trips up JSON dumps + self.sensor_type = sensor_info.strSensorName.decode().split('x')[0] + 'x' + + # work out the camera base parameters for this sensortype + self.baseProps = BaseProps.get(self.sensor_type,BaseProps['default']) + + # note that some uEye cameras have a sensor size which exceeds the 'usable' ROI + self.SetROI(0, 0, self._chip_size[0], self._chip_size[1]) + + self.check_success(ueye.is_SetColorMode(self.h, getattr(ueye, + 'IS_CM_MONO%d' % self.nbits))) + + # turn off hardware gamma if supported. + hw_gamma = ueye.is_SetHardwareGamma(self.h, ueye.int(ueye.IS_GET_HW_SUPPORTED_GAMMA)) + if hw_gamma == ueye.IS_SET_HW_GAMMA_ON: # SetHardwareGamma returns IS_SET_HW_GAMMA_ON (1) if supported + logger.debug('model supports hardware gamma correction, turning it off') + self.check_success(ueye.is_SetHardwareGamma(self.h, ueye.IS_SET_HW_GAMMA_OFF)) + + self.SetAcquisitionMode(self.MODE_CONTINUOUS) + self._buffers = [] + self.full_buffers = queue.Queue() + self.free_buffers = None + + self.n_full = 0 + + self.n_accum = 1 + self.n_accum_current = 0 + + self.SetIntegTime(0.1) + self.Init() + + def check_success(self, function_return): + if function_return != ueye.IS_SUCCESS: + error, message = GetError(self.h) + raise RuntimeError('Error %d: %s' % (error, message)) + + def Init(self): + self._poll = False + self.poll_loop_active = True + self.poll_thread = threading.Thread(target=self._poll_loop) + self.poll_thread.start() + + def GetCCDWidth(self): + return self._chip_size[0] + + def GetCCDHeight(self): + return self._chip_size[1] + + def InitBuffers(self, n_buffers=50, n_accum_buffers=50): + for ind in range(n_buffers): + data = ueye.c_mem_p() + buffer_id = ueye.int() + + if self.nbits == 8: + bitsperpix = 8 + bufferdtype = np.uint8 + else: # 10 & 12 bits + bitsperpix = 16 + bufferdtype = np.uint16 + + self.check_success(ueye.is_AllocImageMem(self.h, self.GetPicWidth(), self.GetPicHeight(), bitsperpix, data, buffer_id)) + self.check_success(ueye.is_AddToSequence(self.h, data, buffer_id)) + + self._buffers.append((buffer_id, data)) + + self.check_success(ueye.is_ImageQueue(self.h, ueye.IS_IMAGE_QUEUE_CMD_INIT, None, ctypes.c_int(0))) + + self.curr_height, self.curr_width = self.GetPicHeight(), self.GetPicWidth() + self.transfer_buffer_size = self.curr_height * self.curr_width * bufferdtype().itemsize + self.transfer_buffer_dtype = bufferdtype + self.transfer_buffer = ctypes.create_string_buffer(self.transfer_buffer_size) + self.transfer_buffer_memory_v = ueye.char() + self.transfer_buffer_memory = ueye._pointer_cast(self.transfer_buffer_memory_v, ueye.char_p) + self.transfer_buffer_id = ueye.int() + self.wait_buffer = ueye.IMAGEQUEUEWAITBUFFER() + self.wait_buffer.timeout = ueye.uint(1000) + + self.wait_buffer.pnMemId = ueye._pointer_cast(self.transfer_buffer_id, ctypes.POINTER(ueye.int)) + self.wait_buffer.ppcMem = ueye._pointer_cast(self.transfer_buffer_memory, ctypes.POINTER(ueye.char_p)) + + self.free_buffers = queue.Queue() + # CS: we leave this as uint16 regardless of 8 or 12 bits for now as accumulation + # of the underlying 12 bit data should be ok (but maybe not?) + for ind in range(n_accum_buffers): + self.free_buffers.put(np.zeros([self.GetPicHeight(), + self.GetPicWidth()], np.uint16)) + self.accum_buffer = self.free_buffers.get() + self.n_accum_current = 0 + self._poll = True + + def DestroyBuffers(self): + self._poll = False # already in StopAq, can probably remove + self.n_full = 0 + # exit the image queue + self.check_success(ueye.is_ImageQueue(self.h, ueye.IS_IMAGE_QUEUE_CMD_EXIT, None, ueye.int(0))) + + # remove all image memories from the sequence list we created + self.check_success(ueye.is_ClearSequence(self.h)) + + # free up each image memory we allocated on the device + while len(self._buffers) > 0: + buffer_id, data = self._buffers.pop() + self.check_success(ueye.is_FreeImageMem(self.h, data, buffer_id)) + + # destroy free buffers and remove queue of full ones + self.free_buffers = None + while not self.full_buffers.empty(): + try: + self.full_buffers.get_nowait() + except queue.Empty: + pass + + def StartExposure(self): + logger.debug('StartAq') + if self._poll: + # stop, we'll allocate buffers and restart + self.StopAq() + # allocate at least 2 seconds of buffers + buffer_size = int(max(2 * self.GetFPS(), 50)) + self.InitBuffers(buffer_size, buffer_size) + + self._log_exposure_start() + if self._cont_mode: + self.check_success(ueye.is_CaptureVideo(self.h, ueye.IS_DONT_WAIT)) + else: + self.check_success(ueye.is_FreezeVideo(self.h, ueye.IS_DONT_WAIT)) + return 0 + + def SetAcquisitionMode(self, mode): + if mode == self.MODE_SINGLE_SHOT: + self._cont_mode = False + else: + self._cont_mode = True + + def GetAcquisitionMode(self): + if self._cont_mode: + return self.MODE_CONTINUOUS + else: + return self.MODE_SINGLE_SHOT + + def ExpReady(self): + return (self.full_buffers is not None) and (self.n_full > 0) + + def ExtractColor(self, ch_slice, mode): + # get nowait to hard-throw an Empty error if we've entered this method + # and we shouldn't have + buf = self.full_buffers.get_nowait() + ch_slice[:] = buf.T + if self.free_buffers is not None: + # recycle buffer + self.free_buffers.put(buf) + self.n_full -= 1 + + def StopAq(self): + self._poll = False + # cancel any ongoing waits (e.g. shutdown) + self.check_success(ueye.is_ImageQueue(self.h, ueye.IS_IMAGE_QUEUE_CMD_CANCEL_WAIT, None, ueye.int(0))) + self.check_success(ueye.is_StopLiveVideo(self.h, ueye.IS_WAIT))# ueye.IS_FORCE_VIDEO_STOP)) + self.DestroyBuffers() + + def _poll_buffer(self): + try: + # Query the ID/location of earlier frame in the ring buffer, or wait + # for the next one. Will fill wait_buffer.nMemId and pcMem + self.check_success(ueye.is_ImageQueue(self.h, ueye.IS_IMAGE_QUEUE_CMD_WAIT, + self.wait_buffer, ueye.sizeof(self.wait_buffer))) + # self.check_success(ueye.is_CopyImageMem(self.h, self.wait_buffer.ppcMem, self.transfer_buffer_id, + # self.transfer_buffer))#.ctypes.data_as(ctypes.POINTER(ueye.char_p)))) + # self.transfer_buffer.ctypes.data_as(ctypes.POINTER(ctypes.c_uint16)))) + ctypes.memmove(self.transfer_buffer, self.wait_buffer.ppcMem.contents, self.transfer_buffer_size) + arr = np.frombuffer(self.transfer_buffer, dtype=self.transfer_buffer_dtype) + arr = arr.reshape((self.curr_height, self.curr_width)) + except RuntimeError as e: + if 'Error %d' % ueye.IS_OPERATION_ABORTED in str(e): + # we are shutting down the camera, let the other thread handle + # clean-up + logger.debug('ImageQueue wait canceled, returning') + return + logger.error(e) + try: + self.check_success(ueye.is_UnlockSeqBuf(self.h, self.transfer_buffer_id, None)) + except Exception as e: + logger.error(e) + finally: + return + + if self.n_accum_current == 0: + self.accum_buffer[:] = arr + else: + self.accum_buffer[:] = self.accum_buffer + arr + self.n_accum_current += 1 + + self.check_success(ueye.is_UnlockSeqBuf(self.h, self.transfer_buffer_id, None)) + + if self.n_accum_current >= self.n_accum: + self.full_buffers.put(self.accum_buffer) + self.accum_buffer = self.free_buffers.get() + self.n_accum_current = 0 + self.n_full += 1 + + def _poll_loop(self): + while self.poll_loop_active: + if self._poll: # only poll if an acquisition is running + try: + self._poll_buffer() + except Exception as e: + logger.exception(str(e)) + else: + time.sleep(.05) + + def CamReady(self): + """ + Returns true if the camera is ready (initialized) not really used for + anything, but might still be checked. + + Returns + ------- + bool + Is the camera ready? + """ + + return self.initialized + + def SetIntegTime(self, integ_time): + """ + Sets the exposure time in s. Currently assumes that we will want to go as fast as possible at this exposure time + and also sets the frame rate to match. + + Parameters + ---------- + iTime : float + Exposure time in s + + Returns + ------- + None + + See Also + -------- + GetIntegTime + """ + new_fps = ueye.double() + self.check_success(ueye.is_SetFrameRate(self.h, 1 / integ_time, + new_fps)) + # by default, set exposure time to max for this frame rate + # "If 0 is passed, the exposure time is set to the maximum value of 1/frame rate." + exposure = ueye.double(0) + self.check_success(ueye.is_Exposure(self.h, + ueye.IS_EXPOSURE_CMD_SET_EXPOSURE, + exposure, ueye.sizeof(exposure))) + + def GetIntegTime(self): + """ + Get Camera object integration time. + + Returns + ------- + float + The exposure time in s + + See Also + -------- + SetIntegTime + """ + exposure = ueye.double() + self.check_success(ueye.is_Exposure(self.h, + ueye.IS_EXPOSURE_CMD_GET_EXPOSURE, + exposure, ueye.sizeof(exposure))) + return exposure.value / 1e3 + + + def GetCycleTime(self): + """ + Get camera cycle time (1/fps) in seconds (float) + + Returns + ------- + float + Camera cycle time (seconds) + """ + return 1 / self.GetFPS() + + def GetPicWidth(self): + """ + Returns the width (in pixels) of the currently selected ROI. + + Returns + ------- + int + Width of ROI (pixels) + """ + x0, _, x1, _ = self.GetROI() + return x1 - x0 + + def GetPicHeight(self): + """ + Returns the height (in pixels) of the currently selected ROI + + Returns + ------- + int + Height of ROI (pixels) + """ + _, y0, _, y1 = self.GetROI() + return y1 - y0 + + def SetROI(self, x1, y1, x2, y2): + """ + Set the ROI via coordinates (as opposed to via an index). + + Parameters + ---------- + x1 : int + Left x-coordinate, zero-indexed + y1 : int + Top y-coordinate, zero-indexed + x2 : int + Right x-coordinate, (excluded from ROI) + y2 : int + Bottom y-coordinate, (excluded from ROI) + + Returns + ------- + None + + + """ + logger.debug('setting ROI: %d, %d, %d, %d' % (x1, y1, x2, y2)) + limits = ROI_LIMITS[self.sensor_type] + x1 = max(x1, limits['xmin']) + y1 = max(y1, limits['ymin']) + x2 = min(x2, self.GetCCDWidth()) + y2 = min(y2, self.GetCCDHeight()) + + x_change = (x2 - x1) % limits['xstep'] + y_change = (y2 - y1) % limits['ystep'] + x2 -= x_change + y2 -= y_change + logger.debug('adjusted ROI: %d, %d, %d, %d' % (x1, y1, x2, y2)) + + aoi = ueye.IS_RECT() + aoi.s32X = ueye.int(x1) + aoi.s32Y = ueye.int(y1) + aoi.s32Width = ueye.int(x2 - x1) + aoi.s32Height = ueye.int(y2 - y1) + + self.check_success(ueye.is_AOI(self.h, ueye.IS_AOI_IMAGE_SET_AOI, aoi, + ueye.sizeof(aoi))) + # have to set the integration time explicitly after changing AOI + self.SetIntegTime(self.GetIntegTime()) + + + def GetROI(self): + """ + + Returns + ------- + + The ROI, [x1, y1, x2, y2] in the numpy convention used by SetROI + + """ + aoi = ueye.IS_RECT() + self.check_success(ueye.is_AOI(self.h, ueye.IS_AOI_IMAGE_GET_AOI, aoi, + ueye.sizeof(aoi))) + x0, y0 = aoi.s32X.value, aoi.s32Y.value + return x0, y0, x0 + aoi.s32Width.value, y0 + aoi.s32Height.value + + + def GetNumImsBuffered(self): + """ + Return the number of images in the buffer. + + Returns + ------- + int + Number of images in buffer + """ + return self.n_full + + def GetBufferSize(self): + """ + Return the total size of the buffer (in images). + + Returns + ------- + int + Number of images that can be stored in the buffer. + """ + if self._poll: + return len(self._buffers) + else: + # if we aren't polling, spoof infinitely large buffer so we don't + # flag a buffer overflow while we e.g. rebuild the buffers. This + # makes no functional difference for us, but avoids a spurious + # warning from frameWrangler about the buffer overflowing. + return np.iinfo(np.int32).max + + def GetCCDTemp(self): + di = self._GetDeviceInfo() + tword = di.infoDevHeartbeat.wTemperature.value + # from IDS docs: + # wTemperature + # Camera temperature in degrees Celsius + # Bits 15: algebraic sign + # Bits 14...11: filled according to algebraic sign + # Bits 10...4: temperature (places before the decimal point) + # Bits 3...0: temperature (places after the decimal point) + tempfloat = 1.0*(tword >> 4 & 0b1111111) + 0.1 * (tword & 0b1111) + if (tword >> 15): + tempfloat = -1.0 * tempfloat + return tempfloat + + @property + def noise_properties(self): + try: # try and get noise properties following the current convention + return super().noise_properties + except RuntimeError: # fall back loudly on "base properties" + logger.exception('Noise properties not set up for this camera, falling back on values which are likely wrong') + return {'ElectronsPerCount': self.baseProps['ElectronsPerCount']/self.GetGainFactor(), + 'ReadNoise': self.baseProps['ReadNoise'], + 'ADOffset': self.baseProps['ADOffset'], + 'SaturationThreshold': 2 ** self.nbits - 1} + + @property + def _gain_mode(self): + return '%d-bit' % self.nbits + + + # @property + # def noise_properties(self): + # """ + + # Returns + # ------- + + # a dictionary with the following entries: + + # 'ReadNoise' : camera read noise as a standard deviation in units of photoelectrons (e-) + # 'ElectronsPerCount' : AD conversion factor - how many electrons per ADU + # 'NoiseFactor' : excess (multiplicative) noise factor 1.44 for EMCCD, 1 for standard CCD/sCMOS. See + # doi: 10.1109/TED.2003.813462 + + # and optionally + # 'ADOffset' : the dark level (in ADU) + # 'DefaultEMGain' : a sensible EM gain setting to use for localization recording + # 'SaturationThreshold' : the full well capacity (in ADU) + + # """ + + # return { + # 'ReadNoise': 1, + # 'ElectronsPerCount': 1, + # 'NoiseFactor': 1, + # 'ADOffset': 0, + # 'SaturationThreshold': 2 ** self.nbits - 1 + # } + + def GetFPS(self): + """ + Get the camera frame rate in frames per second (float). + + Returns + ------- + float + Camera frame rate (frames per second) + """ + fps = ueye.double() + self.check_success(ueye.is_GetFramesPerSecond(self.h, fps)) + return fps.value + + def GetSerialNumber(self): + return self._serno + + def GetName(self): + return 'ueye-camera' + + def GetHeadModel(self): + return self.sensor_type + + def SetGain(self, gain=100): + self.check_success(ueye.is_SetHardwareGain(self.h, gain, ueye.IS_IGNORE_PARAMETER, + ueye.IS_IGNORE_PARAMETER, ueye.IS_IGNORE_PARAMETER)) + + def GetGain(self): + ret = ueye.is_SetHardwareGain(self.h, ueye.IS_GET_MASTER_GAIN, + ueye.IS_IGNORE_PARAMETER, ueye.IS_IGNORE_PARAMETER, ueye.IS_IGNORE_PARAMETER) + return ret + + def GetGainFactor(self): + gain = self.GetGain() + ret = ueye.is_SetHWGainFactor(self.h, ueye.IS_INQUIRE_MASTER_GAIN_FACTOR, gain) + return 0.01*ret + + def SetOutputTrigger(self, mode, delay=0, width=0.0001, positive=True): + """ + Set output trigger of the camera. Currently hard-coded to use GPIO1 + + Parameters + ---------- + mode : str + Currently supported modes include: + low: constant TTL low + high: constant TTL high + flash: goes TTL high during exposure. If 0 is passed, the flash + output will be active until the end of the exposure time. + With global start shutter this is the time until the end of + exposure for the first row. + delay : float, optional + delay after trigger event, in seconds, to emit TTL high (assuming + posiive polarity), by default 0 s. + width : float, optional + TTL high pulse width, in seconds, by default 0.0001 s, or 0.1 ms + positive : bool, optional + CURRENTLY IGNORED Sets polarity of the output trigger to positive + (True) or negative (False). True, by default. + + """ + # set GPIO1 to be output flash + m = ueye.int(ueye.IO_FLASH_MODE_GPIO_1) + self.check_success(ueye.is_IO(self.h, + ueye.IS_IO_CMD_FLASH_SET_MODE, + m, ueye.sizeof(m))) + + if mode == 'high': + mode = ueye.int(ueye.IO_FLASH_MODE_CONSTANT_HIGH) + self.check_success(ueye.is_IO(self.h, + ueye.IS_IO_CMD_FLASH_SET_MODE, + mode, ueye.sizeof(mode))) + return + elif mode == 'low': + mode = ueye.int(ueye.IO_FLASH_MODE_CONSTANT_LOW) + self.check_success(ueye.is_IO(self.h, + ueye.IS_IO_CMD_FLASH_SET_MODE, + mode, ueye.sizeof(mode))) + return + + if mode =='flash': + mode = ueye.int(ueye.IO_FLASH_MODE_FREERUN_HI_ACTIVE) + self.check_success(ueye.is_IO(self.h, + ueye.IS_IO_CMD_FLASH_SET_MODE, + mode, ueye.sizeof(mode))) + else: + raise RuntimeError('Unsupported output trigger mode: %s' % mode) + + fp = ueye.IO_FLASH_PARAMS() + fp.s32Delay = ueye.c_int(int(delay * 1e6)) # [s] -> [us] + fp.u32Duration = ueye.c_uint(int(width * 1e6)) # [s] -> [us] + self.check_success(ueye.is_IO(self.h, + ueye.IS_IO_CMD_FLASH_SET_PARAMS, + fp, ueye.sizeof(fp))) + + #### Some extra functions for this camera + + def _GetDeviceInfo(self): + dev_info = ueye.IS_DEVICE_INFO() + self.check_success(ueye.is_DeviceInfo(self.h,ueye.IS_DEVICE_INFO_CMD_GET_DEVICE_INFO, + dev_info,ueye.sizeof(dev_info))) + return dev_info + + def GetGlobalFlashSettings(self): + """Query current global 'flash', i.e. output trigger settings + + Returns + ------- + delay: float + [s] + width: float + duration of TTL in [s] + + """ + fp = ueye.IO_FLASH_PARAMS() + self.check_success(ueye.is_IO(self.h, + ueye.IS_IO_CMD_FLASH_GET_GLOBAL_PARAMS, + fp, ueye.sizeof(fp))) + return fp.s32Delay.value / 1e6, fp.u32Duration.value / 1e6 + + +#TODO - replace MultiviewCameraMixin with a Multiview wrapper so that we don't need to have explicit multiview versions of all cameras. +class MultiviewUEye(MultiviewCameraMixin, UEyeCamera): + def __init__(self, camNum, multiview_info, nbits=8): + UEyeCamera.__init__(self, camNum, nbits) + # default to the whole chip + default_roi = dict(xi=0, xf=int(self._chip_size[0]), yi=0, yf=int(self._chip_size[1])) + MultiviewCameraMixin.__init__(self, multiview_info, default_roi, UEyeCamera) diff --git a/PYME/Acquire/Loft/FrSpool.py b/PYME/Acquire/Loft/FrSpool.py deleted file mode 100755 index e200c7184..000000000 --- a/PYME/Acquire/Loft/FrSpool.py +++ /dev/null @@ -1,212 +0,0 @@ -#!/usr/bin/python - -################## -# FrSpool.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#Boa:Frame:FrSpool - -import wx -import datetime -import win32api -import os - -from PYME.Acquire import Spooler - -def create(parent): - return FrSpool(parent) - -[wxID_FRSPOOL, wxID_FRSPOOLBSETSPOOLDIR, wxID_FRSPOOLBSTARTSPOOL, - wxID_FRSPOOLBSTOPSPOOLING, wxID_FRSPOOLPANEL1, wxID_FRSPOOLSTATICBOX1, - wxID_FRSPOOLSTATICBOX2, wxID_FRSPOOLSTATICTEXT1, wxID_FRSPOOLSTNIMAGES, - wxID_FRSPOOLSTSPOOLDIRNAME, wxID_FRSPOOLSTSPOOLINGTO, - wxID_FRSPOOLTCSPOOLFILE, -] = [wx.NewId() for _init_ctrls in range(12)] - -def baseconvert(number,todigits): - x = number - - # create the result in base 'len(todigits)' - res="" - - if x == 0: - res=todigits[0] - - while x>0: - digit = x % len(todigits) - res = todigits[digit] + res - x /= len(todigits) - - return res - - -class FrSpool(wx.Frame): - def _init_ctrls(self, prnt): - # generated method, don't edit - wx.Frame.__init__(self, id=wxID_FRSPOOL, name='FrSpool', parent=prnt, - pos=wx.Point(543, 403), size=wx.Size(290, 240), - style=wx.DEFAULT_FRAME_STYLE, title='Spooling') - self.SetClientSize(wx.Size(282, 213)) - - self.panel1 = wx.Panel(id=wxID_FRSPOOLPANEL1, name='panel1', - parent=self, pos=wx.Point(0, 0), size=wx.Size(282, 213), - style=wx.TAB_TRAVERSAL) - - self.bStartSpool = wx.Button(id=wxID_FRSPOOLBSTARTSPOOL, - label='Start Spooling', name='bStartSpool', parent=self.panel1, - pos=wx.Point(186, 67), size=wx.Size(88, 23), style=0) - self.bStartSpool.Bind(wx.EVT_BUTTON, self.OnBStartSpoolButton, - id=wxID_FRSPOOLBSTARTSPOOL) - - self.staticBox1 = wx.StaticBox(id=wxID_FRSPOOLSTATICBOX1, - label='Spooling Progress', name='staticBox1', parent=self.panel1, - pos=wx.Point(7, 101), size=wx.Size(265, 104), style=0) - self.staticBox1.Enable(False) - - self.stSpoolingTo = wx.StaticText(id=wxID_FRSPOOLSTSPOOLINGTO, - label='Spooling to .....', name='stSpoolingTo', - parent=self.panel1, pos=wx.Point(26, 125), size=wx.Size(76, 13), - style=0) - self.stSpoolingTo.Enable(False) - - self.stNImages = wx.StaticText(id=wxID_FRSPOOLSTNIMAGES, - label='NNNNN images spooled in MM minutes', name='stNImages', - parent=self.panel1, pos=wx.Point(26, 149), size=wx.Size(181, 13), - style=0) - self.stNImages.Enable(False) - - self.bStopSpooling = wx.Button(id=wxID_FRSPOOLBSTOPSPOOLING, - label='Stop', name='bStopSpooling', parent=self.panel1, - pos=wx.Point(105, 173), size=wx.Size(75, 23), style=0) - self.bStopSpooling.Enable(False) - self.bStopSpooling.Bind(wx.EVT_BUTTON, self.OnBStopSpoolingButton, - id=wxID_FRSPOOLBSTOPSPOOLING) - - self.staticBox2 = wx.StaticBox(id=wxID_FRSPOOLSTATICBOX2, - label='Spool Directory', name='staticBox2', parent=self.panel1, - pos=wx.Point(8, 8), size=wx.Size(264, 48), style=0) - - self.stSpoolDirName = wx.StaticText(id=wxID_FRSPOOLSTSPOOLDIRNAME, - label='Save images in: Blah Blah', name='stSpoolDirName', - parent=self.panel1, pos=wx.Point(21, 28), size=wx.Size(136, 13), - style=0) - - self.bSetSpoolDir = wx.Button(id=wxID_FRSPOOLBSETSPOOLDIR, label='Set', - name='bSetSpoolDir', parent=self.panel1, pos=wx.Point(222, 23), - size=wx.Size(40, 23), style=0) - self.bSetSpoolDir.SetThemeEnabled(False) - self.bSetSpoolDir.Bind(wx.EVT_BUTTON, self.OnBSetSpoolDirButton, - id=wxID_FRSPOOLBSETSPOOLDIR) - - self.tcSpoolFile = wx.TextCtrl(id=wxID_FRSPOOLTCSPOOLFILE, - name='tcSpoolFile', parent=self.panel1, pos=wx.Point(81, 68), - size=wx.Size(100, 21), style=0, value='dd_mm_series_a') - self.tcSpoolFile.Bind(wx.EVT_TEXT, self.OnTcSpoolFileText, - id=wxID_FRSPOOLTCSPOOLFILE) - - self.staticText1 = wx.StaticText(id=wxID_FRSPOOLSTATICTEXT1, - label='Series name:', name='staticText1', parent=self.panel1, - pos=wx.Point(11, 72), size=wx.Size(66, 13), style=0) - - def __init__(self, parent, scope, defDir, defSeries='%(day)d_%(month)d_series'): - self._init_ctrls(parent) - self.scope = scope - - dtn = datetime.datetime.now() - - dateDict = {'username' : win32api.GetUserName(), 'day' : dtn.day, 'month' : dtn.month, 'year':dtn.year} - - self.dirname = defDir % dateDict - self.seriesStub = defSeries % dateDict - - self.seriesCounter = 0 - self.seriesName = self._GenSeriesName() - - self.stSpoolDirName.SetLabel(self.dirname) - self.tcSpoolFile.SetValue(self.seriesName) - - def _GenSeriesName(self): - return self.seriesStub + '_' + self._NumToAlph(self.seriesCounter) - - def _NumToAlph(self, num): - return baseconvert(num, 'ABCDEFGHIJKLMNOPQRSTUVXWYZ') - - - def OnBStartSpoolButton(self, event): - #fn = wx.FileSelector('Save spooled data as ...', default_extension='.log',wildcard='*.log') - #if not fn == '': #if the user cancelled - # self.spooler = Spooler.Spooler(self.scope, fn, self.scope.frameWrangler, self) - # self.bStartSpool.Enable(False) - # self.bStopSpooling.Enable(True) - # self.stSpoolingTo.Enable(True) - # self.stNImages.Enable(True) - # self.stSpoolingTo.SetLabel('Spooling to ' + fn) - # self.stNImages.SetLabel('0 images spooled in 0 minutes') - - fn = self.tcSpoolFile.GetValue() - - if fn == '': #sanity checking - wx.MessageBox('Please enter a series name', 'No series name given', wx.OK) - return #bail - - if not os.path.exists(self.dirname): - os.makedirs(self.dirname) - - if fn in os.listdir(self.dirname): #check to see if data with the same name exists - ans = wx.MessageBox('A series with the same name already exists ... overwrite?', 'Warning', wx.YES_NO) - if ans == wx.NO: - return #bail - - self.spooler = Spooler.Spooler(self.scope, self.dirname + fn + '.log', self.scope.frameWrangler, self) - self.bStartSpool.Enable(False) - self.bStopSpooling.Enable(True) - self.stSpoolingTo.Enable(True) - self.stNImages.Enable(True) - self.stSpoolingTo.SetLabel('Spooling to ' + fn) - self.stNImages.SetLabel('0 images spooled in 0 minutes') - - - def OnBStopSpoolingButton(self, event): - self.spooler.StopSpool() - self.bStartSpool.Enable(True) - self.bStopSpooling.Enable(False) - self.stSpoolingTo.Enable(False) - self.stNImages.Enable(False) - - self.seriesCounter +=1 - self.seriesName = self._GenSeriesName() - self.tcSpoolFile.SetValue(self.seriesName) - - def Tick(self): - dtn = datetime.datetime.now() - - dtt = dtn - self.spooler.dtStart - - self.stNImages.SetLabel('%d images spooled in %d seconds' % (self.spooler.imNum, dtt.seconds)) - - def OnBSetSpoolDirButton(self, event): - ndir = wx.DirSelector() - if not ndir == '': - self.dirname = ndir + os.sep - self.stSpoolDirName.SetLabel(self.dirname) - - def OnTcSpoolFileText(self, event): - event.Skip() - diff --git a/PYME/Acquire/Loft/KDFSpooler.py b/PYME/Acquire/Loft/KDFSpooler.py deleted file mode 100644 index 355320ed6..000000000 --- a/PYME/Acquire/Loft/KDFSpooler.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/python - -################## -# KDFSpooler.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import os -import logparser -import datetime - -class Spooler: - def __init__(self, scope, filename, acquisator, parent=None): - self.scope = scope - self.filename=filename - self.acq = acquisator - self.parent = parent - - self.dirname =filename[:-4] - os.mkdir(self.dirname) - - self.filestub = self.dirname.split(os.sep)[-1] - - self.imNum=0 - self.log = {} - self.doStartLog() - - self.acq.WantFrameNotification.append(self.Tick) - - self.spoolOn = True - - def StopSpool(self): - self.acq.WantFrameNotification.remove(self.Tick) - self.doStopLog() - self.writeLog() - self.spoolOn = False - - def Tick(self, caller): - fn = self.dirname + os.sep + self.filestub +'%05d.kdf' % self.imNum - caller.ds.SaveToFile(fn.encode()) - self.imNum += 1 - if not self.parent is None: - self.parent.Tick() - - def doStartLog(self): - if not 'GENERAL' in self.log.keys(): - self.log['GENERAL'] = {} - if not 'PIEZOS' in self.log.keys(): - self.log['PIEZOS'] = {} - if not 'CAMERA' in self.log.keys(): - self.log['CAMERA'] = {} - - for pz in self.scope.piezos: - self.log['PIEZOS']['%s_Pos' % pz[2]] = pz[0].GetPos(pz[1]) - - #self.log['STEPPER']['XPos'] = m_pDoc->Step.GetNullX(), - #m_pDoc->Step.GetPosX(), m_pDoc->Step.GetNullY(), m_pDoc->Step.GetPosY(), - #m_pDoc->Step.GetNullZ(), m_pDoc->Step.GetPosZ()); - self.scope.cam.GetStatus() - self.log['CAMERA']['Binning'] = self.scope.cam.GetHorizBin() - #m_pDoc->LogData.SetIntegTime(m_pDoc->Camera.GetIntegTime()); - if 'tKin' in dir(self.scope.cam): #check for Andor cam - self.log['CAMERA']['IntegrationTime'] = self.scope.cam.tExp - self.log['CAMERA']['CycleTime'] = self.scope.cam.tKin - self.log['CAMERA']['EMGain'] = self.scope.cam.GetEMGain() - self.log['GENERAL']['Width'] = self.scope.cam.GetPicWidth() - self.log['GENERAL']['Height'] = self.scope.cam.GetPicHeight() - self.log['CAMERA']['ROIPosX'] = self.scope.cam.GetROIX1() - self.log['CAMERA']['ROIPosY'] = self.scope.cam.GetROIY1() - self.log['CAMERA']['ROIWidth'] = self.scope.cam.GetROIX2() - self.scope.cam.GetROIX1() - self.log['CAMERA']['ROIHeight'] = self.scope.cam.GetROIY2() - self.scope.cam.GetROIY1() - self.log['CAMERA']['StartCCDTemp'] = self.scope.cam.GetCCDTemp() - self.log['CAMERA']['StartElectrTemp'] = self.scope.cam.GetElectrTemp() - - #self.log['GENERAL']['NumChannels'] = self.ds.getNumChannels() - #self.log['GENERAL']['NumHWChans'] = self.numHWChans - - #for ind in range(self.numHWChans): - # self.log['SHUTTER_%d' % ind] = {} - # self.log['SHUTTER_%d' % ind]['Name'] = self.chans.names[ind] - # self.log['SHUTTER_%d' % ind]['IntegrationTime'] = self.chans.itimes[ind] - # self.log['SHUTTER_%d' % ind]['Mask'] = self.hwChans[ind] - - # s = '' - ## bef = 0 - ## if (self.cols[ind] & self.BW): - ## s = s + 'BW' - ## bef = 1 - ## if (self.cols[ind] & self.RED): - ## if bef: - ## s = s + ' ' - ## s = s + 'R' - ## bef = 1 - ## if (self.cols[ind] & self.GREEN1): - ## if bef: - ## s = s + ' ' - ## s = s + 'G1' - ## bef = 1 - ## if (self.cols[ind] & self.GREEN2): - ## if bef: - ## s = s + ' ' - ## s = s + 'G2' - ## bef = 1 - ## if (self.cols[ind] & self.BLUE): - ## if bef: - ## s = s + ' ' - ## s = s + 'B' - ## #bef = 1 - ## self.log['SHUTTER_%d' % ind]['Colours'] = s - - dt = datetime.datetime.now() - - self.dtStart = dt - - self.log['GENERAL']['Date'] = '%d/%d/%d' % (dt.day, dt.month, dt.year) - self.log['GENERAL']['StartTime'] = '%d:%d:%d' % (dt.hour, dt.minute, dt.second) - - def doStopLog(self): - #self.log['GENERAL']['Depth'] = self.ds.getDepth() - #self.log['PIEZOS']['EndPos'] = self.GetEndPos() - self.scope.cam.GetStatus() - self.log['CAMERA']['EndCCDTemp'] = self.scope.cam.GetCCDTemp() - self.log['CAMERA']['EndElectrTemp'] = self.scope.cam.GetElectrTemp() - - dt = datetime.datetime.now() - self.log['GENERAL']['EndTime'] = '%d:%d:%d' % (dt.hour, dt.minute, dt.second) - self.log['GENERAL']['NumImages'] = '%d' % self.imNum - - def writeLog(self): - lw = logparser.logwriter() - s = lw.write(self.log) - log_f = open(self.filename, 'w') - log_f.write(s) - log_f.close() - - def __del__(self): - if self.spoolOn: - self.StopSpool() diff --git a/PYME/Acquire/Loft/chanedit.py b/PYME/Acquire/Loft/chanedit.py deleted file mode 100755 index c00c63010..000000000 --- a/PYME/Acquire/Loft/chanedit.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/python - -################## -# chanedit.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#!/usr/bin/env python -# generated by wxGlade 0.3.3 on Fri Sep 24 09:22:05 2004 - -import wx - -class ChanEditDialog(wx.Dialog): - def __init__(self, cname, cols, hw, *args, **kwds): - # begin wxGlade: ChanEditDialog.__init__ - kwds["style"] = wx.DEFAULT_DIALOG_STYLE - wx.Dialog.__init__(self, *args, **kwds) - - self.cname = cname - self.cols = cols - self.hw = hw - - self.panel_1 = wx.Panel(self, -1) - self.label_1 = wx.StaticText(self.panel_1, -1, "Name: ") - self.tName = wx.TextCtrl(self.panel_1, -1, cname) - self.cbSh0 = wx.CheckBox(self.panel_1, -1, "0") - self.cbSh0.SetValue(hw&1) - self.cbSh1 = wx.CheckBox(self.panel_1, -1, "1") - self.cbSh1.SetValue(hw&2) - self.cbSh2 = wx.CheckBox(self.panel_1, -1, "2") - self.cbSh2.SetValue(hw&4) - self.cbSh3 = wx.CheckBox(self.panel_1, -1, "3") - self.cbSh3.SetValue(hw&8) - self.cbSh4 = wx.CheckBox(self.panel_1, -1, "4") - self.cbSh4.SetValue(hw&16) - self.cbSh5 = wx.CheckBox(self.panel_1, -1, "5") - self.cbSh5.SetValue(hw&32) - self.cbSh6 = wx.CheckBox(self.panel_1, -1, "6") - self.cbSh6.SetValue(hw&64) - self.cbSh7 = wx.CheckBox(self.panel_1, -1, "7") - self.cbSh7.SetValue(hw&128) - self.cbBW = wx.CheckBox(self.panel_1, -1, "B/W") - self.cbBW.SetValue(cols&1) - self.cbRed = wx.CheckBox(self.panel_1, -1, "Red") - self.cbRed.SetValue(cols&2) - self.cbGreen1 = wx.CheckBox(self.panel_1, -1, "Green1") - self.cbGreen1.SetValue(cols&4) - self.cbGreen2 = wx.CheckBox(self.panel_1, -1, "Green2") - self.cbGreen2.SetValue(cols&8) - self.cbBlue = wx.CheckBox(self.panel_1, -1, "Blue") - self.cbBlue.SetValue(cols&16) - self.bOK = wx.Button(self.panel_1, -1, "OK") - self.bCancel = wx.Button(self.panel_1, -1, "Cancel") - - wx.EVT_BUTTON(self, self.bOK.GetId(), self.onOK) - wx.EVT_BUTTON(self, self.bCancel.GetId(), self.onCancel) - - self.__set_properties() - self.__do_layout() - # end wxGlade - - def __set_properties(self): - # begin wxGlade: ChanEditDialog.__set_properties - self.SetTitle("Edit Channel") - self.bOK.SetDefault() - # end wxGlade - - def __do_layout(self): - # begin wxGlade: ChanEditDialog.__do_layout - sizer_6 = wx.BoxSizer(wx.HORIZONTAL) - sizer_4 = wx.BoxSizer(wx.HORIZONTAL) - sizer_5 = wx.BoxSizer(wx.VERTICAL) - grid_sizer_1 = wx.BoxSizer(wx.VERTICAL) - coloursize = wx.StaticBoxSizer(wx.StaticBox(self.panel_1, -1, "Colours"), wx.HORIZONTAL) - shutsize = wx.StaticBoxSizer(wx.StaticBox(self.panel_1, -1, "Shutters"), wx.HORIZONTAL) - namesize = wx.BoxSizer(wx.HORIZONTAL) - namesize.Add(self.label_1, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 3) - namesize.Add(self.tName, 1, wx.ALL|wx.EXPAND, 3) - grid_sizer_1.Add(namesize, 0, wx.EXPAND, 0) - shutsize.Add(self.cbSh0, 0, wx.ALL, 3) - shutsize.Add(self.cbSh1, 0, wx.ALL, 3) - shutsize.Add(self.cbSh2, 0, wx.ALL, 3) - shutsize.Add(self.cbSh3, 0, wx.ALL, 3) - shutsize.Add(self.cbSh4, 0, wx.ALL, 3) - shutsize.Add(self.cbSh5, 0, wx.ALL, 3) - shutsize.Add(self.cbSh6, 0, wx.ALL, 3) - shutsize.Add(self.cbSh7, 0, wx.ALL, 3) - grid_sizer_1.Add(shutsize, 1, wx.ALL|wx.EXPAND, 3) - coloursize.Add(self.cbBW, 0, wx.ALL, 3) - coloursize.Add(self.cbRed, 0, wx.ALL, 3) - coloursize.Add(self.cbGreen1, 0, wx.ALL, 3) - coloursize.Add(self.cbGreen2, 0, wx.ALL, 3) - coloursize.Add(self.cbBlue, 0, wx.ALL, 3) - grid_sizer_1.Add(coloursize, 1, wx.ALL|wx.EXPAND, 3) - sizer_4.Add(grid_sizer_1, 1, wx.EXPAND, 0) - sizer_5.Add((20, 20), 1, wx.EXPAND, 0) - sizer_5.Add(self.bOK, 0, wx.ALL, 3) - sizer_5.Add(self.bCancel, 0, wx.ALL, 3) - sizer_4.Add(sizer_5, 0, wx.EXPAND, 0) - self.panel_1.SetAutoLayout(1) - self.panel_1.SetSizer(sizer_4) - sizer_4.Fit(self.panel_1) - sizer_4.SetSizeHints(self.panel_1) - sizer_6.Add(self.panel_1, 1, wx.EXPAND, 0) - self.SetAutoLayout(1) - self.SetSizer(sizer_6) - sizer_6.Fit(self) - sizer_6.SetSizeHints(self) - self.Layout() - # end wxGlade - - def onOK(self, event): - self.cname = self.tName.GetValue() - if not (len(self.cname) > 0): - md = wx.MessageDialog(self, 'Name cannot be empty', 'Invalid Name', wx.OK) - md.ShowModal() - return - - if not (self.cname.find(' ') == -1): - md = wx.MessageDialog(self, 'Name cannot contain spaces', 'Invalid Name', wx.OK) - md.ShowModal() - return - - self.cols = self.cbBW.GetValue() + self.cbRed.GetValue()*2 + self.cbGreen1.GetValue()*4 - self.cols = self.cols + self.cbGreen2.GetValue()*8 + self.cbBlue.GetValue()*16 - - if not self.cols > 0: - md = wx.MessageDialog(self, 'Must select at least one colour', 'Invalid Colours', wx.OK) - md.ShowModal() - return - - self.hw = self.cbSh0.GetValue() + self.cbSh1.GetValue()*2 + self.cbSh2.GetValue()*4 + self.cbSh3.GetValue()*8 - self.hw = self.hw + self.cbSh4.GetValue()*16 + self.cbSh5.GetValue()*32 + self.cbSh6.GetValue()*64 + self.cbSh7.GetValue()*128 - - self.EndModal(True) - - def onCancel(self, event): - self.EndModal(False) - -# end of class ChanEditDialog - - diff --git a/PYME/Acquire/Loft/chanedit.wxg b/PYME/Acquire/Loft/chanedit.wxg deleted file mode 100755 index c3e64ba05..000000000 --- a/PYME/Acquire/Loft/chanedit.wxg +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - - - wxHORIZONTAL - - wxEXPAND - 0 - - - wxVERTICAL - - wxEXPAND - 0 - - - wxHORIZONTAL - - wxALL|wxALIGN_CENTER_VERTICAL - 3 - - - 1 - - - - - wxALL|wxEXPAND - 3 - - - - - - - - wxALL|wxEXPAND - 3 - - - wxHORIZONTAL - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - - - wxALL|wxEXPAND - 3 - - - wxHORIZONTAL - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - wxALL - 3 - - - - - - - - - - - wxEXPAND - 0 - - - wxVERTICAL - - wxEXPAND - 0 - - - 20 - 20 - - - - wxALL - 3 - - - 1 - - - - - wxALL - 3 - - - - - - - - - - - - dialog_2 - - wxHORIZONTAL - - wxEXPAND - 0 - - - - - - - - diff --git a/PYME/Acquire/Loft/chanfr.py b/PYME/Acquire/Loft/chanfr.py deleted file mode 100755 index c2e12c6b1..000000000 --- a/PYME/Acquire/Loft/chanfr.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/python - -################## -# chanfr.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#!/usr/bin/env python -# generated by wxGlade 0.3.3 on Mon Jun 14 06:48:07 2004 - -import wx -#import sys -#sys.path.append(".") - -#import viewpanel -#import example - -from PYME.Acquire.chanpanel import ChannelPan - -class ChanFrame(wx.Dialog): - def __init__(self, parent, chaninfo, title='Edit Shutters/Channels'): - wx.Dialog.__init__(self,parent, -1, title) - - self.cp = ChannelPan(chaninfo, parent=self) - self.bOK = wx.Button(self, -1, "OK") - sizer = wx.BoxSizer(wx.VERTICAL) - sizer.Add(self.cp, 1,wx.EXPAND,0) - sizer.Add(self.bOK, 0,wx.TOP, 5) - self.SetAutoLayout(1) - self.SetSizer(sizer) - sizer.Fit(self) - #sizer.SetSizeHints(self) - - self.Layout() - - wx.EVT_BUTTON(self, self.bOK.GetId(), self.OnOK) - - def OnOK(self, event): - self.EndModal(1) - - - - -# end of class ViewFrame diff --git a/PYME/Acquire/Loft/chanpanel.py b/PYME/Acquire/Loft/chanpanel.py deleted file mode 100755 index 83d286800..000000000 --- a/PYME/Acquire/Loft/chanpanel.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/python - -################## -# chanpanel.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#!/usr/bin/env python -# generated by wxGlade 0.3.3 on Thu Jun 17 09:28:57 2004 - -import wx -from PYME.Acquire import previewaquisator -from PYME.Acquire import chanedit - -class ChannelPan(wx.Panel): - def __init__(self, chaninfo, parent,*args, **kwds): - # begin wxGlade: ChannelPan.__init__ - kwds["style"] = wx.TAB_TRAVERSAL - wx.Panel.__init__(self,parent,-1, *args, **kwds) - - self.chaninfo = chaninfo - - self.lChans = wx.ListBox(self, -1,size=(180,60), choices=[]) - self.bAdd = wx.Button(self, -1, "Add") - self.bEdit = wx.Button(self, -1, "Edit") - self.bDelete = wx.Button(self, -1, "Delete") - - wx.EVT_BUTTON(self, self.bAdd.GetId(), self.onAdd) - wx.EVT_BUTTON(self, self.bEdit.GetId(), self.onEdit) - wx.EVT_BUTTON(self, self.bDelete.GetId(), self.onDelete) - - self.UpdateList() - - self.__set_properties() - self.__do_layout() - # end wxGlade - - def __set_properties(self): - # begin wxGlade: ChannelPan.__set_properties - self.lChans.SetSelection(0) - # end wxGlade - - def __do_layout(self): - # begin wxGlade: ChannelPan.__do_layout - sizer_6 = wx.StaticBoxSizer(wx.StaticBox(self, -1, "Channels"), wx.HORIZONTAL) - sizer_7 = wx.BoxSizer(wx.VERTICAL) - sizer_6.Add(self.lChans, 1, wx.ALL|wx.EXPAND, 5) - sizer_7.Add(self.bAdd, 0, wx.LEFT|wx.RIGHT|wx.TOP, 5) - sizer_7.Add(self.bEdit, 0, wx.LEFT, 5) - sizer_7.Add(self.bDelete, 0, wx.LEFT|wx.BOTTOM, 5) - sizer_6.Add(sizer_7, 0, 0, 0) - self.SetAutoLayout(1) - self.SetSizer(sizer_6) - sizer_6.Fit(self) - sizer_6.SetSizeHints(self) - # end wxGlade - - def UpdateList(self): - self.chanstrings = [] - for c in range(len(self.chaninfo.names)): - bef = 0 - s = self.chaninfo.names[c] + ' ' - if (self.chaninfo.cols[c] & previewaquisator.PreviewAquisator.BW): - s = s + 'BW' - bef = 1 - if (self.chaninfo.cols[c] & previewaquisator.PreviewAquisator.RED): - if bef: - s = s + '|' - s = s + 'R' - bef = 1 - if (self.chaninfo.cols[c] & previewaquisator.PreviewAquisator.GREEN1): - if bef: - s = s + '|' - s = s + 'G1' - bef = 1 - if (self.chaninfo.cols[c] & previewaquisator.PreviewAquisator.GREEN2): - if bef: - s = s + '|' - s = s + 'G2' - bef = 1 - if (self.chaninfo.cols[c] & previewaquisator.PreviewAquisator.BLUE): - if bef: - s = s + '|' - s = s + 'B' - bef = 1 - - h = self.chaninfo.hw[c] - s = s + ' ' + '%d%d%d%d%d%d%d%d' % (h&1, h&2, h&4, h&8, h&16, h&32, h&64, h&128) - - self.chanstrings.append(s) - self.lChans.Set(self.chanstrings) - - if len(self.chanstrings) > 0: - self.bEdit.Enable(True) - self.bDelete.Enable(True) - else: - self.bEdit.Disable() - self.bDelete.Disable() - - def onAdd(self, event): - ce = chanedit.ChanEditDialog('',0,0, None, -1, '') - if (ce.ShowModal()): - self.chaninfo.names.append(ce.cname) - self.chaninfo.cols.append(ce.cols) - self.chaninfo.hw.append(ce.hw) - self.chaninfo.itimes.append(100) - self.UpdateList() - self.lChans.SetSelection(len(self.chaninfo.names)-1) - - def onEdit(self, event): - ind = self.lChans.GetSelection() - ce = chanedit.ChanEditDialog(self.chaninfo.names[ind],self.chaninfo.cols[ind],self.chaninfo.hw[ind], None, -1, '') - if (ce.ShowModal()): - self.chaninfo.names[ind] = ce.cname - self.chaninfo.cols[ind] = ce.cols - self.chaninfo.hw[ind] = ce.hw - self.UpdateList() - self.lChans.SetSelection(ind) - - def onDelete(self, event): - ind = self.lChans.GetSelection() - self.chaninfo.names.pop(ind) - self.chaninfo.cols.pop(ind) - self.chaninfo.hw.pop(ind) - self.chaninfo.itimes.pop(ind) - self.UpdateList() - self.lChans.SetSelection(0) - - - -# end of class ChannelPan - - diff --git a/PYME/Acquire/Loft/chanpannel.wxg b/PYME/Acquire/Loft/chanpannel.wxg deleted file mode 100755 index 0f851456b..000000000 --- a/PYME/Acquire/Loft/chanpannel.wxg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - wxHORIZONTAL - - - wxALL|wxEXPAND - 5 - - - 0 - - - - - - 0 - - - wxVERTICAL - - wxLEFT|wxRIGHT|wxTOP - 5 - - - - - - - wxLEFT - 5 - - - - - - - wxLEFT|wxBOTTOM - 5 - - - - - - - - - - diff --git a/PYME/Acquire/Loft/noclosefr.py b/PYME/Acquire/Loft/noclosefr.py deleted file mode 100755 index 43c5bd37a..000000000 --- a/PYME/Acquire/Loft/noclosefr.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/python - -################## -# noclosefr.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import wx - -class noCloseFrame(wx.Frame): - def __init__(self,*args, **kwds): - wx.Frame.__init__(self,*args, **kwds) - wx.EVT_CLOSE(self, self.OnCloseWindow) - - def OnCloseWindow(self, event): - if (not event.CanVeto()): - self.Destroy() - else: - event.Veto() - self.Hide() - -class wxFrame(wx.Frame): - def __init__(self,*args, **kwds): - wx.Frame.__init__(self,*args, **kwds) - wx.EVT_CLOSE(self, self.OnCloseWindow) - - def OnCloseWindow(self, event): - if (not event.CanVeto()): - self.Destroy() - else: - event.Veto() - self.Hide() \ No newline at end of file diff --git a/PYME/Acquire/Loft/previewaquisator.py b/PYME/Acquire/Loft/previewaquisator.py deleted file mode 100755 index 118e327c5..000000000 --- a/PYME/Acquire/Loft/previewaquisator.py +++ /dev/null @@ -1,472 +0,0 @@ -#!/usr/bin/python - -################## -# previewaquisator.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import wx -#from PYME.cSMI import CDataStack, CDataStack_AsArray - -import numpy as np - -import time -import traceback - -from PYME.contrib import dispatch - -from PYME.Acquire import eventLog - -#class dsFake(object): -# def __init__(self, width, height, length, nChans): -# self.width = width -# self.height = height -# self.length = length -# self.nChans = nChans - - - - -class PreviewAquisator(wx.EvtHandler): -# BW = 1 -# RED = 2 -# GREEN1 = 4 -# GREEN2 = 8 -# BLUE = 16 - - def __init__(self, _chans, _cam, _shutters, _ds = None): - wx.EvtHandler.__init__(self) - self.timer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.Notify) - - self.chans = _chans - #self.hwChans = _chans.hw - #self.numHWChans = len(_chans.hw) - #self.cols = _chans.cols - self.dsa = _ds - self.cam = _cam - self.shutters = _shutters - self.loopnuf = 0 - self.aqOn = False - #lists of functions to call on a new frame, and when the aquisition ends - #self.WantFrameNotification = [] - #self.WantStopNotification = [] - #self.WantStartNotification = [] - #list of functions to call to see if we ought to wait on any hardware - self.HardwareChecks = [] - #should we start a new exposure on the next timer check? - self.needExposureStart = False - - self.tLastFrame=0 - self.tThisFrame=0 - self.nFrames = 0 - self.tl=0 - - self.inNotify = False - - self.zPos = 0 - - #will be needed to allow the display load to be minimised by, e.g. only updating display once per poll rather than once per frame - #self.WantFrameGroupNotification = [] - - - #Signals - ########################## - # these allow other files to listen to key events happingin within the acquisiotn - #new style signals - these will replace the WantFrameNotification etc ... - #which are currently being kept for backwards compatibility - - self.onFrame = dispatch.Signal(['frameData']) #called each time a new frame appears in our buffer - self.onFrameGroup = dispatch.Signal() #called on each new frame group (once per polling interval) - use for updateing GUIs etc. - self.onStop = dispatch.Signal() - self.onStart = dispatch.Signal() - - - def Prepare(self, keepds=False): - """Prepare for acquisition by allocating the buffer which will store the - data we recieve. The buffer stores a single frame, and all frames pass - through this buffer. The current state of the buffer is accessible via - the currentFrame variable. - - Parameters - ---------- - keepds: Whether or not to keep the previously allocated array - - """ - self.looppos=0 - self.curMemChn=0 - - self.hwChans = self.chans.hw - self.numHWChans = len(self.chans.hw) - self.cols = self.chans.cols - - order = 'F' - if 'order' in dir(self.cam): - order = self.cam.order - - if (self.dsa is None or keepds == False): - self.dsa = None - #self.ds = CDataStack(self.cam.GetPicWidth(), self.cam.GetPicHeight(), - # self.GetSeqLength(),self.getReqMemChans(self.cols)) - #self.dsa = CDataStack_AsArray(self.ds, 0) - self.dsa = np.zeros([self.cam.GetPicWidth(), self.cam.GetPicHeight(), - self.GetSeqLength()], dtype = 'uint16', order = order) - - - - def getFrame(self, colours=None): - """Ask the camera to put a frame into our buffer""" - #print self.zPos - if ('numpy_frames' in dir(self.cam)): - cs = self.dsa[:,:,self.zPos] - else: - cs = self.dsa[:,:,self.zPos].ctypes.data - - #Get camera to insert data into our array (results passed back "by reference") - #this is a kludge/artifact of an old call into c-code - #in this context cs is a pointer to the memory we want the frame to go into - #for newer cameras, we pass a numpy array object, and the camera code - #copies the data into that array. - self.cam.ExtractColor(cs,0) - - def purge(self): - """purge (and discard) all remaining frames in the camera buffer""" - while(self.cam.ExpReady()): - self.curMemChn = 0 - self.getFrame(self.BW) - self.curMemChn = 0 - - - - def onExpReady(self): - """ There is an exposure waiting in the Camera, - looppos inticates which hardware (shutter) channel we're currently on """ - - self.loopnuf = self.loopnuf + 1 - - #If this was the last set of shutter combinations, move us to the position - # for the next slice. - if (self.looppos == (self.numHWChans - 1)): - self.doPiezoStep() - - # Set the shutters for the next exposure - #self.shutters.setShutterStates(self.hwChans[(self.looppos + 1)%self.numHWChans]) - self.shutters.setShutterStates(0) - - #self.Wait(15) #give shutters a chance to close - should fix hardware - - self.looppos = self.looppos + 1 - - contMode = False - if ('contMode' in dir(self.cam)): #hack for continous acquisition - do not want to/can't keep setting iTime - contMode =self.cam.contMode - - if ('itimes' in dir(self.chans) and not contMode): #maintain compatibility with old versions - self.cam.SetIntegTime(self.chans.itimes[self.looppos%self.numHWChans]) - self.cam.SetCOC() - - self.shutters.setShutterStates(self.hwChans[(self.looppos)%self.numHWChans]) - #self.Wait(15) - - #self.cam.StartExposure() - - - # Pull the existing data from the camera - try: - self.getFrame(self.cols[self.looppos-1]) - except: - traceback.print_exc() - finally: - - if not contMode: - #flag the need to start a new exposure - #print 'se' - self.needExposureStart = True - #self.cam.StartExposure() - - - if (self.looppos >= self.numHWChans): - self.looppos = 0 - self.curMemChn = 0 - - #for a in self.WantFrameNotification: - # a(self) - - #print 'onFrame' - self.onFrame.send(sender=self, frameData=self.dsa) - - # If we're at the end of the Data Stack, then stop - # Note that in normal sequence aquisition this is the line which determines how long to - # record for - in this class (ie the live preview) getNextDsSlice is defined such that - # it doesn't move through the stack, and always returns true, such that the aquisition - # continues for ever unless we stop it some other way (ie by clicking "Stop Live Preview") - # in CRealAquisator it's overridden to behave in the right way. - if not (self.getNextDsSlice()): - self.stop() - - - - def getReqMemChans(self, colours): - """ Use this function to calc how may channels to allocate when creating a new data stack """ - return 1 - -# t = 0 -# for c in colours: -# if(c & self.BW): -# t = t + 1 -# if(c & self.RED): -# t = t + 1 -# if(c & self.GREEN1): -# t = t + 1 -# if(c & self.GREEN2): -# t = t + 1 -# if(c & self.BLUE): -# t = t + 1 -# -# return t - - - def Notify(self, event=None): - """Callback which is called regularly by a system timer to poll the - camera""" - - #check to see if we are already running - if self.inNotify: - print('Already in notify, skip for now') - return - - try: - self.inNotify = True - "Should be called on each timer tick" - self.te = time.clock() - #print self.te - self.tl - self.tl = self.te - #print "Notify" - - #self.loopnuf = self.loopnuf + 1 - - if (True): #check that we are aquiring - - - if(not (self.cam.CamReady() and self.piezoReady())): - # Stop the aquisition if there is a hardware error - self.stop() - return - - #is there a picture waiting for us? - #if so do the relevant processing - #otherwise do nothing ... - - nFrames = 0 #number of frames grabbed this pass - - bufferOverflowed = False - - while(self.cam.ExpReady()): #changed to deal with multiple frames being ready - if 'GetNumImsBuffered' in dir(self.cam): - bufferOverflowing = self.cam.GetNumImsBuffered() >= (self.cam.GetBufferSize() - 1) - else: - bufferOverflowing = False - if bufferOverflowing: - bufferOverflowed = True - print('Warning: Camera buffer overflowing - purging buffer') - eventLog.logEvent('Camera Buffer Overflow') - #stop the aquisition - we're going to restart after we're read out to purge the buffer - #doing it this way _should_ stop the black frames which I guess are being caused by the reading the frame which is - #currently being written to - self.cam.StopAq() - - self.onExpReady() - nFrames += 1 - #te= time.clock() - - #If we can't deal with the data fast enough (e.g. due to file i/o limitations) this can turn into an infinite loop - - #avoid this by bailing out with a warning if nFrames exceeds a certain value. This will probably lead to buffer overflows - #and loss of data, but is arguably better than an unresponsive app. - #This value is (currently) chosen fairly arbitrarily, taking the following facts into account: - #the buffer has enough storage for ~3s when running flat out, - #we're polling at ~5hz, and we should be able to get more frames than would be expected during the polling intervall to - #allow us to catch up following glitches of one form or another, although not too many more. - if ('GetNumImsBuffered' in dir(self.cam)) and (nFrames > self.cam.GetBufferSize()/2): - print(('Warning: not keeping up with camera, giving up with %d frames still in buffer' % self.cam.GetNumImsBuffered())) - break - - if bufferOverflowed: - print('nse') - self.needExposureStart = True - - if self.needExposureStart and self.checkHardware(): - self.needExposureStart = False - self.cam.StartExposure() #restart aquisition - this should purge buffer - - - #if 'nQueued' in dir(self.cam): - # print '\n', nFrames, self.cam.nQueued, self.cam.nFull , self.cam.doPoll - if nFrames > 0: - self.n_Frames += nFrames - - self.tLastFrame = self.tThisFrame - self.nFrames = nFrames - self.tThisFrame = time.clock() - - self.onFrameGroup.send_robust(self) - else: - self._stop() - except: - traceback.print_exc() - finally: - self.inNotify = False - #self.timer.StartOnce(self.tiint) - self.timer.Start(self.tiint, wx.TIMER_ONE_SHOT) - - - @property - def currentFrame(self): - """Whatever frame is currently passing through the acquisition queue - - NB: this is an attempt to give a more user friendly name to .dsa - """ - return self.dsa - - def checkHardware(self): - """Check to see if our hardware is ready for us to take the next frame - - NB: This is largely legacy code, as the camera is usually used in - free-running mode.""" - for callback in self.HardwareChecks: - if not callback(): - print('Waiting for hardware') - return False - - return True - - def stop(self): - "Stop sequence aquisition" - - self.timer.Stop() - - self.aqOn = False - - if 'StopAq' in dir(self.cam): #deal with Andor without breaking sensicam - self.cam.StopAq() - - self.shutters.closeShutters(self.shutters.ALL) - - self.zPos = 0 - - self.piezoGoHome() - - self.doStopLog() - - - self.onStop.send_robust(self) - - def start(self, tiint = 100): - "Start aquisition" - self.tiint = tiint - - self.looppos = 0 - #self.ds.setZPos(0) #go to start of data stack - self.zPos = 0 - - #set the shutters up for the first frame - self.shutters.setShutterStates(self.hwChans[self.looppos]) - - #clear saturation intervened flag - self.cam.saturationIntervened = False - - #move piezo to starting position - self.setPiezoStartPos() - - self.doStartLog() - - self.onStart.send_robust(self) - - if ('itimes' in dir(self.chans)): #maintain compatibility with old versions - self.cam.SetIntegTime(self.chans.itimes[self.looppos]) - self.cam.SetCOC() - - iErr = self.cam.StartExposure() - self.cam.DisplayError(iErr) - - if (iErr < 0): - self.stop() - return False - - self.aqOn = True - - self.t_old = time.time() - self.n_Frames = 0 - - self.timer.Start(self.tiint, wx.TIMER_ONE_SHOT) - return True - - -# def Wait(self,iTime): -# """ Dirty delay routine - blocks until given no of milliseconds has elapsed\n -# Probably best not to use with a delay of more than about a second or windows\n -# could rightly assume that the programme is """ -# time.sleep(iTime/1000) -# #FirstTime = time.clock() -# #dc = 0 -# #while(time.clock() < (FirstTime + iTime/1000)): -# # dc = dc + 1 - - def isRunning(self): - return self.aqOn - - def getFPS(self): - t = time.time() - dt = t - self.t_old - - fps = 1.0*self.n_Frames/(1e-5 + dt) - self.t_old = t - self.n_Frames = 0 - - return fps - - #return 1.0*self.nFrames/(1e-5 + self.tThisFrame - self.tLastFrame) - - #place holders ... for overridden class which actually knows - #about the piezo - def doPiezoStep(self): - pass - - def piezoReady(self): - return True - - def setPiezoStartPos(self): - pass - - def getNextDsSlice(self): - return True - - def _stop(self): - self.stop() - - def doStartLog(self): - pass - - def doStopLog(self): - pass - - def GetSeqLength(self): - return 1 - - def piezoGoHome(self): - pass diff --git a/PYME/Acquire/Loft/previewaquisator2.py b/PYME/Acquire/Loft/previewaquisator2.py deleted file mode 100644 index 4e536c14e..000000000 --- a/PYME/Acquire/Loft/previewaquisator2.py +++ /dev/null @@ -1,426 +0,0 @@ -#!/usr/bin/python - -################## -# previewaquisator.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import wx -from PYME.cSMI import CDataStack, CDataStack_AsArray - -import time -import traceback - -from PYME.Acquire import eventLog - -class PreviewAquisator(wx.Timer): - BW = 1 - RED = 2 - GREEN1 = 4 - GREEN2 = 8 - BLUE = 16 - - def __init__(self, _chans, _cam, _shutters, _ds = None): - wx.Timer.__init__(self) - - self.chans = _chans - #self.hwChans = _chans.hw - #self.numHWChans = len(_chans.hw) - #self.cols = _chans.cols - self.ds = _ds - self.cam = _cam - self.shutters = _shutters - self.loopnuf = 0 - self.aqOn = False - #lists of functions to call on a new frame, and when the aquisition ends - self.WantFrameNotification = [] - self.WantStopNotification = [] - self.WantStartNotification = [] - #list of functions to call to see if we ought to wait on any hardware - self.HardwareChecks = [] - #should we start a new exposure on the next timer check? - self.needExposureStart = False - - self.tLastFrame=0 - self.tThisFrame=0 - self.nFrames = 0 - self.tl=0 - - self.inNotify = False - - #will be needed to allow the display load to be minimised by, e.g. only updating display once per poll rather than once per frame - self.WantFrameGroupNotification = [] - - def Prepare(self, keepds=False): - self.looppos=0 - self.curMemChn=0 - - self.hwChans = self.chans.hw - self.numHWChans = len(self.chans.hw) - self.cols = self.chans.cols - - if (self.ds is None or keepds == False): - self.ds = None - self.ds = CDataStack(self.cam.GetPicWidth(), self.cam.GetPicHeight(), - self.GetSeqLength(),self.getReqMemChans(self.cols)) - self.dsa = CDataStack_AsArray(self.ds, 0) - - i = 0 - for j in range(len(self.cols)): - a = self.chans.names[j] - c = self.chans.cols[j] - #for (a,c) in (self.chans.names, self.chans.cols): - - if(c & self.BW): - self.ds.setChannelName(i, (a + "_BW").encode()) - i = i + 1 - if(c & self.RED): - self.ds.setChannelName(i, (a + "_R").encode()) - i = i + 1 - if(c & self.GREEN1): - self.ds.setChannelName(i, (a + "_G1").encode()) - i = i + 1 - if(c & self.GREEN2): - self.ds.setChannelName(i, (a + "_G2").encode()) - i = i + 1 - if(c & self.BLUE): - self.ds.setChannelName(i, (a + "_B").encode()) - i = i + 1 - - - - #Check to see if the DataStack is big enough! - if (self.ds.getNumChannels() < self.getReqMemChans(self.cols)): - raise Exception("Not enough channels in Data Stack") - - self.shutters.closeShutters(self.shutters.ALL) - - def getFrame(self, colours): - """ Get a frame from the camera and extract the channels we want, - putting them into ds. """ - if ('numpy_frames' in dir(self.cam)): - cs = self.dsa[:,:,0] - else: - cs = ds.getCurrentChannelSlice(chan) - - self.cam.ExtractColor(cs,0) - - - def purge(self): - while(self.cam.ExpReady()): - self.curMemChn = 0 - self.getFrame(self.BW) - self.curMemChn = 0 - - - - def onExpReady(self): - """ There is an exposure waiting in the Camera, - looppos inticates which hardware (shutter) channel we're currently on """ - - #self.loopnuf = self.loopnuf + 1 - - #If this was the last set of shutter combinations, move us to the position - # for the next slice. - #if (self.looppos == (self.numHWChans - 1)): - # self.doPiezoStep() - - # Set the shutters for the next exposure - #self.shutters.setShutterStates(self.hwChans[(self.looppos + 1)%self.numHWChans]) - #self.shutters.setShutterStates(0) - - #self.Wait(15) #give shutters a chance to close - should fix hardware - - #self.looppos = self.looppos + 1 - - contMode = False - if ('contMode' in dir(self.cam)): #hack for continous acquisition - do not want to/can't keep setting iTime - contMode =self.cam.contMode - - if (not contMode and 'itimes' in dir(self.chans)): #maintain compatibility with old versions - self.cam.SetIntegTime(self.chans.itimes[self.looppos%self.numHWChans]) - self.cam.SetCOC() - - #self.shutters.setShutterStates(self.hwChans[(self.looppos)%self.numHWChans]) - #self.Wait(15) - - #self.cam.StartExposure() - - - # Pull the existing data from the camera - try: - self.getFrame(self.cols[self.looppos-1]) - except: - traceback.print_exc() - finally: - - if not contMode: - #flag the need to start a new exposure - self.needExposureStart = True - #self.cam.StartExposure() - - - #if (self.looppos >= self.numHWChans): - # self.looppos = 0 - # self.curMemChn = 0 - - for a in self.WantFrameNotification: - a(self) - - # If we're at the end of the Data Stack, then stop - # Note that in normal sequence aquisition this is the line which determines how long to - # record for - in this class (ie the live preview) getNextDsSlice is defined such that - # it doesn't move through the stack, and always returns true, such that the aquisition - # continues for ever unless we stop it some other way (ie by clicking "Stop Live Preview") - # in CRealAquisator it's overridden to behave in the right way. - #if not (self.getNextDsSlice()): - # self.stop() - - - - - - def getReqMemChans(self, colours): - """ Use this function to calc how may channels to allocate when creating a new data stack """ - - t = 0 - for c in colours: - if(c & self.BW): - t = t + 1 - if(c & self.RED): - t = t + 1 - if(c & self.GREEN1): - t = t + 1 - if(c & self.GREEN2): - t = t + 1 - if(c & self.BLUE): - t = t + 1 - - return t - - - def Notify(self): - #check to see if we are already running - if self.inNotify: - print('Already in notify, skip for now') - return - - try: - self.inNotify = True - "Should be called on each timer tick" - self.te = time.clock() - #print self.te - self.tl - self.tl = self.te - #print "Notify" - - #self.loopnuf = self.loopnuf + 1 - - if (True): #check that we are aquiring - - - if(not (self.cam.CamReady() and self.piezoReady())): - # Stop the aquisition if there is a hardware error - self.stop() - return - - #is there a picture waiting for us? - #if so do the relevant processing - #otherwise do nothing ... - - nFrames = 0 #number of frames grabbed this pass - - bufferOverflowed = False - - self.cam.camLock.acquire() - while(self.cam.ExpReady()): #changed to deal with multiple frames being ready - if 'GetNumImsBuffered' in dir(self.cam) and not self.cam.burstMode: - bufferOverflowing = self.cam.GetNumImsBuffered() >= (self.cam.GetBufferSize() - 1) - else: - bufferOverflowing = False - if bufferOverflowing: - bufferOverflowed = True - print('Warning: Camera buffer overflowing - purging buffer') - eventLog.logEvent('Camera Buffer Overflow') - #stop the aquisition - we're going to restart after we're read out to purge the buffer - #doing it this way _should_ stop the black frames which I guess are being caused by the reading the frame which is - #currently being written to - self.cam.StopAq() - - self.onExpReady() - nFrames += 1 - #te= time.clock() - - #If we can't deal with the data fast enough (e.g. due to file i/o limitations) this can turn into an infinite loop - - #avoid this by bailing out with a warning if nFrames exceeds a certain value. This will probably lead to buffer overflows - #and loss of data, but is arguably better than an unresponsive app. - #This value is (currently) chosen fairly arbitrarily, taking the following facts into account: - #the buffer has enough storage for ~3s when running flat out, - #we're polling at ~5hz, and we should be able to get more frames than would be expected during the polling intervall to - #allow us to catch up following glitches of one form or another, although not too many more. - if ('GetNumImsBuffered' in dir(self.cam)) and (nFrames > self.cam.GetBufferSize()/4): - print(('Warning: not keeping up with camera, giving up with %d frames still in buffer' % self.cam.GetNumImsBuffered())) - break - self.cam.camLock.release() - - if bufferOverflowed: - self.needExposureStart = True - - if self.needExposureStart and self.checkHardware(): - self.cam.StartExposure() #restart aquisition - this should purge buffer - self.needExposureStart = False - - #if 'nQueued' in dir(self.cam): - # print '\n', nFrames, self.cam.nQueued, self.cam.nFull , self.cam.doPoll - if nFrames > 0: - self.n_Frames += nFrames - - self.tLastFrame = self.tThisFrame - self.nFrames = nFrames - self.tThisFrame = time.clock() - - for a in self.WantFrameGroupNotification: - a(self) - else: - self._stop() - finally: - self.inNotify = False - - def checkHardware(self): - for callback in self.HardwareChecks: - if not callback(): - return False - - return True - - def stop(self): - "Stop sequence aquisition" - - wx.Timer.Stop(self) - - self.aqOn = False - - if 'StopAq' in dir(self.cam): #deal with Andor without breaking sensicam - self.cam.StopAq() - - self.shutters.closeShutters(self.shutters.ALL) - #self.cam.StopLifePreview() - self.ds.setZPos(0) - - self.piezoGoHome() - - self.doStopLog() - - for a in self.WantStopNotification: - a(self) - - def start(self, tiint = 100): - "Start aquisition" - - self.looppos = 0 - self.ds.setZPos(0) #go to start of data stack - - #set the shutters up for the first frame - self.shutters.setShutterStates(self.hwChans[self.looppos]) - - #clear saturation intervened flag - self.cam.saturationIntervened = False - - #move piezo to starting position - self.setPiezoStartPos() - - self.doStartLog() - - for cb in self.WantStartNotification: - cb(self) - - #self.Wait(1000) # Warten, so dass Piezotisch wieder in Ruhe - - if ('itimes' in dir(self.chans)): #maintain compatibility with old versions - self.cam.SetIntegTime(self.chans.itimes[self.looppos]) - self.cam.SetCOC() - - iErr = self.cam.StartExposure() - self.cam.DisplayError(iErr) - - if (iErr < 0): - self.stop() - return False - - self.aqOn = True - - self.t_old = time.time() - self.n_Frames = 0 - - wx.Timer.Start(self,tiint) - return True - - - def Wait(self,iTime): - """ Dirty delay routine - blocks until given no of milliseconds has elapsed\n - Probably best not to use with a delay of more than about a second or windows\n - could rightly assume that the programme is """ - time.sleep(iTime/1000) - #FirstTime = time.clock() - #dc = 0 - #while(time.clock() < (FirstTime + iTime/1000)): - # dc = dc + 1 - - def isRunning(self): - return self.aqOn - - def getFPS(self): - t = time.time() - dt = t - self.t_old - - fps = 1.0*self.n_Frames/(1e-5 + dt) - self.t_old = t - self.n_Frames = 0 - - return fps - - #return 1.0*self.nFrames/(1e-5 + self.tThisFrame - self.tLastFrame) - - #place holders ... for overridden class which actually knows - #about the piezo - def doPiezoStep(self): - pass - - def piezoReady(self): - return True - - def setPiezoStartPos(self): - pass - - def getNextDsSlice(self): - return True - - def _stop(self): - self.stop() - - def doStartLog(self): - pass - - def doStopLog(self): - pass - - def GetSeqLength(self): - return 1 - - def piezoGoHome(self): - pass diff --git a/PYME/Acquire/Loft/psliders.py b/PYME/Acquire/Loft/psliders.py deleted file mode 100755 index 66040697e..000000000 --- a/PYME/Acquire/Loft/psliders.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/python - -################## -# psliders.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#!/usr/bin/env python -# generated by wxGlade 0.3.3 on Thu Sep 23 08:22:22 2004 - -import wx -#import noclosefr -import sys - -class PiezoSliders(wx.Panel): - def __init__(self, piezos, parent, joystick = None, id=-1): - # begin wxGlade: MyFrame1.__init__ - #kwds["style"] = wx.DEFAULT_FRAME_STYLE - wx.Panel.__init__(self, parent, id) - - self.piezos = piezos - self.joystick = joystick - #self.panel_1 = wx.Panel(self, -1) - self.sliders = [] - self.sliderLabels = [] - #self.SetTitle("Piezo Control") - #sizer_1 = wx.BoxSizer(wx.VERTICAL) - sizer_2 = wx.BoxSizer(wx.VERTICAL) - - for p in self.piezos: - #if sys.platform == 'darwin': #sliders are subtly broken on MacOS, requiring workaround - sl = wx.Slider(self, -1, 100*p[0].GetPos(p[1]), 100*p[0].GetMin(p[1]), 100*p[0].GetMax(p[1]), size=wx.Size(100,-1), style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS|wx.SL_LABELS) - #else: - # sl = wx.Slider(self.panel_1, -1, 100*p[0].GetPos(p[1]), 100*p[0].GetMin(p[1]), 100*p[0].GetMax(p[1]), size=wx.Size(300,-1), style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_LABELS) - #sl.SetSize((800,20)) - if 'units' in dir(p[0]): - unit = p[0].units - else: - unit = u'\u03BCm' - sLab = wx.StaticBox(self, -1, u'%s - %2.4f %s' % (p[2], p[0].GetPos(p[1]), unit)) - -# if 'minorTick' in dir(p): -# sl.SetTickFreq(100, p.minorTick) -# else: -# sl.SetTickFreq(100, 1) - sz = wx.StaticBoxSizer(sLab, wx.HORIZONTAL) - sz.Add(sl, 1, wx.ALL|wx.EXPAND, 2) - #sz.Add(sLab, 0, wx.ALL|wx.EXPAND, 2) - sizer_2.Add(sz,1,wx.EXPAND,0) - - self.sliders.append(sl) - self.sliderLabels.append(sLab) - - - if not joystick is None: - self.cbJoystick = wx.CheckBox(self, -1, 'Enable Joystick') - sizer_2.Add(self.cbJoystick,0,wx.TOP|wx.BOTTOM,2) - self.cbJoystick.Bind(wx.EVT_CHECKBOX, self.OnJoystickEnable) - - #sizer_2.AddSpacer(1) - - wx.EVT_SCROLL(self,self.onSlide) - - - #self.SetAutoLayout(1) - self.SetSizer(sizer_2) - sizer_2.Fit(self) - #sizer_2.SetSizeHints(self) - - #self.Layout() - # end wxGlade - - def OnJoystickEnable(self, event): - self.joystick.Enable(self.cbJoystick.IsChecked()) - - def onSlide(self, event): - sl = event.GetEventObject() - ind = self.sliders.index(sl) - self.sl = sl - self.ind = ind - self.piezos[ind][0].MoveTo(self.piezos[ind][1], sl.GetValue()/100.0, False) - self.sliderLabels[ind].SetLabel(u'%s - %2.2f \u03BCm' % (self.piezos[ind][2],sl.GetValue()/100.0)) - - def update(self): - for ind in range(len(self.piezos)): - p = self.piezos[ind] - - pz, chan, name = self.piezos[ind] - if 'units' in dir(pz): - unit = pz.units - else: - unit = u'\u03BCm' - - #try: - # on_target = pz.OnTarget() - #except AttributeError: - # on_target = True - - if 'GetTargetPos' in dir(pz): - pos = pz.GetTargetPos(chan) - print('target pos') - elif 'lastPos' in dir(pz): - pos = pz.lastPos - elif 'GetLastPos' in dir(pz): - pos = pz.GetLastPos(chan) - else: - pos = pz.GetPos(chan) - - self.sliders[ind].SetValue(100*pos) - self.sliderLabels[ind].SetLabel(u'%s - %2.4f %s' % (name,pos, unit)) - - self.sliders[ind].SetMin(100*pz.GetMin(chan)) - self.sliders[ind].SetMax(100*pz.GetMax(chan)) - - if not self.joystick is None: - self.cbJoystick.SetValue(self.joystick.IsEnabled()) - - - - diff --git a/PYME/Acquire/Loft/simplesequenceaquisator.py b/PYME/Acquire/Loft/simplesequenceaquisator.py deleted file mode 100755 index c261e0b66..000000000 --- a/PYME/Acquire/Loft/simplesequenceaquisator.py +++ /dev/null @@ -1,275 +0,0 @@ -#!/usr/bin/python - -################## -# simplesequenceaquisator.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import math -#import datetime -from PYME.Acquire.previewaquisator import PreviewAquisator -import time -from PYME.IO import MetaDataHandler - -class SimpleSequenceAquisitor(PreviewAquisator): - # 'Constants' - PHASE = 1 - OBJECT = 0 - CENTRE_AND_LENGTH = 0 - START_AND_END = 1 - FORWARDS = 1 - BACKWARDS = -1 - - def __init__(self,chans, cam, shutters,_piezos, _log={}): - PreviewAquisator.__init__(self, chans, cam, shutters, None) - self.piezos = _piezos - self.log = _log - self.mdh = MetaDataHandler.NestedClassMDHandler() - #register as a provider of metadata - MetaDataHandler.provideStartMetadata.append(self.ProvideStackMetadata) - - self.ScanChan = 0 - self.StartMode = self.CENTRE_AND_LENGTH - self.SeqLength = 100 - self.StepSize = 0.2 - self.startPos = 0 - self.endPos = 0 - - self.direction = self.FORWARDS - - def doPiezoStep(self): - if(self.GetStepSize() > 0): - #self.piezos[self.GetScanChannel()][0].MoveTo(self.piezos[self.GetScanChannel()][1], self.piezos[self.GetScanChannel()][0].GetPos(self.piezos[self.GetScanChannel()][1]) + - # self.GetDirection()*self.GetStepSize(), False) - self.piezos[self.GetScanChannel()][0].MoveTo(self.piezos[self.GetScanChannel()][1], self.startPosRef + self.GetDirection()*self.GetStepSize()*(self.ds.getZPos() +1), False) - #time.sleep(0.01) - #print self.piezos[self.GetScanChannel()][0].GetPos(self.piezos[self.GetScanChannel()][1]) - #print self.GetDirection()*self.GetStepSize()*(self.ds.getZPos() +1) - #print self.startPosRef - #print self.GetScanChannel() - def piezoReady(self): - return not ((self.GetStepSize() > 0) and - (self.piezos[self.GetScanChannel()][0].GetControlReady() == False)) - def setPiezoStartPos(self): - """ Used internally to move the piezo at the beginning of the aquisition """ - #self.curpos = self.piezos[self.GetScanChannel()][0].GetPos(self.piezos[self.GetScanChannel()][1]) - self.SetPrevPos(self._CurPos()) # aktuelle Position der Piezotische merken - self.startPosRef = self.GetStartPos() - self.piezos[self.GetScanChannel()][0].MoveTo(self.piezos[self.GetScanChannel()][1], self.GetStartPos(), False) - - def getNextDsSlice(self): - return self.ds.nextZ() - def GetScanChannel(self): - return self.ScanChan - def piezoGoHome(self): - self.piezos[self.GetScanChannel()][0].MoveTo(self.piezos[self.GetScanChannel()][1],self.GetPrevPos(),False) - def SetScanChannel(self,iMode): - self.ScanChan = iMode - def SetSeqLength(self,iLength): - self.SeqLength = iLength - def GetSeqLength(self): - if (self.StartMode == 0): - return self.SeqLength - else: - return int(math.ceil(abs(self.GetEndPos() - self.GetStartPos())/self.GetStepSize())) - def SetStartMode(self, iMode): - self.StartMode = iMode - def GetStartMode(self): - return self.StartMode - def SetStepSize(self, fSize): - self.StepSize = fSize - def GetStepSize(self): - return self.StepSize - def SetStartPos(self, sPos): - self.startPos = sPos - def GetStartPos(self): - if (self.GetStartMode() == 0): - return self._CurPos() - (self.GetStepSize()*(self.GetSeqLength() - 1)*self.GetDirection()/2) - else: - if not ("startPos" in dir(self)): - raise RuntimeError("Please call SetStartPos first !!") - return self.startPos - def SetEndPos(self, ePos): - self.endPos = ePos - def GetEndPos(self): - if (self.GetStartMode() == 0): - return self._CurPos() + (self.GetStepSize()*(self.GetSeqLength() - 1)*self.GetDirection()/2) - else: - if not ("endPos" in dir(self)): - raise RuntimeError("Please call SetEndPos first !!") - return self.endPos - def SetPrevPos(self, sPos): - self.prevPos = sPos - def GetPrevPos(self): - if not ("prevPos" in dir(self)): - raise RuntimeError("Please call SetPrevPos first !!") - return self.prevPos - def SetDirection(self, dir): - " Fowards = 1, backwards = -1 " - self.direction = dir - def GetDirection(self): - if (self.GetStartMode() == 0): - if (self.direction > 0.1): - return 1 - else: - return -1 - else: - if ((self.GetEndPos() - self.GetStartPos()) > 0.1): - return 1 - else: - return -1 - - def _CurPos(self): - return self.piezos[self.GetScanChannel()][0].GetPos(self.piezos[self.GetScanChannel()][1]) - - def Verify(self): - if (self.GetStartPos() < self.piezos[self.GetScanChannel()][0].GetMin(self.piezos[self.GetScanChannel()][1])): - return (False, 'StartPos', 'StartPos is smaller than piezo minimum',self.piezos[self.GetScanChannel()][0].GetMin(self.piezos[self.GetScanChannel()][1])) - - if (self.GetStartPos() > self.piezos[self.GetScanChannel()][0].GetMax(self.piezos[self.GetScanChannel()][1])): - return (False, 'StartPos', 'StartPos is larger than piezo maximum',self.piezos[self.GetScanChannel()][0].GetMax(self.piezos[self.GetScanChannel()][1])) - if (self.GetEndPos() < self.piezos[self.GetScanChannel()][0].GetMin(self.piezos[self.GetScanChannel()][1])): - return (False, 'EndPos', 'EndPos is smaller than piezo minimum',self.piezos[self.GetScanChannel()][0].GetMin(self.piezos[self.GetScanChannel()][1])) - - if (self.GetEndPos() > self.piezos[self.GetScanChannel()][0].GetMax(self.piezos[self.GetScanChannel()][1])): - return (False, 'EndPos', 'EndPos is larger than piezo maximum',self.piezos[self.GetScanChannel()][0].GetMax(self.piezos[self.GetScanChannel()][1])) - - #if (self.GetEndPos() < self.GetStartPos()): - # return (False, 'EndPos', 'EndPos is before Startpos',self.GetStartPos()) - #stepsize limits are at present arbitrary == not really restricted to sensible values - #if (self.GetStepSize() < 0.001): - # return (False, 'StepSize', 'StepSize is smaller than piezo minimum',0.001) - - #if (self.GetStartPos() > 90): - # return (False, 'StepSize', 'Simplesequenceaquisator StepSize is larger than piezo maximum',90) - - return (True,) - - - def ProvideStackMetadata(self, mdh): - mdh.setEntry('StackSettings.StartPos', self.GetStartPos()) - mdh.setEntry('StackSettings.EndPos', self.GetEndPos()) - mdh.setEntry('StackSettings.StepSize', self.GetStepSize()) - mdh.setEntry('StackSettings.NumSlices', self.GetSeqLength()) - mdh.setEntry('StackSettings.ScanMode', ['Middle and Number', 'Start and End'][self.GetStartMode()]) - mdh.setEntry('StackSettings.ScanPiezo', self.piezos[self.GetScanChannel()][2]) - - mdh.setEntry('voxelsize.z', self.GetStepSize()) - - - def doStartLog(self): - #new metadata handling - self.mdh.setEntry('StartTime', time.time()) - - self.mdh.setEntry('AcquisitionType', 'Stack') - - #loop over all providers of metadata - for mdgen in MetaDataHandler.provideStartMetadata: - mdgen(self.mdh) - - ############################## - #old log stuff follows (DEPRECATED!!!) -# if not 'GENERAL' in self.log.keys(): -# self.log['GENERAL'] = {} -# if not 'PIEZOS' in self.log.keys(): -# self.log['PIEZOS'] = {} -# if not 'CAMERA' in self.log.keys(): -# self.log['CAMERA'] = {} -# -# self.log['PIEZOS']['Piezo'] = self.piezos[self.GetScanChannel()][2] -# self.log['PIEZOS']['Stepsize'] = self.GetStepSize() -# self.log['PIEZOS']['StartPos'] = self.GetStartPos() -# for pz in self.piezos: -# self.log['PIEZOS']['%s_Pos' % pz[2]] = pz[0].GetPos(pz[1]) -# -# #self.log['STEPPER']['XPos'] = m_pDoc->Step.GetNullX(), -# #m_pDoc->Step.GetPosX(), m_pDoc->Step.GetNullY(), m_pDoc->Step.GetPosY(), -# #m_pDoc->Step.GetNullZ(), m_pDoc->Step.GetPosZ()); -# self.cam.GetStatus() -# self.log['CAMERA']['Binning'] = self.cam.GetHorizBin() -# #m_pDoc->LogData.SetIntegTime(m_pDoc->Camera.GetIntegTime()); -# self.log['GENERAL']['Width'] = self.cam.GetPicWidth() -# self.log['GENERAL']['Height'] = self.cam.GetPicHeight() -# self.log['CAMERA']['ROIPosX'] = self.cam.GetROIX1() -# self.log['CAMERA']['ROIPosY'] = self.cam.GetROIY1() -# self.log['CAMERA']['ROIWidth'] = self.cam.GetROIX2() - self.cam.GetROIX1() -# self.log['CAMERA']['ROIHeight'] = self.cam.GetROIY2() - self.cam.GetROIY1() -# self.log['CAMERA']['StartCCDTemp'] = self.cam.GetCCDTemp() -# self.log['CAMERA']['StartElectrTemp'] = self.cam.GetElectrTemp() -# -# self.log['GENERAL']['NumChannels'] = self.ds.getNumChannels() -# self.log['GENERAL']['NumHWChans'] = self.numHWChans -# -# for ind in range(self.numHWChans): -# self.log['SHUTTER_%d' % ind] = {} -# self.log['SHUTTER_%d' % ind]['Name'] = self.chans.names[ind] -# self.log['SHUTTER_%d' % ind]['IntegrationTime'] = self.chans.itimes[ind] -# self.log['SHUTTER_%d' % ind]['Mask'] = self.hwChans[ind] -# -# s = '' -# bef = 0 -# if (self.cols[ind] & self.BW): -# s = s + 'BW' -# bef = 1 -# if (self.cols[ind] & self.RED): -# if bef: -# s = s + ' ' -# s = s + 'R' -# bef = 1 -# if (self.cols[ind] & self.GREEN1): -# if bef: -# s = s + ' ' -# s = s + 'G1' -# bef = 1 -# if (self.cols[ind] & self.GREEN2): -# if bef: -# s = s + ' ' -# s = s + 'G2' -# bef = 1 -# if (self.cols[ind] & self.BLUE): -# if bef: -# s = s + ' ' -# s = s + 'B' -# #bef = 1 -# self.log['SHUTTER_%d' % ind]['Colours'] = s -# -# dt = datetime.datetime.now() -# -# self.log['GENERAL']['Date'] = '%d/%d/%d' % (dt.day, dt.month, dt.year) -# self.log['GENERAL']['StartTime'] = '%d:%d:%d' % (dt.hour, dt.minute, dt.second) - #m_pDoc->LogData.SaveSeqROIMode(m_pDoc->Camera.GetROIMode()); - #pass - - def doStopLog(self): - self.mdh.setEntry('EndTime', time.time()) - - #loop over all providers of metadata - for mdgen in MetaDataHandler.provideStopMetadata: - mdgen(self.mdh) - -# self.log['GENERAL']['Depth'] = self.ds.getDepth() -# self.log['PIEZOS']['EndPos'] = self.GetEndPos() -# self.cam.GetStatus() -# self.log['CAMERA']['EndCCDTemp'] = self.cam.GetCCDTemp() -# self.log['CAMERA']['EndElectrTemp'] = self.cam.GetElectrTemp() -# -# dt = datetime.datetime.now() -# self.log['GENERAL']['EndTime'] = '%d:%d:%d' % (dt.hour, dt.minute, dt.second) - - diff --git a/PYME/Acquire/Loft/stepDialog.py b/PYME/Acquire/Loft/stepDialog.py deleted file mode 100755 index 9e66081fe..000000000 --- a/PYME/Acquire/Loft/stepDialog.py +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/python - -################## -# stepDialog.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#Boa:Dialog:stepDialog - -#from wxPython.wx. import * -import wx -import math -import time - -#redefine wx.Frame with a version that hides when someone tries to close it -#dirty trick, but lets the Boa gui builder still work with frames we do this to -#NB must come after 'from wx..... import *' !!! -#from noclosefr import * - -def create(parent): - return stepDialog(parent) - -[wxID_STEPDIALOG, wxID_STEPDIALOGBGOTO, wxID_STEPDIALOGBGOTOXY, - wxID_STEPDIALOGBON, wxID_STEPDIALOGCHSPEED, wxID_STEPDIALOGSTATICBOX1, - wxID_STEPDIALOGSTATICBOX2, wxID_STEPDIALOGSTATICBOX3, - wxID_STEPDIALOGSTATICBOX4, wxID_STEPDIALOGSTATICBOX5, wxID_STEPDIALOGTXPOS, - wxID_STEPDIALOGTYPOS, wxID_STEPDIALOGTZPOS, -] = [wx.NewId() for i in range(13)] - -class stepPanel(wx.Panel): - def _init_ctrls(self, prnt): - # generated method, don't edit - wx.Panel.__init__(self, id=wxID_STEPDIALOG, - parent=prnt, size=wx.Size(183, 222)) - self.SetClientSize(wx.Size(175, 195)) - #self.SetBackgroundColour(wx.Colour(209, 208, 203)) - - self.staticBox1 = wx.StaticBox(id=wxID_STEPDIALOGSTATICBOX1, - label='YPos', name='staticBox1', parent=self, pos=wx.Point(8, 56), - size=wx.Size(64, 48), style=0) - - self.tYPos = wx.TextCtrl(id=wxID_STEPDIALOGTYPOS, name='tYPos', - parent=self, pos=wx.Point(16, 72), size=wx.Size(48, 24), style=0, - value='0') - self.tYPos.Enable(True) - - self.tZPos = wx.TextCtrl(id=wxID_STEPDIALOGTZPOS, name='tZPos', - parent=self, pos=wx.Point(16, 120), size=wx.Size(48, 24), style=0, - value='0') - self.tZPos.Enable(True) - - self.staticBox2 = wx.StaticBox(id=wxID_STEPDIALOGSTATICBOX2, - label='ZPos', name='staticBox2', parent=self, pos=wx.Point(8, 104), - size=wx.Size(64, 48), style=0) - - self.tXPos = wx.TextCtrl(id=wxID_STEPDIALOGTXPOS, name='tXPos', - parent=self, pos=wx.Point(16, 24), size=wx.Size(48, 24), style=0, - value='0') - self.tXPos.Enable(True) - - self.staticBox3 = wx.StaticBox(id=wxID_STEPDIALOGSTATICBOX3, - label='XPos', name='staticBox3', parent=self, pos=wx.Point(8, 8), - size=wx.Size(64, 48), style=0) - - self.chSpeed = wx.Choice(choices=['1', '2', '3', '4', '5', '6', '7', '8', - '9', '10'], id=wxID_STEPDIALOGCHSPEED, name='chSpeed', - parent=self, pos=wx.Point(96, 40), size=wx.Size(56, 21), style=0) - self.chSpeed.SetSelection(0) - self.chSpeed.Bind(wx.EVT_CHOICE, self.OnChoiceSpeed) - - self.staticBox4 = wx.StaticBox(id=wxID_STEPDIALOGSTATICBOX4, - label='Speed', name='staticBox4', parent=self, pos=wx.Point(88, - 24), size=wx.Size(72, 48), style=0) - - self.bOn = wx.Button(id=wxID_STEPDIALOGBON, label='On', name='bOn', - parent=self, pos=wx.Point(96, 80), size=wx.Size(56, 23), style=0) - self.bOn.Bind(wx.EVT_BUTTON, self.OnButtonOn) - - self.staticBox5 = wx.StaticBox(id=wxID_STEPDIALOGSTATICBOX5, - label='Joystick', name='staticBox5', parent=self, pos=wx.Point(80, - 8), size=wx.Size(88, 104), style=0) - - self.bGoto = wx.Button(id=wxID_STEPDIALOGBGOTO, label='Goto', - name='bGoto', parent=self, pos=wx.Point(8, 160), size=wx.Size(64, - 23), style=0) - self.bGoto.Bind(wx.EVT_BUTTON, self.OnBGotoButton) - - self.bGotoXY = wx.Button(id=wxID_STEPDIALOGBGOTOXY, label='Goto XY', - name='bGotoXY', parent=self, pos=wx.Point(88, 120), size=wx.Size(75, - 23), style=0) - self.bGotoXY.Bind(wx.EVT_BUTTON, self.OnBGotoXYButton) - - def __init__(self, parent, scope): - self._init_ctrls(parent) - self.scope = scope - - self.chSpeed.SetSelection(self.scope.step.GetJoystickSpeed() -1) - - if self.scope.step.GetJoystickStatus(): - self.bOn.SetLabel('Off') - else: - self.bOn.SetLabel('On') - - def OnChoiceSpeed(self, event): - self.scope.step.SetJoystickSpeed(self.chSpeed.GetSelection() + 1) - self.scope.step.SetMoveSpeed(self.chSpeed.GetSelection() + 1) - #event.Skip() - - def OnButtonOn(self, event): - self.scope.step.SetJoystickOnOff() - if self.scope.step.GetJoystickStatus(): - self.bOn.SetLabel('Off') - else: - self.bOn.SetLabel('On') - #event.Skip() - - def OnBGotoButton(self, event): - # Goto -- with dodgy hack to get around problem with moves larger than 5mm - #jstat = self.scope.step.GetJoystickStatus() - #if (jstat): - # self.scope.step.SetJoystickOnOff() - - x0 = self.scope.step.GetPosX() - y0 = self.scope.step.GetPosY() - z0 = self.scope.step.GetPosZ() - - x = float(self.tXPos.GetValue()) - y = float(self.tYPos.GetValue()) - z = float(self.tZPos.GetValue()) - - print(('(%s, %s, %s)\n' % (x,y,z))) - - #dodgy hack ... - dist = math.sqrt(math.pow(x - x0, 2) + math.pow(y - y0,2) + math.pow(z - z0,2)) - - #while (dist > 4000): - # xt = x0 + (x - x0)*4000/dist - # yt = y0 + (y - y0)*4000/dist - # zt = z0 + (z - z0)*4000/dist - # - # self.scope.step.MoveTo(xt,yt,zt) - # - # time.sleep(1) -## -## self.scope.step.ContIO() -## x0 = self.scope.step.GetPosX() -## y0 = self.scope.step.GetPosY() -## z0 = self.scope.step.GetPosZ() -## -## dist = math.sqrt(math.pow(x - x0, 2) + math.pow(y - y0,2) + math.pow(z - z0,2)) - - self.scope.step.MoveTo(x,y,z) - - time.sleep(1) - - self.scope.step.ContIO() - - #if (jstat): - # self.scope.step.SetJoystickOnOff() - #event.Skip() - - def OnBGotoXYButton(self, event): - # Uses current ZPos - #jstat = self.scope.step.GetJoystickStatus() - #if (jstat): - # self.scope.step.SetJoystickOnOff() - - x0 = self.scope.step.GetPosX() - y0 = self.scope.step.GetPosY() - z0 = self.scope.step.GetPosZ() - - x = float(self.tXPos.GetValue()) - y = float(self.tYPos.GetValue()) - #z = float(self.tZPos.GetValue()) - z = self.scope.step.GetPosZ() - - print(('(%s, %s, %s)\n' % (x,y,z))) - - #dodgy hack ... - dist = math.sqrt(math.pow(x - x0, 2) + math.pow(y - y0,2) + math.pow(z - z0,2)) - print(( 'dist = %s' % (dist))) - -## while (dist > 4000): -## xt = x0 + (x - x0)*4000/dist -## yt = y0 + (y - y0)*4000/dist -## zt = z0 + (z - z0)*4000/dist -## -## print '(%s, %s, %s)\n' % (xt,yt,zt) -## -## self.scope.step.MoveTo(xt,yt,zt) -## -## time.sleep(1) -## -## self.scope.step.ContIO() -## x0 = self.scope.step.GetPosX() -## y0 = self.scope.step.GetPosY() -## z0 = self.scope.step.GetPosZ() -## -## dist = math.sqrt(math.pow(x - x0, 2) + math.pow(y - y0,2) + math.pow(z - z0,2)) -## print 'dist = %s' % (dist) - - - self.scope.step.MoveTo(x,y,z) - - time.sleep(1) - self.scope.step.ContIO() - - #if (jstat): - # self.scope.step.SetJoystickOnOff() - #event.Skip() diff --git a/PYME/Acquire/Loft/timeseqdialog.py b/PYME/Acquire/Loft/timeseqdialog.py deleted file mode 100755 index 19c992c3c..000000000 --- a/PYME/Acquire/Loft/timeseqdialog.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/python - -################## -# timeseqdialog.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -#Boa:Dialog:seqDialog -#from wxPython.wx import * -import wx -import timesequenceaquisator -#redefine wxFrame with a version that hides when someone tries to close it -#dirty trick, but lets the Boa gui builder still work with frames we do this to -#NB must come after 'from wx.... import *' !!! -from noclosefr import * -def create(parent): - return seqDialog(parent) -[wxID_TSEQDIALOG, wxID_TSEQDIALOGBSTART, wxID_TSEQDIALOGSTATICBOX5, - wxID_TSEQDIALOGSTATICBOX6, wxID_TSEQDIALOGSTMEMORY, wxID_TSEQDIALOGTNUMSLICES, - wxID_TSEQDIALOGTTIMESTEP, -] = map(lambda _init_ctrls: wx.NewId(), range(7)) -class seqDialog(wxFrame): - def _init_ctrls(self, prnt): - # generated method, don't edit - wxFrame.__init__(self, id=wxID_TSEQDIALOG, name='tseqDialog', parent=prnt, - pos=wx.Point(374, 265), size=wx.Size(259, 160), - style=wx.DEFAULT_FRAME_STYLE, title='Time Sequence') - self.SetClientSize(wx.Size(251, 133)) - self.SetBackgroundColour(wx.Colour(209, 208, 203)) - self.staticBox5 = wx.StaticBox(id=wxID_TSEQDIALOGSTATICBOX5, - label='# Slices', name='staticBox5', parent=self, pos=wx.Point(24, - 8), size=wx.Size(88, 48), style=0) - self.tNumSlices = wx.TextCtrl(id=wxID_TSEQDIALOGTNUMSLICES, - name='tNumSlices', parent=self, pos=wx.Point(40, 24), - size=wx.Size(64, 24), style=0, value='100') - #self.tNumSlices.Enable(False) - wx.EVT_KILL_FOCUS(self.tNumSlices, self.OnTNumSlicesKillFocus) - self.staticBox6 = wx.StaticBox(id=wxID_TSEQDIALOGSTATICBOX6, - label=' Time step (s)', name='staticBox6', parent=self, - pos=wx.Point(128, 8), size=wx.Size(88, 48), style=0) - self.tTimeStep = wx.TextCtrl(id=wxID_TSEQDIALOGTTIMESTEP, - name='tTimeStep', parent=self, pos=wx.Point(136, 24), - size=wx.Size(64, 24), style=0, value='0.1') - wx.EVT_KILL_FOCUS(self.tTimeStep, self.OnTStepSizeKillFocus) - self.bStart = wx.Button(id=wxID_TSEQDIALOGBSTART, label='Go, Go, Go !!! ', - name='bStart', parent=self, pos=wx.Point(80, 104), size=wx.Size(75, - 23), style=0) - wx.EVT_BUTTON(self.bStart, wxID_TSEQDIALOGBSTART, self.OnBStartButton) - self.stMemory = wx.StaticText(id=wxID_TSEQDIALOGSTMEMORY, - label='staticText1', name='stMemory', parent=self, pos=wx.Point(8, - 72), size=wx.Size(232, 16), style=0) - def __init__(self, parent, scope): - self.scope = scope - self._init_ctrls(parent) - - if not ('ta' in self.scope.__dict__): - self.scope.ta = timesequenceaquisator.TimeSequenceAquisitor(self.scope.chaninfo, self.scope.cam, self.scope.shutters) - - self.UpdateDisp() - - - def OnBStartButton(self, event): - res = self.scope.ta.Verify() - if res[0]: - self.scope.frameWrangler.stop() - #try: - self.scope.ta.Prepare() - #except: - # dialog = wxMessageDialog(None, 'The most likely reason is a lack of memory \nTry the following: Close any open aquisitions, Chose a ROI, Delete unnecessary channels, or decrease the # of slices', "Could not start aquisition", wx.OK) - # dialog.ShowModal() - # self.scope.ta.ds=[] - # self.scope.frameWrangler.Prepare(True) - # self.scope.frameWrangler.start() - self.scope.ta.WantFrameNotification=[] - self.scope.ta.WantFrameNotification.append(self.scope.aqt_refr) - self.scope.ta.WantStopNotification=[] - self.scope.ta.WantStopNotification.append(self.scope.aqt_end) - self.scope.ta.start() - self.scope.pb = wx.ProgressDialog('Aquisition in progress ...', 'Slice 1 of %d' % self.scope.ta.ds.getDepth(), self.scope.ta.ds.getDepth(), style = wx.PD_APP_MODAL|wx.PD_AUTO_HIDE|wx.PD_REMAINING_TIME|wx.PD_CAN_ABORT) - - - - - - else: - dialog = wx.MessageDialog(None, res[2] + ' (%2.3f)'% res[3], "Parameter Error", wx.OK) - dialog.ShowModal() - - if res[1] == 'StepSize': - self.tStepSize.SetFocus() - elif (self.scope.ta.GetStartMode() == self.scope.ta.CENTRE_AND_LENGTH): - self.tNumSlices.SetFocus() - elif (res[1] == 'StartPos'): - self.tStPos.SetFocus() - else: - self.tEndPos.SetFocus() - - - #event.Skip() - - def OnTNumSlicesKillFocus(self, event): - self.scope.ta.SetSeqLength(int(self.tNumSlices.GetValue())) - self.UpdateDisp() - #event.Skip() - def OnTStepSizeKillFocus(self, event): - self.scope.ta.SetTimeStep(float(self.tTimeStep.GetValue())) - self.UpdateDisp() - #event.Skip() - - def UpdateDisp(self): - - self.tTimeStep.SetValue('%2.3f' % self.scope.ta.GetTimeStep()) - self.tNumSlices.SetValue('%d' % self.scope.ta.GetSeqLength()) - self.stMemory.SetLabel('Required Memory: %2.3f MB' % (self.scope.cam.GetPicWidth()*self.scope.cam.GetPicHeight()*self.scope.ta.GetSeqLength()*2*self.scope.ta.getReqMemChans(self.scope.ta.chans.cols)/(1024.0*1024.0))) diff --git a/PYME/Acquire/Loft/timesequenceaquisator.py b/PYME/Acquire/Loft/timesequenceaquisator.py deleted file mode 100755 index d5c4e2a34..000000000 --- a/PYME/Acquire/Loft/timesequenceaquisator.py +++ /dev/null @@ -1,136 +0,0 @@ -#!/usr/bin/python - -################## -# timesequenceaquisator.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import math -import datetime -from previewaquisator import PreviewAquisator -class TimeSequenceAquisitor(PreviewAquisator): - # 'Constants' - - def __init__(self,chans, cam, shutters,_log={}): - PreviewAquisator.__init__(self, chans, cam,shutters, None) - #self.piezos = _piezos - self.log = _log - - - self.SeqLength = 100 - self.TimeStep = 0.1 - def getNextDsSlice(self): - return self.ds.nextZ() - def SetSeqLength(self,iLength): - self.SeqLength = iLength - def GetSeqLength(self): - return self.SeqLength - - def SetTimeStep(self,iLength): - self.TimeStep = iLength - def GetTimeStep(self): - return self.TimeStep - - def start(self): - PreviewAquisator.start(self,1000*self.TimeStep) - - def Verify(self): - return (True,) - def doStartLog(self): - if not 'GENERAL' in self.log.keys(): - self.log['GENERAL'] = {} - if not 'PIEZOS' in self.log.keys(): - self.log['PIEZOS'] = {} - if not 'CAMERA' in self.log.keys(): - self.log['CAMERA'] = {} - - #self.log['PIEZOS']['Piezo'] = self.piezos[self.GetScanChannel()][2] - #self.log['PIEZOS']['Stepsize'] = self.GetStepSize() - #self.log['PIEZOS']['StartPos'] = self.GetStartPos() - #for pz in self.piezos: - # self.log['PIEZOS']['%s_Pos' % pz[2]] = pz[0].GetPos(pz[1]) - - #self.log['STEPPER']['XPos'] = m_pDoc->Step.GetNullX(), - #m_pDoc->Step.GetPosX(), m_pDoc->Step.GetNullY(), m_pDoc->Step.GetPosY(), - #m_pDoc->Step.GetNullZ(), m_pDoc->Step.GetPosZ()); - self.cam.GetStatus() - self.log['CAMERA']['Binning'] = self.cam.GetHorizBin() - #m_pDoc->LogData.SetIntegTime(m_pDoc->Camera.GetIntegTime()); - self.log['GENERAL']['Width'] = self.cam.GetPicWidth() - self.log['GENERAL']['Height'] = self.cam.GetPicHeight() - self.log['CAMERA']['ROIPosX'] = self.cam.GetROIX1() - self.log['CAMERA']['ROIPosY'] = self.cam.GetROIY1() - self.log['CAMERA']['ROIWidth'] = self.cam.GetROIX2() - self.cam.GetROIX1() - self.log['CAMERA']['ROIHeight'] = self.cam.GetROIY2() - self.cam.GetROIY1() - self.log['CAMERA']['StartCCDTemp'] = self.cam.GetCCDTemp() - self.log['CAMERA']['StartElectrTemp'] = self.cam.GetElectrTemp() - - self.log['GENERAL']['NumChannels'] = self.ds.getNumChannels() - self.log['GENERAL']['NumHWChans'] = self.numHWChans - - for ind in range(self.numHWChans): - self.log['SHUTTER_%d' % ind] = {} - self.log['SHUTTER_%d' % ind]['Name'] = self.chans.names[ind] - self.log['SHUTTER_%d' % ind]['IntegrationTime'] = self.chans.itimes[ind] - self.log['SHUTTER_%d' % ind]['Mask'] = self.hwChans[ind] - - s = '' - bef = 0 - if (self.cols[ind] & self.BW): - s = s + 'BW' - bef = 1 - if (self.cols[ind] & self.RED): - if bef: - s = s + ' ' - s = s + 'R' - bef = 1 - if (self.cols[ind] & self.GREEN1): - if bef: - s = s + ' ' - s = s + 'G1' - bef = 1 - if (self.cols[ind] & self.GREEN2): - if bef: - s = s + ' ' - s = s + 'G2' - bef = 1 - if (self.cols[ind] & self.BLUE): - if bef: - s = s + ' ' - s = s + 'B' - #bef = 1 - self.log['SHUTTER_%d' % ind]['Colours'] = s - - dt = datetime.datetime.now() - - self.log['GENERAL']['Date'] = '%d/%d/%d' % (dt.day, dt.month, dt.year) - self.log['GENERAL']['StartTime'] = '%d:%d:%d' % (dt.hour, dt.minute, dt.second) - #m_pDoc->LogData.SaveSeqROIMode(m_pDoc->Camera.GetROIMode()); - #pass - def doStopLog(self): - self.log['GENERAL']['Depth'] = self.ds.getDepth() - #self.log['PIEZOS']['EndPos'] = self.GetEndPos() - self.cam.GetStatus() - self.log['CAMERA']['EndCCDTemp'] = self.cam.GetCCDTemp() - self.log['CAMERA']['EndElectrTemp'] = self.cam.GetElectrTemp() - - dt = datetime.datetime.now() - self.log['GENERAL']['EndTime'] = '%d:%d:%d' % (dt.hour, dt.minute, dt.second) - - diff --git a/PYME/Acquire/PYMEAcquire.py b/PYME/Acquire/PYMEAcquire.py index 185f0f4ab..e314ba7ea 100755 --- a/PYME/Acquire/PYMEAcquire.py +++ b/PYME/Acquire/PYMEAcquire.py @@ -32,39 +32,60 @@ If run without an intialisation file it defaults to using simulated hardware. """ +from PYME.misc import big_sur_fix +from PYME.ui import patch_traitsui #!/usr/bin/python import wx -import matplotlib +#import matplotlib #matplotlib.use('WXAgg') -from PYME.Acquire import acquiremainframe #from PYME import mProfile +#make wx less spammy with warnings +import warnings +warnings.simplefilter('once', wx.wxPyDeprecationWarning) + import os -import json import logging import logging.config +import os -def setup_logging( - default_path='logging.json', - default_level=logging.DEBUG, - env_key='LOG_CFG' - ): +import PYME.config as pyme_config + +def setup_logging(default_level=logging.DEBUG): """Setup logging configuration """ - path = os.path.join(os.path.split(__file__)[0], default_path) + import yaml + import PYME.config + from PYME.util import log_verbosity + default_config_file = os.path.join(os.path.split(__file__)[0], 'logging.yaml') + + path = PYME.config.get('Acquire-logging_conf_file', default_config_file) print('attempting to load load logging config from %s' % path) - value = os.getenv(env_key, None) - if value: - path = value + if os.path.exists(path): with open(path, 'rt') as f: - config = json.load(f) + config = yaml.safe_load(f) + + if pyme_config.get('Acquire-logging_include_pid', False): + # replace the filename in the file handler with a version that includes the PID + # this is useful for debugging multiple instances of PYMEAcquire + # The caveat is that each time you run the program you will get a new log file + # and these will accumulate until manually deleted / removed. + # To enable, add the following line to ~/.PYME/config.yaml + # Acquire-logging_include_pid: True + try: + config['handlers']['file']['filename'] = config['handlers']['file']['filename'].replace('.log', f'_{os.getpid()}.log') + except KeyError: + print('No file handler in logging config - skipping PID replacement') + logging.config.dictConfig(config) else: logging.basicConfig(level=default_level) + # suppress excessively verbose logging in dependency packages + log_verbosity.patch_log_verbosity() class BoaApp(wx.App): def __init__(self, options, *args): @@ -73,36 +94,58 @@ def __init__(self, options, *args): def OnInit(self): - #wx.InitAllImageHandlers() + if self.options.server: + from PYME.Acquire import acquirewx as acquiremainframe + else: + from PYME.Acquire import acquiremainframe + self.main = acquiremainframe.create(None, self.options) - #self.main.Show() self.SetTopWindow(self.main) + + if self.options.browser: + import webbrowser + webbrowser.open('http://localhost:8999') #FIXME - delay this until server is up return True def main(): import os import sys - from optparse import OptionParser + #from optparse import OptionParser + import argparse setup_logging() from PYME import config logger = logging.getLogger(__name__) - parser = OptionParser() - parser.add_option("-i", "--init-file", dest="initFile", + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--init-file", dest="initFile", help="Read initialisation from file [defaults to init.py]", metavar="FILE", default='init.py') - parser.add_option("-m", "--gui_mode", dest="gui_mode", default='default', + parser.add_argument("-m", "--gui_mode", dest="gui_mode", default='default', help="GUI mode for PYMEAcquire - either default or 'compact'") - parser.add_option("-t", "--title", dest="window_title", default='PYME Acquire', + parser.add_argument("-t", "--title", dest="window_title", default='PYME Acquire', help="Set the PYMEAcquire display name (useful when running multiple copies - e.g. for drift tracking)") + + parser.add_argument('-p', '--port', dest='port', default=8999, help='port to use for server functions') + parser.add_argument('-a', '--bind_addr', dest='bind_addr', default=None, help='address to bind to for server functions (defaults to localhost). Only bind to an external address if you are on a trusted network and *really* know what you are doing. NB - university networks should generally not be trusted.') + parser.add_argument('-s', '--server', dest='server', default=False, action='store_true', help='run in server mode') + parser.add_argument('-b', '--browser', dest='browser', default=False, action='store_true', help='launch web browser based ui') + parser.add_argument('-e', '--threaded_event_loop', dest='threaded_event_loop', default=False, action='store_true', help='Run hardware event loop in separate thread. Required (and implied) for server mode.') + parser.add_argument('-I', '--ipy', dest='ipy', default=False, action='store_true', help='launch ipython server for remote control') + parser.add_argument('--no-wx', dest='no_wx', default=False, action='store_true', help='run without wx gui') + #(options, args) = parser.parse_args() + + options = parser.parse_args() + + print(options) + + if options.server: + options.threaded_event_loop = True - (options, args) = parser.parse_args() - # continue to support loading scripts from the PYMEAcquire/Scripts directory legacy_scripts_dir = os.path.join(os.path.dirname(__file__), 'Scripts') @@ -118,8 +161,20 @@ def main(): logger.debug('using initialization script %s, %s' % (init_file, os.path.abspath(init_file))) - application = BoaApp(options, 0) - application.MainLoop() + if options.no_wx: + #implies server mode (with or without a browser GUI) + assert options.server + + from PYME.Acquire.acquire_server import AcquireHTTPServer + server = AcquireHTTPServer(options, port=int(options.port), bind_addr=options.bind_addr) + if options.browser: + import webbrowser + webbrowser.open('http://localhost:8999') #FIXME - delay this until server is up + + server.run() + else: + application = BoaApp(options, 0) + application.MainLoop() if __name__ == '__main__': from PYME.util import mProfile, fProfile diff --git a/PYME/Acquire/Protocols/.cvsignore b/PYME/Acquire/Protocols/.cvsignore deleted file mode 100644 index 1d907a708..000000000 --- a/PYME/Acquire/Protocols/.cvsignore +++ /dev/null @@ -1,42 +0,0 @@ -simulPAZStack.pyo -ZStackQ.pyo -prebleach671ND1-250frames.pyc -tile.pyc -dual671_470.pyo -prebleach671ND2-250frames.pyc -spdscan.pyc -ZStack488.pyc -darkThenOn.pyc -prebleach671-250frames.py -standard671_no532.pyo -ZStackQ.pyc -__init__.pyo -simul488.pyo -prebleach671ND2.pyc -dual671_470.pyc -dual671_488.pyo -prebleach671-250frames.pyc -tile671.pyc -simul488.pyc -standard671_no532.pyc -standard671.pyc -standard470.pyo -prebleach671ND1-250f-NDstep.pyc -ZStack488.pyo -prebleach671ND1.pyc -__init__.pyc -simulPA.pyc -prebleach671ND2-250frames.py -standard671.pyo -darkThenOn.pyo -ZStack671.pyo -prebleach671ND1-250frames.py -prebleach671ND1-250f-NDstep.py -dual671_488.pyc -standard470.pyc -standard488.pyo -simulPAZStack.pyc -standard488.pyc -simulPA.pyo -ZStack671.pyc -ZStack.pyc diff --git a/PYME/Acquire/Protocols/htsms-calibration.py b/PYME/Acquire/Protocols/htsms-cal-psf.py similarity index 64% rename from PYME/Acquire/Protocols/htsms-calibration.py rename to PYME/Acquire/Protocols/htsms-cal-psf.py index cd82615e5..624648977 100644 --- a/PYME/Acquire/Protocols/htsms-calibration.py +++ b/PYME/Acquire/Protocols/htsms-cal-psf.py @@ -3,8 +3,17 @@ # T(when, what, *args) creates a new task. "when" is the frame number, "what" is a function to # be called, and *args are any additional arguments. taskList = [ - T(-1, scope.l642.TurnOn), - T(-1, scope.l560.TurnOn), + T(-1, scope.state.update, { + 'Lasers.MPB560.On': True, + 'Lasers.MPB560.Power': 0.0, + 'Lasers.MPB642.On': True, + 'Lasers.MPB642.Power': 0.0, + 'Lasers.OBIS405.On': False, + 'Lasers.OBIS488.On': False, + 'Multiview.ActiveViews': [0, 1, 2, 3], + 'Multiview.ROISize': [256, 256], + 'Camera.IntegrationTime': 0.1, + }), T(0, scope.focus_lock.DisableLock), T(maxint, scope.turnAllLasersOff), # T(maxint, scope.focus_lock.EnableLock), @@ -19,5 +28,5 @@ # must be defined for protocol to be discovered PROTOCOL = TaskListProtocol(taskList, metaData, preflight, filename=__file__) -PROTOCOL_STACK = ZStackTaskListProtocol(taskList, 1, 3, metaData, preflight, slice_order='triangle', +PROTOCOL_STACK = ZStackTaskListProtocol(taskList, 1, 3, metaData, preflight, slice_order='saw', require_camera_restart=True, filename=__file__) \ No newline at end of file diff --git a/PYME/Acquire/Protocols/htsms-cal-registration.py b/PYME/Acquire/Protocols/htsms-cal-registration.py new file mode 100644 index 000000000..3b544b997 --- /dev/null +++ b/PYME/Acquire/Protocols/htsms-cal-registration.py @@ -0,0 +1,52 @@ +from PYME.Acquire.protocol import * +from PYME.Acquire.Utils.pointScanner import PointScanner +import numpy as np +import logging + +logger = logging.getLogger(__name__) + +class Scanner(PointScanner): + def __init__(self, steps=10): + self._steps = steps + + def setup(self): + fs = np.array((scope.cam.size_x, scope.cam.size_y)) + fov_size = 2 * np.mean(fs * np.array(scope.GetPixelSize())) + + PointScanner.__init__(self, scope, self._steps, + fov_size/self._steps, 1, 0, + False, True, trigger=True, + stop_on_complete=True, + return_to_start=True) + self.on_stop.connect(scope.spoolController.StopSpooling) + self.genCoords() + +scanner = Scanner() + +taskList = [ + T(-1, scope.state.update, { + 'Lasers.MPB560.On': True, + 'Lasers.MPB560.Power': 0.0, + 'Lasers.MPB642.On': True, + 'Lasers.MPB642.Power': 0.0, + 'Lasers.OBIS405.On': False, + 'Lasers.OBIS488.On': False, + 'Multiview.ActiveViews': [0, 1, 2, 3], + 'Multiview.ROISize': [256, 256], + 'Camera.IntegrationTime': 0.1, + }), + T(-1, scanner.setup), + T(0, scanner.start), + T(maxint, scope.turnAllLasersOff), +] + +metaData = [ + ('Protocol.DataStartsAt', 0), +] + +preflight = [] # no preflight checks + +# must be defined for protocol to be discovered +PROTOCOL = TaskListProtocol(taskList, metaData, preflight, filename=__file__) +PROTOCOL_STACK = ZStackTaskListProtocol(taskList, 1, 3, metaData, preflight, slice_order='saw', + require_camera_restart=True, filename=__file__) diff --git a/PYME/Acquire/Protocols/htsms-tile.py b/PYME/Acquire/Protocols/htsms-tile.py index 7b537eb87..b24b101aa 100644 --- a/PYME/Acquire/Protocols/htsms-tile.py +++ b/PYME/Acquire/Protocols/htsms-tile.py @@ -43,16 +43,17 @@ def check_focus_lock_ok(self): # T(frame, function, *args) creates a new task taskList = [ - T(-1, scope.turnAllLasersOff), T(-1, scope.state.update, { 'Lasers.OBIS405.Power': 1.0, + 'Lasers.OBIS405.On': True, 'Multiview.ActiveViews': [1], 'Multiview.ROISize': [256, 256], - 'Camera.IntegrationTime': 0.005, + 'Camera.IntegrationTime': 0.02, }), + T(-1, scope._stage_leveler.acquire_focus_lock), T(-1, scanner.setup), - T(-1, scope.l405.TurnOn), T(0, scanner.start), + T(1, scope.l405.TurnOn), # ~superstitious, but the obis is a punk sometimes T(maxint, scope.turnAllLasersOff), ] diff --git a/PYME/Acquire/Protocols/shiftfield.py b/PYME/Acquire/Protocols/shiftfield.py index e80d2b42b..0c0462f86 100644 --- a/PYME/Acquire/Protocols/shiftfield.py +++ b/PYME/Acquire/Protocols/shiftfield.py @@ -162,7 +162,7 @@ def ShowSFDialog(): #stop after one full scan stopTask.when = 23 + 2*ps.imsize - print((stopTask.when)) + #print((stopTask.when)) def toggle_joystick(enabled=False): diff --git a/PYME/Acquire/Protocols/simul642.py b/PYME/Acquire/Protocols/simul642.py new file mode 100644 index 000000000..af6e2f6bd --- /dev/null +++ b/PYME/Acquire/Protocols/simul642.py @@ -0,0 +1,63 @@ +#!/usr/bin/python + +################## +# simul642.py +# +# This protocol is intended for use with the `init_sim_htsms.py` init script +# for a simulated high-throughput SMLM acquisition with cluster-based analysis. +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + +#import all the stuff to make this work +from PYME.Acquire.protocol import * +import numpy + +#define a list of tasks, where T(when, what, *args) creates a new task +#when is the frame number, what is a function to be called, and *args are any +#additional arguments +taskList = [ +T(-1, scope.turnAllLasersOff), +T(-1, scope.state.update, { + 'Multiview.ActiveViews': [0, 1, 2, 3], + 'Multiview.ROISize': [256, 256], + 'Camera.IntegrationTime': 0.01, + }), +T(20, scope.state.update, {'Lasers.l642.Power' : 1000, 'Lasers.l642.On' : True, }), +# T(30, MainFrame.pan_spool.OnBAnalyse, None), +T(maxint, scope.turnAllLasersOff) +] + +#optional - metadata entries +metaData = [ +('Protocol.DarkFrameRange', (0, 20)), +('Protocol.DataStartsAt', 21) +] + +#optional - pre-flight check +#a list of checks which should be performed prior to launching the protocol +#syntax: C(expression to evaluate (quoted, should have boolean return), message to display on failure), +preflight = [ +#C('scope.cam.GetEMGain() == 150', 'Was expecting an intial e.m. gain of 150'), +#C('scope.cam.GetROIX1() > 0', 'Looks like no ROI has been set'), +#C('scope.cam.GetIntegTime() <= 50', 'Camera integration time may be too long'), +] + +#must be defined for protocol to be discovered +PROTOCOL = TaskListProtocol(taskList, metaData, preflight) +PROTOCOL_STACK = ZStackTaskListProtocol(taskList, 20, 100, metaData, preflight, randomise = False) diff --git a/PYME/Acquire/Protocols/simul642HTSMS.py b/PYME/Acquire/Protocols/simul642HTSMS.py new file mode 100644 index 000000000..c021e7f9e --- /dev/null +++ b/PYME/Acquire/Protocols/simul642HTSMS.py @@ -0,0 +1,63 @@ +#!/usr/bin/python + +################## +# simul642.py +# +# This protocol is intended for use with the `init_sim_htsms.py` init script +# for a simulated high-throughput SMLM acquisition with cluster-based analysis. +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + +#import all the stuff to make this work +from PYME.Acquire.protocol import * +import numpy + +#define a list of tasks, where T(when, what, *args) creates a new task +#when is the frame number, what is a function to be called, and *args are any +#additional arguments +taskList = [ +T(-1, scope.turnAllLasersOff), +T(-1, scope.state.update, { + 'Multiview.ActiveViews': [0, 1, 2, 3], + 'Multiview.ROISize': [256, 256], + 'Camera.IntegrationTime': 0.01, + }), +T(20, scope.state.update, {'Lasers.l642.Power' : 1000, 'Lasers.l642.On' : True, }), +T(30, MainFrame.pan_spool.OnBAnalyse, None), +T(maxint, scope.turnAllLasersOff) +] + +#optional - metadata entries +metaData = [ +('Protocol.DarkFrameRange', (0, 20)), +('Protocol.DataStartsAt', 21) +] + +#optional - pre-flight check +#a list of checks which should be performed prior to launching the protocol +#syntax: C(expression to evaluate (quoted, should have boolean return), message to display on failure), +preflight = [ +#C('scope.cam.GetEMGain() == 150', 'Was expecting an intial e.m. gain of 150'), +#C('scope.cam.GetROIX1() > 0', 'Looks like no ROI has been set'), +#C('scope.cam.GetIntegTime() <= 50', 'Camera integration time may be too long'), +] + +#must be defined for protocol to be discovered +PROTOCOL = TaskListProtocol(taskList, metaData, preflight) +PROTOCOL_STACK = ZStackTaskListProtocol(taskList, 20, 100, metaData, preflight, randomise = False) diff --git a/PYME/Acquire/QueueSpooler.py b/PYME/Acquire/QueueSpooler.py deleted file mode 100644 index 59810c5f5..000000000 --- a/PYME/Acquire/QueueSpooler.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/python - -################## -# QueueSpooler.py -# -# Copyright David Baddeley, 2009 -# d.baddeley@auckland.ac.nz -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# -################## - -import tables -from PYME.IO import MetaDataHandler -import os -import time - -import PYME.Acquire.Spooler as sp -#from PYME.Acquire import protocol as p -from PYME.IO.FileUtils import fileID - -try: - from PYME.misc import hybrid_ns - - ns = hybrid_ns.getNS() -except ImportError: - ns = None - -#rom PYME.Acquire import eventLog - -class SpoolEvent(tables.IsDescription): - EventName = tables.StringCol(32) - Time = tables.Time64Col() - EventDescr = tables.StringCol(256) - -class EventLogger: - def __init__(self, spool, tq, queueName): - self.spooler = spool - #self.scope = scope - self.tq = tq - self.queueName = queueName - - def logEvent(self, eventName, eventDescr = '', timestamp=None): - if eventName == 'StartAq': - eventDescr = '%d' % self.spooler.imNum - - if timestamp is None: - timestamp = sp.timeFcn() - self.tq.logQueueEvent(self.queueName, (eventName, eventDescr, timestamp)) - - -class Spooler(sp.Spooler): - def __init__(self, filename, frameSource, frameShape, complevel=6, complib='zlib', **kwargs): -# if 'PYME_TASKQUEUENAME' in os.environ.keys(): -# taskQueueName = os.environ['PYME_TASKQUEUENAME'] -# else: -# taskQueueName = 'taskQueue' - import Pyro.core - - from PYME.misc.computerName import GetComputerName - compName = GetComputerName() - - taskQueueName = 'TaskQueues.%s' % compName - - if ns: - URI = ns.resolve(taskQueueName) - else: - URI = 'PYRONAME://' + taskQueueName - - self.tq = Pyro.core.getProxyForURI(URI) - self.tq._setOneway(['postTask', 'postTasks', 'addQueueEvents', 'setQueueMetaData', 'logQueueEvent']) - - self.seriesName = filename - self.buffer = [] - self.buflen = 30 - - self.tq.createQueue('HDFTaskQueue',self.seriesName, filename, frameSize = frameShape, complevel=complevel, complib=complib) - - self.md = MetaDataHandler.QueueMDHandler(self.tq, self.seriesName) - self.evtLogger = EventLogger(self, self.tq, self.seriesName) - - sp.Spooler.__init__(self, filename, frameSource, **kwargs) - - def OnFrame(self, sender, frameData, **kwargs): - if not self.watchingFrames: - #we have already disconnected - return - - #self.tq.postTask(cSMI.CDataStack_AsArray(caller.ds, 0).reshape(1,self.scope.cam.GetPicWidth(),self.scope.cam.GetPicHeight()), self.seriesName) - - # NOTE: copy is now performed in frameWrangler, so we don't need to worry about it here - if frameData.shape[0] == 1: - self.buffer.append(frameData) - else: - self.buffer.append(frameData.reshape(1,frameData.shape[0],frameData.shape[1])) - - if self.imNum == 0: #first frame - self.md.setEntry('imageID', fileID.genFrameID(self.buffer[-1].squeeze())) - - if (len(self.buffer) >= self.buflen): - self.FlushBuffer() - - sp.Spooler.OnFrame(self) - - def FlushBuffer(self): - t1 = time.time() - self.tq.postTasks(self.buffer, self.seriesName) - #print time.time() -t1 - self.buffer = [] - - - - diff --git a/PYME/Acquire/Scripts/.cvsignore b/PYME/Acquire/Scripts/.cvsignore deleted file mode 100644 index eb375c884..000000000 --- a/PYME/Acquire/Scripts/.cvsignore +++ /dev/null @@ -1,12 +0,0 @@ -relaxTest5.pyo -relaxTest.pyo -init_TIRF.pyo -init_smi1.pyo -activeTest2.pyo -relaxTest4.pyo -calibCCD.pyo -activeTest.pyo -init_twophoton.pyo -relaxTest3.pyo -init.pyo -relaxTest2.pyo diff --git a/PYME/Acquire/Scripts/init.py b/PYME/Acquire/Scripts/init.py index 7dc74fe9d..06a6b124e 100755 --- a/PYME/Acquire/Scripts/init.py +++ b/PYME/Acquire/Scripts/init.py @@ -38,17 +38,24 @@ @init_hardware('Fake Piezos') def pz(scope): from PYME.Acquire.Hardware.Simulator import fakePiezo - scope.fakePiezo = fakePiezo.FakePiezo(100) + from PYME.Acquire.Hardware.Piezos import offsetPiezoREST + + scope._fakePiezo = fakePiezo.FakePiezo(100) + #scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + scope.fakePiezo = offsetPiezoREST.server_class()(scope._fakePiezo) scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + - scope.fakeXPiezo = fakePiezo.FakePiezo(100) + scope.fakeXPiezo = fakePiezo.FakePiezo(10000) scope.register_piezo(scope.fakeXPiezo, 'x') - scope.fakeYPiezo = fakePiezo.FakePiezo(100) + scope.fakeYPiezo = fakePiezo.FakePiezo(10000) scope.register_piezo(scope.fakeYPiezo, 'y') pz.join() #piezo must be there before we start camera + @init_hardware('Fake Camera') def cm(scope): import numpy as np @@ -117,7 +124,8 @@ def samp_db(MainFrame, scope): @init_gui('Fake DMD') def fake_dmd(MainFrame, scope): - from PYMEnf.Hardware import FakeDMD, DMDGui + from PYMEnf.Hardware import FakeDMD + from PYME.Acquire.Hardware import DMDGui scope.LC = FakeDMD.FakeDMD(scope) LCGui = DMDGui.DMDPanel(MainFrame,scope.LC, scope) @@ -132,7 +140,7 @@ def fake_dmd(MainFrame, scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -151,11 +159,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - #MainFrame.time1.WantNotification.append(lcf.update) + #MainFrame.time1.register_callback(lcf.update) #MainFrame.camPanels.append((lcf, 'Laser Control')) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Laser Control')) @init_gui('Focus Keys') @@ -176,13 +184,37 @@ def action_manager(MainFrame, scope): ap = actionUI.ActionPanel(MainFrame, scope.actions, scope) MainFrame.AddPage(ap, caption='Queued Actions') +@init_hardware('Tiling') +def tiling(scope): + from PYME.Acquire.Utils import tiler + scope.spoolController.register_acquisition_type('Tiling', tiler.TileAcquisition) + + from PYME.Acquire import xyztc + scope.spoolController.register_acquisition_type('ZTiling', xyztc.TiledZStackAcquisition) @init_gui('Tiling') -def action_manager(MainFrame, scope): - from PYME.Acquire.ui import tile_panel +def tiling(MainFrame, scope): + from PYME.Acquire.ui import tilesettingsui - ap = tile_panel.TilePanel(MainFrame, scope) - MainFrame.aqPanels.append((ap, 'Tiling')) + ts = tilesettingsui.TileSettingsUI(MainFrame, scope) + MainFrame.register_acquisition_ui('Tiling', (ts, 'Tiling')) + + ts2 = tilesettingsui.ZTileSettingsUI(MainFrame, scope) + MainFrame.register_acquisition_ui('ZTiling', (ts2, 'Tiled Z Stack')) + +# @init_gui('Tiling') +# def action_manager(MainFrame, scope): +# from PYME.Acquire.ui import tile_panel + +# ap = tile_panel.TilePanel(MainFrame, scope) +# MainFrame.aqPanels.append((ap, 'Tiling')) + +@init_gui('Automated analysis') +def chained_analysis(main_frame, scope): + from PYME.Acquire.htsms import rule_ui_v2 + + rule_ui_v2.plug(main_frame, scope) + #must be here!!! diff --git a/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py b/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py index 734059c88..cf05cc2ba 100644 --- a/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py +++ b/PYME/Acquire/Scripts/init_N1_Ti_Exeter_ZIx.py @@ -118,7 +118,7 @@ def GetComputerName(): InitGUI(''' scope.camControls['Zyla'] = AndorNeoControlFrame.AndorNeoPanel(MainFrame, scope.cameras['Zyla'], scope) camPanels.append((scope.camControls['Zyla'], 'Zyla Properties')) -time1.WantNotification.append(scope.camControls['Zyla'].refresh) +time1.register_callback(scope.camControls['Zyla'].refresh) #scope.camControls['B - Right'] = AndorControlFrame.AndorPanel(MainFrame, scope.cameras['B - Right'], scope) #camPanels.append((scope.camControls['B - Right'], 'EMCCD B Properties')) @@ -185,7 +185,7 @@ class chaninfo: pt = positionTracker.PositionTracker(scope, time1) pv = positionTracker.TrackerPanel(MainFrame, pt) MainFrame.AddPage(page=pv, select=False, caption='Track') -time1.WantNotification.append(pv.draw) +time1.register_callback(pv.draw) ''') #splitter @@ -214,7 +214,7 @@ class chaninfo: TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) toolPanels.append((TiPanel, 'Nikon Ti')) -time1.WantNotification.append(TiPanel.SetSelections) +time1.register_callback(TiPanel.SetSelections) MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -223,7 +223,7 @@ class chaninfo: InitGUI(''' from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) ''') #from PYME.Acquire.Hardware import frZStage @@ -313,14 +313,14 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) ''') InitGUI(''' from PYME.Acquire import lasersliders lsf = lasersliders.LaserSliders(toolPanel, scope.lasers) -time1.WantNotification.append(lsf.update) +time1.register_callback(lsf.update) #lsf.update() camPanels.append((lsf, 'Laser Powers')) ''') @@ -331,7 +331,7 @@ class chaninfo: scope.arclampshutter = priorLumen.PriorLumen('Arc lamp shutter', portname=hwconfig['Lumen200S'].portname()) scope.shuttercontrol = [scope.arclampshutter] acf = arclampshutterpanel.Arclampshutterpanel(MainFrame,scope.shuttercontrol) - time1.WantNotification.append(acf.refresh) + time1.register_callback(acf.refresh) camPanels.append((acf, 'Shutter Control')) except: print('Error starting arc-lamp shutter ...') diff --git a/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py b/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py index 9c19f0df6..843359467 100644 --- a/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py +++ b/PYME/Acquire/Scripts/init_N2_Ti_Zyla_lasers.py @@ -93,9 +93,9 @@ class chaninfo: TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) toolPanels.append((TiPanel, 'Nikon Ti')) -#time1.WantNotification.append(TiPanel.SetSelections) -time1.WantNotification.append(scope.dichroic.Poll) -time1.WantNotification.append(scope.lightpath.Poll) +#time1.register_callback(TiPanel.SetSelections) +time1.register_callback(scope.dichroic.Poll) +time1.register_callback(scope.lightpath.Poll) MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -104,7 +104,7 @@ class chaninfo: InitGUI(''' from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) ''') #from PYME.Acquire.Hardware import frZStage @@ -178,14 +178,14 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) ''') InitGUI(''' from PYME.Acquire import lasersliders lsf = lasersliders.LaserSliders(toolPanel, scope.lasers) -time1.WantNotification.append(lsf.update) +time1.register_callback(lsf.update) #lsf.update() camPanels.append((lsf, 'Laser Powers')) ''') diff --git a/PYME/Acquire/Scripts/init_Neo.py b/PYME/Acquire/Scripts/init_Neo.py index 3b3f84d28..e4539166b 100644 --- a/PYME/Acquire/Scripts/init_Neo.py +++ b/PYME/Acquire/Scripts/init_Neo.py @@ -127,7 +127,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -148,7 +148,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) #from PYME.Acquire.Hardware import frZStage @@ -226,7 +226,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) # diff --git a/PYME/Acquire/Scripts/init_NeoSim.py b/PYME/Acquire/Scripts/init_NeoSim.py index 90fbbde66..7d891a564 100644 --- a/PYME/Acquire/Scripts/init_NeoSim.py +++ b/PYME/Acquire/Scripts/init_NeoSim.py @@ -96,7 +96,7 @@ class chaninfo: #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -109,7 +109,7 @@ class chaninfo: #InitGUI(""" #from PYME.Acquire.Hardware import LaserControlFrame #lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -#time1.WantNotification.append(lcf.refresh) +#time1.register_callback(lcf.refresh) #lcf.Show() #toolPanels.append((lcf, 'Laser Control')) #""") @@ -127,7 +127,7 @@ class chaninfo: #InitGUI(""" #from PYME.Acquire.Hardware import focusKeys #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -#time1.WantNotification.append(fk.refresh) +#time1.register_callback(fk.refresh) #""") #InitGUI(""" diff --git a/PYME/Acquire/Scripts/init_TIRF.py b/PYME/Acquire/Scripts/init_TIRF.py index 5af00fbab..0dd6c6c86 100644 --- a/PYME/Acquire/Scripts/init_TIRF.py +++ b/PYME/Acquire/Scripts/init_TIRF.py @@ -94,7 +94,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) InitGUI(""" @@ -102,7 +102,7 @@ class chaninfo: pt = positionTracker.PositionTracker(scope, time1) pv = positionTracker.TrackerPanel(MainFrame, pt) MainFrame.AddPage(page=pv, select=False, caption='Track') -time1.WantNotification.append(pv.draw) +time1.register_callback(pv.draw) """) #splitter @@ -189,7 +189,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) diff --git a/PYME/Acquire/Scripts/init_TIRF_Neo.py b/PYME/Acquire/Scripts/init_TIRF_Neo.py index 15bc09ce2..1f69aea03 100644 --- a/PYME/Acquire/Scripts/init_TIRF_Neo.py +++ b/PYME/Acquire/Scripts/init_TIRF_Neo.py @@ -104,7 +104,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) #InitGUI(""" @@ -112,7 +112,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -199,7 +199,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) diff --git a/PYME/Acquire/Scripts/init_TIRF_NeoO.py b/PYME/Acquire/Scripts/init_TIRF_NeoO.py index 867b7904a..0b146749d 100644 --- a/PYME/Acquire/Scripts/init_TIRF_NeoO.py +++ b/PYME/Acquire/Scripts/init_TIRF_NeoO.py @@ -104,7 +104,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) #InitGUI(""" @@ -112,7 +112,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -198,7 +198,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) diff --git a/PYME/Acquire/Scripts/init_TIRF_onecam.py b/PYME/Acquire/Scripts/init_TIRF_onecam.py index 0525b22cf..23d23e8a9 100644 --- a/PYME/Acquire/Scripts/init_TIRF_onecam.py +++ b/PYME/Acquire/Scripts/init_TIRF_onecam.py @@ -95,7 +95,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) InitGUI(""" @@ -103,7 +103,7 @@ class chaninfo: pt = positionTracker.PositionTracker(scope, time1) pv = positionTracker.TrackerPanel(MainFrame, pt) MainFrame.AddPage(page=pv, select=False, caption='Track') -time1.WantNotification.append(pv.draw) +time1.register_callback(pv.draw) """) #splitter @@ -190,7 +190,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) diff --git a/PYME/Acquire/Scripts/init_Ti.py b/PYME/Acquire/Scripts/init_Ti.py index 9230ce380..f1f1533de 100644 --- a/PYME/Acquire/Scripts/init_Ti.py +++ b/PYME/Acquire/Scripts/init_Ti.py @@ -108,7 +108,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -137,7 +137,7 @@ class chaninfo: TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) toolPanels.append((TiPanel, 'Nikon Ti')) -time1.WantNotification.append(TiPanel.SetSelections) +time1.register_callback(TiPanel.SetSelections) MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -146,7 +146,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) #from PYME.Acquire.Hardware import frZStage @@ -223,7 +223,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) # diff --git a/PYME/Acquire/Scripts/init_UOA_n.py b/PYME/Acquire/Scripts/init_UOA_n.py index 6ff265341..17d2b6998 100644 --- a/PYME/Acquire/Scripts/init_UOA_n.py +++ b/PYME/Acquire/Scripts/init_UOA_n.py @@ -139,7 +139,7 @@ def power_meter(scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") # @init_hardware('Lasers') @@ -167,11 +167,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders # lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - # MainFrame.time1.WantNotification.append(lcf.update) + # MainFrame.time1.register_callback(lcf.update) # MainFrame.camPanels.append((lcf, 'Lasers', False, False)) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Lasers', False, False)) # @init_hardware('Line scanner') diff --git a/PYME/Acquire/Scripts/init_UOA_n2.py b/PYME/Acquire/Scripts/init_UOA_n2.py index 72280dfbc..b5e5ca61e 100644 --- a/PYME/Acquire/Scripts/init_UOA_n2.py +++ b/PYME/Acquire/Scripts/init_UOA_n2.py @@ -135,7 +135,7 @@ def samp_db(MainFrame, scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") # @init_hardware('Lasers') @@ -163,11 +163,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders # lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - # MainFrame.time1.WantNotification.append(lcf.update) + # MainFrame.time1.register_callback(lcf.update) # MainFrame.camPanels.append((lcf, 'Lasers', False, False)) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Lasers', False, False)) # @init_hardware('Line scanner') diff --git a/PYME/Acquire/Scripts/init_Y1.py b/PYME/Acquire/Scripts/init_Y1.py index f3f4eb441..c2d485803 100644 --- a/PYME/Acquire/Scripts/init_Y1.py +++ b/PYME/Acquire/Scripts/init_Y1.py @@ -142,7 +142,7 @@ def GetComputerName(): pt = positionTracker.PositionTracker(scope, time1) pv = positionTracker.TrackerPanel(MainFrame, pt) MainFrame.AddPage(page=pv, select=False, caption='Track') -time1.WantNotification.append(pv.draw) +time1.register_callback(pv.draw) """, 'Position Tracker') #splitter @@ -162,9 +162,9 @@ def GetComputerName(): TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) toolPanels.append((TiPanel, 'Nikon Ti')) -#time1.WantNotification.append(TiPanel.SetSelections) -time1.WantNotification.append(scope.dichroic.Poll) -time1.WantNotification.append(scope.lightpath.Poll) +#time1.register_callback(TiPanel.SetSelections) +time1.register_callback(scope.dichroic.Poll) +time1.register_callback(scope.lightpath.Poll) MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -216,13 +216,13 @@ def GetComputerName(): InitGUI(""" from PYME.Acquire.ui import lasersliders lsf = lasersliders.LaserSliders(toolPanel, scope.state) -time1.WantNotification.append(lsf.update) +time1.register_callback(lsf.update) #lsf.update() camPanels.append((lsf, 'Laser Powers')) if 'lasers' in dir(scope): lcf = lasersliders.LaserToggles(toolPanel, scope.state) - time1.WantNotification.append(lcf.update) + time1.register_callback(lcf.update) camPanels.append((lcf, 'Laser Control')) """, 'Laser Sliders') diff --git a/PYME/Acquire/Scripts/init_Zyla.py b/PYME/Acquire/Scripts/init_Zyla.py index 49e333b99..7551e9927 100644 --- a/PYME/Acquire/Scripts/init_Zyla.py +++ b/PYME/Acquire/Scripts/init_Zyla.py @@ -127,7 +127,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -148,7 +148,7 @@ class chaninfo: #InitGUI(""" #from PYME.Acquire.Hardware import focusKeys #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -#time1.WantNotification.append(fk.refresh) +#time1.register_callback(fk.refresh) #""") #from PYME.Acquire.Hardware import frZStage @@ -226,7 +226,7 @@ class chaninfo: #if 'lasers'in dir(scope): # from PYME.Acquire.Hardware import LaserControlFrame # lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -# time1.WantNotification.append(lcf.refresh) +# time1.register_callback(lcf.refresh) # toolPanels.append((lcf, 'Laser Control')) #""") ## diff --git a/PYME/Acquire/Scripts/init_drift_tracking.py b/PYME/Acquire/Scripts/init_drift_tracking.py index a3139e307..2061db2fa 100644 --- a/PYME/Acquire/Scripts/init_drift_tracking.py +++ b/PYME/Acquire/Scripts/init_drift_tracking.py @@ -62,7 +62,7 @@ def drift_tracking(MainFrame, scope): scope.dt = driftTracking.Correlator(scope, scope.piFoc) dtp = driftTrackGUI.DriftTrackingControl(MainFrame, scope.dt) MainFrame.camPanels.append((dtp, 'Focus Lock')) - MainFrame.time1.WantNotification.append(dtp.refresh) + MainFrame.time1.register_callback(dtp.refresh) @init_gui('Focus Keys') def focus_keys(MainFrame, scope): diff --git a/PYME/Acquire/Scripts/init_emccd_basic.py b/PYME/Acquire/Scripts/init_emccd_basic.py index ddab2934a..a3cac84cf 100644 --- a/PYME/Acquire/Scripts/init_emccd_basic.py +++ b/PYME/Acquire/Scripts/init_emccd_basic.py @@ -128,7 +128,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -157,7 +157,7 @@ class chaninfo: # #TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) #toolPanels.append((TiPanel, 'Nikon Ti')) -#time1.WantNotification.append(TiPanel.SetSelections) +#time1.register_callback(TiPanel.SetSelections) # #MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) #MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -166,7 +166,7 @@ class chaninfo: #InitGUI(""" #from PYME.Acquire.Hardware import focusKeys #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -#time1.WantNotification.append(fk.refresh) +#time1.register_callback(fk.refresh) #""") #from PYME.Acquire.Hardware import frZStage @@ -243,7 +243,7 @@ class chaninfo: #if 'lasers'in dir(scope): # from PYME.Acquire.Hardware import LaserControlFrame # lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -# time1.WantNotification.append(lcf.refresh) +# time1.register_callback(lcf.refresh) # toolPanels.append((lcf, 'Laser Control')) #""") # diff --git a/PYME/Acquire/Scripts/init_htsms.py b/PYME/Acquire/Scripts/init_htsms.py index 35dfa5ee1..cad3817af 100644 --- a/PYME/Acquire/Scripts/init_htsms.py +++ b/PYME/Acquire/Scripts/init_htsms.py @@ -21,11 +21,10 @@ # ################## -from PYME.Acquire.ExecTools import joinBGInit, init_gui, init_hardware - from PYME import config +from PYME.Acquire.ExecTools import joinBGInit, init_gui, init_hardware #enable high-throughput style directory hashing -config.config['acquire-spool_subdirectories'] = True +# config.config['acquire-spool_subdirectories'] = True @init_hardware('XY Stage') # FIXME - may need module-level locks if we add 'x' and 'y' of the xy stage as different piezos def mz_stage(scope): @@ -63,7 +62,7 @@ def pz(scope): scope.register_piezo(scope.piFoc, 'z', needCamRestart=False) scope.focus_lock = RLPIDFocusLockClient() - + try: # check if we've got a focus lock PYMEAcquire instance up already requests.get('http://127.0.0.1:9798/LockEnabled') except requests.exceptions.ConnectionError: @@ -71,9 +70,10 @@ def pz(scope): fl_command += ' -i init_htsms_focus_lock.py -t "Focus Lock"' subprocess.Popen('%s %s' % (sys.executable, fl_command), creationflags=subprocess.CREATE_NEW_CONSOLE) - + scope._stage_leveler = stage_leveling.StageLeveler(scope, scope.piFoc, - focus_lock=scope.focus_lock) + focus_lock=scope.focus_lock, + pause_on_relocate=1.0) @init_hardware('HamamatsuORCA') @@ -94,10 +94,10 @@ def orca_cam(scope): 'Multiview.ChannelColor': [0, 1, 1, 0], 'Multiview.DefaultROISize': (size, size), 'Multiview.ROISizeOptions': [128, 240, 256, 304, 352, 384], - 'Multiview.ROI0Origin': (308 - half_size, 1024 - half_size), - 'Multiview.ROI1Origin': (872 - half_size, 1024 - half_size), - 'Multiview.ROI2Origin': (1272 - half_size, 1024 - half_size), - 'Multiview.ROI3Origin': (1812 - half_size, 1024 - half_size), + 'Multiview.ROI0Origin': (312 - half_size, 1024 - half_size), + 'Multiview.ROI1Origin': (876 - half_size, 1024 - half_size), + 'Multiview.ROI2Origin': (1268 - half_size, 1024 - half_size), + 'Multiview.ROI3Origin': (1744 - half_size, 1024 - half_size), } cam = MultiviewOrca(0, multiview_info) cam.Init() @@ -136,7 +136,6 @@ def lasers(scope): from PYME.Acquire.Hardware.AAOptoelectronics.MDS import AAOptoMDS from PYME.Acquire.Hardware.aotf import AOTFControlledLaser from PYME.config import config - from PYME.Acquire.Hardware.ioslave import ServoFiberShaker import json calib_file = config['aotf-calibration-file'] @@ -176,15 +175,29 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Laser Powers')) +@init_gui('Failsafe') +def failsafe(MainFrame, scope): + from PYME import config + from PYME.Acquire.Utils.failsafe import FailsafeServer + import yaml + + email_info = config.get('email-info-path') + with open(email_info, 'r') as f: + email_info = yaml.safe_load(f) + + address = config.get('failsafeserver-address', '127.0.0.1') + port = config.get('failsafeserver-port', 9119) + scope.failsafe = FailsafeServer(scope, email_info, port, address) + @init_gui('Multiview Selection') def multiview_selection(MainFrame, scope): from PYME.Acquire.ui import multiview_select ms = multiview_select.MultiviewSelect(MainFrame.toolPanel, scope) - MainFrame.time1.WantNotification.append(ms.update) + MainFrame.time1.register_callback(ms.update) MainFrame.camPanels.append((ms, 'Multiview Selection')) @init_gui('Focus Keys') @@ -194,7 +207,7 @@ def focus_keys(MainFrame, scope): fk = focusKeys.FocusKeys(MainFrame, scope.piFoc) panel = FocusLockPanel(MainFrame, scope.focus_lock, offset_piezo=scope.piFoc) MainFrame.camPanels.append((panel, 'Focus Lock')) - MainFrame.time1.WantNotification.append(panel.refresh) + MainFrame.time1.register_callback(panel.refresh) @init_gui('Action manager') def action_manager(MainFrame, scope): @@ -204,14 +217,15 @@ def action_manager(MainFrame, scope): ap = actionUI.ActionPanel(MainFrame, scope.actions, scope) MainFrame.AddPage(ap, caption='Queued Actions') - + ActionManagerServer(scope.actions, 9393, config.get('actionmanagerserver-address', '127.0.0.1')) @init_gui('Chained Analysis') def chained_analysis(main_frame, scope): - from PYME.Acquire.ui.rules import SMLMChainedAnalysisPanel - from PYME.cluster.rules import RecipeRuleFactory + from PYME.Acquire.htsms.rule_ui import SMLMChainedAnalysisPanel, get_rule_tile, RuleChain + from PYME.cluster.rules import RecipeRuleFactory, SpoolLocalLocalizationRuleFactory + from PYME.IO.MetaDataHandler import DictMDHandler import yaml import os @@ -219,30 +233,33 @@ def chained_analysis(main_frame, scope): defaults = {} rec_dir = 'C:\\Users\\Bergamot\\PYMEData\\recipes' - tilerec = os.path.join(rec_dir, 'tile_detect_filter_queue.yaml') + tilerec = os.path.join(rec_dir, '20210125_tile_detect_filter_queue_subset.yaml') with open(tilerec) as f: tilerec = f.read() - defaults['htsms-tile'] = [RecipeRuleFactory(recipe=tilerec)] + defaults['htsms-tile'] = RuleChain([get_rule_tile(RecipeRuleFactory)(recipe=tilerec)]) + + mdh = DictMDHandler({ + "Analysis.BGRange": [-32, 0], + "Analysis.DebounceRadius": 4, + "Analysis.DetectionFilterSize": 4, + "Analysis.DetectionThreshold": 1.0, + "Analysis.FiducialThreshold": 1.8, + "Analysis.FitModule": "AstigGaussGPUFitFR", + "Analysis.GPUPCTBackground": True, + "Analysis.PCTBackground": 0.25, + "Analysis.ROISize": 7.5, + "Analysis.StartAt": 32, + "Analysis.TrackFiducials": False, + "Analysis.subtractBackground": True, + }) + + defaults['htsms-flow'] = RuleChain([get_rule_tile(SpoolLocalLocalizationRuleFactory)(analysisMetadata=mdh)]) + defaults['htsms-staggered'] = RuleChain([get_rule_tile(SpoolLocalLocalizationRuleFactory)(analysisMetadata=mdh)]) SMLMChainedAnalysisPanel.plug(main_frame, scope, defaults) -@init_hardware('tweeter') -def tweeter(scope): - from PYME.Acquire.tweeter import LazyScopeTweeter - scope.tweeter = LazyScopeTweeter(scope.actions.actionQueue, safety=False) - # queue up our favorite condition - condition = { - 'queue_condition': 9999, - 'queue_above': 1, - 'trigger_counts': 1, - 'trigger_above': -1, - 'action_filter': 'spoolController.StartSpooling', - 'message': 'Just finished imaging >= 10,000 fields of view!' - } - scope.tweeter.add_tweet_condition(condition) - @init_gui('Tiling') -def action_manager(MainFrame, scope): +def tiling(MainFrame, scope): from PYME.Acquire.ui import tile_panel ap = tile_panel.CircularTilePanel(MainFrame, scope) @@ -258,5 +275,3 @@ def action_manager(MainFrame, scope): #time.sleep(.5) scope.initDone = True - - diff --git a/PYME/Acquire/Scripts/init_htsms_focus_lock.py b/PYME/Acquire/Scripts/init_htsms_focus_lock.py index 29d7a258b..427ca8e03 100644 --- a/PYME/Acquire/Scripts/init_htsms_focus_lock.py +++ b/PYME/Acquire/Scripts/init_htsms_focus_lock.py @@ -20,22 +20,18 @@ # along with this program. If not, see . # ################## -from PYME.Acquire.ExecTools import joinBGInit, init_gui, init_hardware - - import time +from PYME.Acquire.ExecTools import joinBGInit, init_gui, init_hardware + @init_hardware('Camera') def cam(scope): - from PYME.Acquire.Hardware.uc480 import uCam480 - uCam480.init() - cam = uCam480.uc480Camera(0, nbits=10) + from PYME.Acquire.Hardware.ueye import UEyeCamera + cam = UEyeCamera(0, 10) scope.register_camera(cam, 'Focus') - scope.cam.SetGainBoost(False) # shouldn't be needed, but make sure it is off - scope.cam.SetGain(1) # we really don't need any extra gain, this defaults to 10 on startup + # scope.cam.SetGainBoost(False) # shouldn't be needed, but make sure it is off + # scope.cam.SetGain(1) # we really don't need any extra gain, this defaults to 10 on startup scope.cam.SetROI(289, 827, 1080, 1008) - # Can't get frame rate higher than ~297 Hz for the current ROI, so default to just under that - scope.cam.SetIntegTime(0.0035) # [s] #PIFoc @init_hardware('PIFoc') @@ -56,7 +52,7 @@ def pifoc(scope): # # fg.SetData(np.arange(scope.frameWrangler.currentFrame.shape[1]), scope.frameWrangler.currentFrame.sum(0)) # -# MainFrame.time1.WantNotification.append(refr_profile) +# MainFrame.time1.register_callback(refr_profile) @init_gui('Focus Lock') def focus_lock(MainFrame, scope): @@ -72,14 +68,16 @@ def focus_lock(MainFrame, scope): scope.focus_lock = RLPIDFocusLockServer(scope, scope.piFoc, p=kp, i=ki, d=0, sample_time=0.0035, min_amp=0.5 * 10**5, - max_sigma=14.5) + max_sigma=14.5, + min_lateral_sigma=0) scope.focus_lock.register() panel = FocusLockPanel(MainFrame, scope.focus_lock) MainFrame.camPanels.append((panel, 'Focus Lock')) - MainFrame.time1.WantNotification.append(panel.refresh) + MainFrame.time1.register_callback(panel.refresh) # we don't benefit at all from multiple frames piling up in a polling interval, so try and match the camera cycle + # Can't get frame rate higher than ~297 Hz for the current ROI, so default to just under that + scope.state['Camera.IntegrationTime'] = 0.0035 scope.frameWrangler._polling_interval = 0.0035 - # # display dark-subtracted profile # fg = fastGraph.FastGraphPanel(MainFrame, -1, np.arange(10), np.arange(10)) # MainFrame.AddPage(page=fg, select=False, caption='Profile') @@ -90,7 +88,7 @@ def focus_lock(MainFrame, scope): # profile = profile - scope.focus_lock.subtraction_profile # fg.SetData(np.arange(scope.frameWrangler.currentFrame.shape[1]), profile) # - # MainFrame.time1.WantNotification.append(refresh_profile) + # MainFrame.time1.register_callback(refresh_profile) # # display setpoint / error over time n = 500 @@ -109,7 +107,7 @@ def refresh_position(*args, **kwargs): # time[-1] = scope.focus_lock._last_time position_plot.SetData(time, position) - MainFrame.time1.WantNotification.append(refresh_position) + MainFrame.time1.register_callback(refresh_position) # panel to log focus to file at set intervals focus_logger = FocusLogger(scope.focus_lock.GetPeakPosition) @@ -117,6 +115,16 @@ def refresh_position(*args, **kwargs): MainFrame.camPanels.append((focus_log_panel, 'Focus Logger')) +@init_gui('Interlock') +def interlock(MainFrame, scope): + from PYME import config + from PYME.Acquire.Utils.failsafe import FailsafeClient + + address = config.get('interlockserver-address', '127.0.0.1') + port = config.get('interlockserver-port', 9119) + scope.interlock = FailsafeClient(address, port) + + #must be here!!! joinBGInit() #wait for anyhting which was being done in a separate thread diff --git a/PYME/Acquire/Scripts/init_rev.py b/PYME/Acquire/Scripts/init_rev.py index 4ddbda76d..5f1a504a8 100644 --- a/PYME/Acquire/Scripts/init_rev.py +++ b/PYME/Acquire/Scripts/init_rev.py @@ -59,7 +59,7 @@ def GetComputerName(): scope.dt = driftTracking.correlator(scope, scope.piFoc) dtp = driftTrackGUI.DriftTrackingControl(MainFrame, scope.dt) camPanels.append((dtp, 'Focus Lock')) -time1.WantNotification.append(dtp.refresh) +time1.register_callback(dtp.refresh) """, 'Drift Tracking') diff --git a/PYME/Acquire/Scripts/init_sim100.py b/PYME/Acquire/Scripts/init_sim100.py index 17b861314..9b9754339 100755 --- a/PYME/Acquire/Scripts/init_sim100.py +++ b/PYME/Acquire/Scripts/init_sim100.py @@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lcf.update) + MainFrame.time1.register_callback(lcf.update) MainFrame.camPanels.append((lcf, 'Laser Control')) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Laser Powers')) @init_gui('Focus Keys') diff --git a/PYME/Acquire/Scripts/init_sim103.py b/PYME/Acquire/Scripts/init_sim103.py index 7bbe9e80b..db289e95e 100755 --- a/PYME/Acquire/Scripts/init_sim103.py +++ b/PYME/Acquire/Scripts/init_sim103.py @@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lcf.update) + MainFrame.time1.register_callback(lcf.update) MainFrame.camPanels.append((lcf, 'Laser Control')) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Laser Powers')) @init_gui('Focus Keys') diff --git a/PYME/Acquire/Scripts/init_sim2.py b/PYME/Acquire/Scripts/init_sim2.py new file mode 100644 index 000000000..c507d1f32 --- /dev/null +++ b/PYME/Acquire/Scripts/init_sim2.py @@ -0,0 +1,211 @@ +#!/usr/bin/python + +################## +# init.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + +#!/usr/bin/python +from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware +from PYME import config +import scipy +import time + +# Set a microscope name which describes this hardware configuration (e.g. a room number or similar) +# Used with the splitting ratio database and in other places where a microscope identifier is required. +scope.microscope_name = 'PYMESimulator' + +# set some defaults for PYMEAcquire +# uncomment the line below for high-thoughput style directory hashing +# config.config['acquire-spool_subdirectories'] = True + +@init_hardware('Fake Piezos') +def pz(scope): + from PYME.Acquire.Hardware.Simulator import fakePiezo + scope.fakePiezo = fakePiezo.FakePiezo(100) + scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + scope.fakeXPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeXPiezo, 'x') + + scope.fakeYPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeYPiezo, 'y') + +pz.join() #piezo must be there before we start camera + +@init_hardware('Fake Camera') +def cm(scope): + import numpy as np + from PYME.Acquire.Hardware.Simulator import fakeCam + from PYME.Acquire.Hardware import multiview + size = 256 + cam = fakeCam.FakeCamera(size, #70*np.arange(0.0, 4*256.0), + size, #70*np.arange(0.0, 256.0), + fakeCam.NoiseMaker(), + scope.fakePiezo, xpiezo = scope.fakeXPiezo, + ypiezo = scope.fakeYPiezo, + pixel_size_nm=70., + illumFcn = 'ROIIllumFunction' + ) + cam.SetEMGain(150) + + # mv_cam = multiview.MultiviewWrapper(cam, multiview_info = { + # 'Multiview.NumROIs': 4, + # 'Multiview.ChannelColor': [0, 1, 1, 0], + # 'Multiview.DefaultROISize': (size, size), + # 'Multiview.ROISizeOptions': [128, 240, 256], + # 'Multiview.ROI0Origin': (0, 0), + # 'Multiview.ROI1Origin': (size, 0), + # 'Multiview.ROI2Origin': (2*size, 0), + # 'Multiview.ROI3Origin': (3*size, 0), + # }, + # default_roi= { + # 'xi' : 0, + # 'yi' : 0, + # 'xf' : size*4, + # 'yf' : size + # }) + scope.register_camera(cam,'Fake Camera') + +#scope.EnableJoystick = 'foo' + +#InitBG('Should Fail', """ +#raise Exception, 'test error' +#time.sleep(1) +#""") +# +#InitBG('Should not be there', """ +#raise HWNotPresent, 'test error' +#time.sleep(1) +#""") + + +# @init_gui('Simulation UI') +# def sim_controls(MainFrame, scope): +# from PYME.Acquire.Hardware.Simulator import dSimControl +# dsc = dSimControl.dSimControl(MainFrame, scope) +# MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') +# +# scope.dsc = dsc + + +@init_gui('Simulation UI') +def sim_controls(MainFrame, scope): + from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx + #pre-polulate for dSTORM using tweaked values + #note, probabilities are [spontaneous/s, switching laser/Ws, readout laser/Ws] + transition_tensor = simcontrol.fluor.createSimpleTransitionMatrix(pPA=[1e9, 0, 0], + pOnDark=[0, 0, 0.1], + pDarkOn=[0.02,0.001, 0], + pOnBleach=[0, 0, 0.00]) + scope.simcontrol = simcontrol.SimController(scope, + transistion_tensor=transition_tensor, + spectral_signatures=[[1, 0.05], [0.05, 1]], + splitter_info=([0, 0, 500., 500.], [0, 1, 1, 0]), + excitation_crossections=(1, 200)) + #scope.simcontrol.change_num_channels(4) + #scope.simcontrol.set_psf_model(simcontrol.PSFSettings(zernike_modes={4:1.5})) + dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol, show_status=False) + MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') + + msc = simui_wx.MiniSimPanel(MainFrame, scope.simcontrol) + MainFrame.camPanels.append((msc, 'Simulation')) + + from PYME.simulation import pointsets + scope.simcontrol.point_gen = simcontrol.RandomDistribution(n_instances=25,region_size=70e3, + generator=simcontrol.Group(generators=[pointsets.WiglyFibreSource(), + #simcontrol.AssignChannel(channel=1, generator=pointsets.SHNucleusSource()) + ])) + scope.simcontrol.generate_fluorophores() + + scope.dsc = dsc + +@init_gui('Camera controls') +def cam_controls(MainFrame, scope): + from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame + scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope) + MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False)) + +cm.join() + + +@init_hardware('Lasers') +def lasers(scope): + from PYME.Acquire.Hardware import lasers + scope.l642 = lasers.FakeLaser('l642',scope.cam,1, initPower=1) + scope.l642.register(scope) + #scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10) + #scope.l405.register(scope) + + +@init_gui('Laser controls') +def laser_controls(MainFrame, scope): + from PYME.Acquire.ui import lasersliders + + #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) + #MainFrame.time1.register_callback(lcf.update) + #MainFrame.camPanels.append((lcf, 'Laser Control')) + + lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) + MainFrame.time1.register_callback(lsf.update) + MainFrame.camPanels.append((lsf, 'Laser Control')) + +@init_gui('Focus Keys') +def focus_keys(MainFrame, scope): + from PYME.Acquire.Hardware import focusKeys + fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0]) + +# @init_gui('Sample Metadata') +# def sample_metadata(main_frame, scope): +# from PYME.Acquire.sampleInformation import SimpleSampleInfoPanel +# sampanel = SimpleSampleInfoPanel(main_frame) +# main_frame.camPanels.append((sampanel, 'Sample Metadata')) +# # Prefill the data for our simulated structure +# sampanel.slide.SetValue('Sim01') +# sampanel.notes.SetValue('Chan0: WiglyFibre') + +@init_gui('Action manager') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import actionUI + + ap = actionUI.ActionPanel(MainFrame, scope.actions, scope) + MainFrame.AddPage(ap, caption='Queued Actions') + + +@init_gui('Tiling') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import tile_panel + + ap = tile_panel.TilePanel(MainFrame, scope) + MainFrame.aqPanels.append((ap, 'Tiling')) + +@init_gui('Chained Analysis') +def chained_analysis(main_frame, scope): + from PYME.Acquire.htsms.rule_ui import SMLMChainedAnalysisPanel, get_rule_tile, RuleChain + from PYME.cluster.rules import RecipeRuleFactory, SpoolLocalLocalizationRuleFactory + from PYME.IO.MetaDataHandler import DictMDHandler + + SMLMChainedAnalysisPanel.plug(main_frame, scope) + + +#must be here!!! +joinBGInit() #wait for anything which was being done in a separate thread + + +scope.initDone = True diff --git a/PYME/Acquire/Scripts/init_sim50.py b/PYME/Acquire/Scripts/init_sim50.py index b0c7c7d5a..fb704cac4 100755 --- a/PYME/Acquire/Scripts/init_sim50.py +++ b/PYME/Acquire/Scripts/init_sim50.py @@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lcf.update) + MainFrame.time1.register_callback(lcf.update) MainFrame.camPanels.append((lcf, 'Laser Control')) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Laser Powers')) @init_gui('Focus Keys') diff --git a/PYME/Acquire/Scripts/init_sim75.py b/PYME/Acquire/Scripts/init_sim75.py index aa2e5fc59..dcb683875 100755 --- a/PYME/Acquire/Scripts/init_sim75.py +++ b/PYME/Acquire/Scripts/init_sim75.py @@ -109,7 +109,7 @@ def fake_dmd(MainFrame, scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -128,11 +128,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lcf.update) + MainFrame.time1.register_callback(lcf.update) MainFrame.camPanels.append((lcf, 'Laser Control')) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Laser Powers')) @init_gui('Focus Keys') diff --git a/PYME/Acquire/Scripts/init_sim_drift_tracking.py b/PYME/Acquire/Scripts/init_sim_drift_tracking.py new file mode 100644 index 000000000..36c9c950f --- /dev/null +++ b/PYME/Acquire/Scripts/init_sim_drift_tracking.py @@ -0,0 +1,166 @@ +#!/usr/bin/python + +################## +# init_TIRF.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## +from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware + +from PYME.Acquire.Hardware import fakeShutters +import time +import os +import sys + +def GetComputerName(): + if sys.platform == 'win32': + return os.environ['COMPUTERNAME'] + else: + return os.uname()[1] + +#scope.cameras = {} +#scope.camControls = {} +from PYME.IO import MetaDataHandler + + +#PIFoc +@init_hardware('PIFoc') +def pifoc(scope): + import sys + if sys.version_info.major > 2: + from PYME.Acquire.Hardware.Piezos import offsetPiezoREST as offsetPiezo + else: + from PYME.Acquire.Hardware.Piezos import offsetPiezo + scope.piFoc = offsetPiezo.getClient() + scope.register_piezo(scope.piFoc, 'z') + +pifoc.join() + +@init_hardware('Fake Piezos') +def pz(scope): + from PYME.Acquire.Hardware.Simulator import fakePiezo + scope.fakePiezo = scope.piFoc# fakePiezo.FakePiezo(100) + #scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + scope.fakeXPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeXPiezo, 'x') + + scope.fakeYPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeYPiezo, 'y') + +pz.join() #piezo must be there before we start camera + + + +@init_hardware('Cameras') +def cam(scope): + from PYME.Acquire.Hardware.Simulator import fakeCam + cam = fakeCam.FakeCamera(256, #70*np.arange(0.0, 4*256.0), + 256, #70*np.arange(0.0, 256.0), + fakeCam.NoiseMaker(), + scope.fakePiezo, xpiezo = scope.fakeXPiezo, + ypiezo = scope.fakeYPiezo, + pixel_size_nm=70., + ) + cam.SetEMGain(150) + scope.register_camera(cam,'Fake Camera') + +@init_gui('Simulation UI') +def sim_controls(MainFrame, scope): + from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx + # simulate some fluorescent fiducials + #note, probabilities are [spontaneous/s, switching laser/Ws, readout laser/Ws] + transition_tensor = simcontrol.fluor.createSimpleTransitionMatrix(pPA=[1e9, 0, 0], + pOnDark=[0, 0,0], + pDarkOn=[0.02,0.001, 0], + pOnBleach=[0, 0, 0.00]) + scope.simcontrol = simcontrol.SimController(scope, + transistion_tensor=transition_tensor, + spectral_signatures=[[1, 0.05], [0.05, 1]], + splitter_info=([0, 0, 500., 500.], [0, 1, 1, 0]), + excitation_crossections=(1, 200)) + #scope.simcontrol.change_num_channels(4) + #scope.simcontrol.set_psf_model(simcontrol.PSFSettings(zernike_modes={4:1.5})) + dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol, show_status=False) + MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') + + msc = simui_wx.MiniSimPanel(MainFrame, scope.simcontrol) + MainFrame.camPanels.append((msc, 'Simulation')) + + from PYME.simulation import pointsets + scope.simcontrol.point_gen = simcontrol.Shift(dx=5000, dy=5000, generator=pointsets.RandomSource(numPoints=20)) + #scope.simcontrol.point_gen = simcontrol.RandomDistribution(n_instances=25,region_size=70e3, + # generator=simcontrol.Group(generators=[pointsets.WiglyFibreSource(), + #simcontrol.AssignChannel(channel=1, generator=pointsets.SHNucleusSource()) + # ])) + scope.simcontrol.generate_fluorophores() + + scope.dsc = dsc + +@init_gui('Camera controls') +def cam_controls(MainFrame, scope): + from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame + scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope) + MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False)) + +cam.join() + + +@init_hardware('Lasers') +def lasers(scope): + from PYME.Acquire.Hardware import lasers + scope.l642 = lasers.FakeLaser('l642',scope.cam,1, initPower=1) + scope.l642.register(scope) + #scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10) + #scope.l405.register(scope) + + +@init_gui('Laser controls') +def laser_controls(MainFrame, scope): + from PYME.Acquire.ui import lasersliders + + #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) + #MainFrame.time1.register_callback(lcf.update) + #MainFrame.camPanels.append((lcf, 'Laser Control')) + + lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) + MainFrame.time1.register_callback(lsf.update) + MainFrame.camPanels.append((lsf, 'Laser Control')) + +@init_gui('Drift tracking') +def drift_tracking(MainFrame, scope): + from PYME.Acquire.Hardware import driftTracking, driftTrackGUI + scope.dt = driftTracking.Correlator(scope, scope.piFoc) + dtp = driftTrackGUI.DriftTrackingControl(MainFrame, scope.dt) + MainFrame.camPanels.append((dtp, 'Focus Lock')) + MainFrame.time1.register_callback(dtp.refresh) + +@init_gui('Focus Keys') +def focus_keys(MainFrame, scope): + from PYME.Acquire.Hardware import focusKeys + fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0]) + + + +#must be here!!! +joinBGInit() #wait for anyhting which was being done in a separate thread + +#scope.SetCamera('A') + +time.sleep(.5) +scope.initDone = True diff --git a/PYME/Acquire/Scripts/init_sim_htsms.py b/PYME/Acquire/Scripts/init_sim_htsms.py new file mode 100644 index 000000000..7d81307ab --- /dev/null +++ b/PYME/Acquire/Scripts/init_sim_htsms.py @@ -0,0 +1,224 @@ +#!/usr/bin/python + +################## +# init.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + +#!/usr/bin/python +from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware +from PYME import config +import scipy +import time + +# Set a microscope name which describes this hardware configuration (e.g. a room number or similar) +# Used with the splitting ratio database and in other places where a microscope identifier is required. +scope.microscope_name = 'PYMESimulator' + +# set some defaults for PYMEAcquire +# uncomment the line below for high-thoughput style directory hashing +# config.config['acquire-spool_subdirectories'] = True + +@init_hardware('Fake Piezos') +def pz(scope): + from PYME.Acquire.Hardware.Simulator import fakePiezo + scope.fakePiezo = fakePiezo.FakePiezo(100) + scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + scope.fakeXPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeXPiezo, 'x') + + scope.fakeYPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeYPiezo, 'y') + +pz.join() #piezo must be there before we start camera + +@init_hardware('Fake Camera') +def cm(scope): + import numpy as np + from PYME.Acquire.Hardware.Simulator import fakeCam + from PYME.Acquire.Hardware import multiview + size = 256 + cam = fakeCam.FakeCamera(size, #70*np.arange(0.0, 4*256.0), + size, #70*np.arange(0.0, 256.0), + fakeCam.NoiseMaker(), + scope.fakePiezo, xpiezo = scope.fakeXPiezo, + ypiezo = scope.fakeYPiezo, + pixel_size_nm=100., + illumFcn = 'ROIIllumFunction' + ) + cam.SetEMGain(150) + + mv_cam = multiview.MultiviewWrapper(cam, multiview_info = { + 'Multiview.NumROIs': 4, + 'Multiview.ChannelColor': [0, 1, 1, 0], + 'Multiview.DefaultROISize': (size, size), + 'Multiview.ROISizeOptions': [128, 240, 256], + 'Multiview.ROI0Origin': (0, 0), + 'Multiview.ROI1Origin': (size, 0), + 'Multiview.ROI2Origin': (2*size, 0), + 'Multiview.ROI3Origin': (3*size, 0), + }, + default_roi= { + 'xi' : 0, + 'yi' : 0, + 'xf' : size*4, + 'yf' : size + }) + scope.register_camera(mv_cam,'Fake Camera') + mv_cam.register_state_handlers(scope.state) + +#scope.EnableJoystick = 'foo' + +#InitBG('Should Fail', """ +#raise Exception, 'test error' +#time.sleep(1) +#""") +# +#InitBG('Should not be there', """ +#raise HWNotPresent, 'test error' +#time.sleep(1) +#""") + + +# @init_gui('Simulation UI') +# def sim_controls(MainFrame, scope): +# from PYME.Acquire.Hardware.Simulator import dSimControl +# dsc = dSimControl.dSimControl(MainFrame, scope) +# MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') +# +# scope.dsc = dsc + + +@init_gui('Simulation UI') +def sim_controls(MainFrame, scope): + from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx + #pre-polulate for dSTORM using tweaked values + #note, probabilities are [spontaneous/s, switching laser/Ws, readout laser/Ws] + transition_tensor = simcontrol.fluor.createSimpleTransitionMatrix(pPA=[1e9, 0, 0], + pOnDark=[0, 0, 0.1], + pDarkOn=[0.02,0.001, 0], + pOnBleach=[0, 0, 0.01]) + scope.simcontrol = simcontrol.SimController(scope, + transistion_tensor=transition_tensor, + spectral_signatures=[[1, 0.05], [0.05, 1]], + splitter_info=([0, 0, 500., 500.], [0, 1, 1, 0])) + scope.simcontrol.change_num_channels(4) + scope.simcontrol.set_psf_model(simcontrol.PSFSettings(zernike_modes={4:1.5})) + dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol, show_status=False) + MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') + + msc = simui_wx.MiniSimPanel(MainFrame, scope.simcontrol) + MainFrame.camPanels.append((msc, 'Simulation')) + + from PYME.simulation import pointsets + scope.simcontrol.point_gen = simcontrol.RandomDistribution(n_instances=25,region_size=70e3, force_at_origin=True, + generator=simcontrol.Group(generators=[pointsets.WiglyFibreSource(), + simcontrol.AssignChannel(channel=1, generator=pointsets.SHNucleusSource()) + ])) + scope.simcontrol.generate_fluorophores() + + scope.dsc = dsc + +@init_gui('Camera controls') +def cam_controls(MainFrame, scope): + from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame + scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope) + MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False)) + + MainFrame.AddMenuItem('Camera', 'Set Multiview', + lambda e: scope.state.setItem('Multiview.ActiveViews', [0, 1, 2, 3])) + MainFrame.AddMenuItem('Camera', 'Clear Multiview', + lambda e: scope.state.setItem('Multiview.ActiveViews', [])) + + +cm.join() + +@init_gui('Multiview Selection') +def multiview_selection(MainFrame, scope): + from PYME.Acquire.ui import multiview_select + + ms = multiview_select.MultiviewSelect(MainFrame.toolPanel, scope) + MainFrame.time1.register_callback(ms.update) + MainFrame.camPanels.append((ms, 'Multiview Selection')) + +@init_hardware('Lasers') +def lasers(scope): + from PYME.Acquire.Hardware import lasers + scope.l642 = lasers.FakeLaser('l642',scope.cam,1, initPower=1) + scope.l642.register(scope) + #scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10) + #scope.l405.register(scope) + + +@init_gui('Laser controls') +def laser_controls(MainFrame, scope): + from PYME.Acquire.ui import lasersliders + + #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) + #MainFrame.time1.register_callback(lcf.update) + #MainFrame.camPanels.append((lcf, 'Laser Control')) + + lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) + MainFrame.time1.register_callback(lsf.update) + MainFrame.camPanels.append((lsf, 'Laser Control')) + +@init_gui('Focus Keys') +def focus_keys(MainFrame, scope): + from PYME.Acquire.Hardware import focusKeys + fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0]) + +@init_gui('Sample Metadata') +def sample_metadata(main_frame, scope): + from PYME.Acquire.sampleInformation import SimpleSampleInfoPanel + sampanel = SimpleSampleInfoPanel(main_frame) + main_frame.camPanels.append((sampanel, 'Sample Metadata')) + # Prefill the data for our simulated structure + sampanel.slide.SetValue('HTSMS_Sim01') + sampanel.notes.SetValue('Chan0: WiglyFibre, Chan1: SHNucleus') + +@init_gui('Action manager') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import actionUI + + ap = actionUI.ActionPanel(MainFrame, scope.actions, scope) + MainFrame.AddPage(ap, caption='Queued Actions') + + +@init_gui('Tiling') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import tile_panel + + ap = tile_panel.TilePanel(MainFrame, scope) + MainFrame.aqPanels.append((ap, 'Tiling')) + +@init_gui('Chained Analysis') +def chained_analysis(main_frame, scope): + from PYME.Acquire.htsms.rule_ui import SMLMChainedAnalysisPanel, get_rule_tile, RuleChain + from PYME.cluster.rules import RecipeRuleFactory, SpoolLocalLocalizationRuleFactory + from PYME.IO.MetaDataHandler import DictMDHandler + + SMLMChainedAnalysisPanel.plug(main_frame, scope) + + +#must be here!!! +joinBGInit() #wait for anything which was being done in a separate thread + + +scope.initDone = True diff --git a/PYME/Acquire/Scripts/init_sim_htsms_n.py b/PYME/Acquire/Scripts/init_sim_htsms_n.py new file mode 100644 index 000000000..664c4dc50 --- /dev/null +++ b/PYME/Acquire/Scripts/init_sim_htsms_n.py @@ -0,0 +1,233 @@ +#!/usr/bin/python + +################## +# init.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + +#!/usr/bin/python +from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware +from PYME import config +import scipy +import time + +# Set a microscope name which describes this hardware configuration (e.g. a room number or similar) +# Used with the splitting ratio database and in other places where a microscope identifier is required. +scope.microscope_name = 'PYMESimulator' + +# set some defaults for PYMEAcquire +# uncomment the line below for high-thoughput style directory hashing +# config.config['acquire-spool_subdirectories'] = True + +@init_hardware('Fake Piezos') +def pz(scope): + from PYME.Acquire.Hardware.Simulator import fakePiezo + scope.fakePiezo = fakePiezo.FakePiezo(100) + scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + scope.fakeXPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeXPiezo, 'x') + + scope.fakeYPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeYPiezo, 'y') + +pz.join() #piezo must be there before we start camera + +@init_hardware('Fake Camera') +def cm(scope): + import numpy as np + from PYME.Acquire.Hardware.Simulator import fakeCam + from PYME.Acquire.Hardware import multiview + size = 256 + cam = fakeCam.FakeCamera(size, #70*np.arange(0.0, 4*256.0), + size, #70*np.arange(0.0, 256.0), + fakeCam.NoiseMaker(), + scope.fakePiezo, xpiezo = scope.fakeXPiezo, + ypiezo = scope.fakeYPiezo, + pixel_size_nm=70., + illumFcn = 'ROIIllumFunction' + ) + cam.SetEMGain(150) + + mv_cam = multiview.MultiviewWrapper(cam, multiview_info = { + 'Multiview.NumROIs': 4, + 'Multiview.ChannelColor': [0, 1, 1, 0], + 'Multiview.DefaultROISize': (size, size), + 'Multiview.ROISizeOptions': [128, 240, 256], + 'Multiview.ROI0Origin': (0, 0), + 'Multiview.ROI1Origin': (size, 0), + 'Multiview.ROI2Origin': (2*size, 0), + 'Multiview.ROI3Origin': (3*size, 0), + }, + default_roi= { + 'xi' : 0, + 'yi' : 0, + 'xf' : size*4, + 'yf' : size + }) + scope.register_camera(mv_cam,'Fake Camera') + mv_cam.register_state_handlers(scope.state) + +#scope.EnableJoystick = 'foo' + +#InitBG('Should Fail', """ +#raise Exception, 'test error' +#time.sleep(1) +#""") +# +#InitBG('Should not be there', """ +#raise HWNotPresent, 'test error' +#time.sleep(1) +#""") + + +# @init_gui('Simulation UI') +# def sim_controls(MainFrame, scope): +# from PYME.Acquire.Hardware.Simulator import dSimControl +# dsc = dSimControl.dSimControl(MainFrame, scope) +# MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') +# +# scope.dsc = dsc + + +@init_gui('Simulation UI') +def sim_controls(MainFrame, scope): + from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx + #pre-polulate for dSTORM using tweaked values + #note, probabilities are [spontaneous/s, switching laser/Ws, readout laser/Ws] + transition_tensor = simcontrol.fluor.createSimpleTransitionMatrix(pPA=[1e9, 0, 0], + pOnDark=[0, 0, 0.1], + pDarkOn=[0.02,0.001, 0], + pOnBleach=[0, 0, 0.01]) + scope.simcontrol = simcontrol.SimController(scope, + transistion_tensor=transition_tensor, + spectral_signatures=[[1, 0.05], [0.05, 1]], + splitter_info=([0, 0, 500., 500.], [0, 1, 1, 0])) + scope.simcontrol.change_num_channels(4) + scope.simcontrol.set_psf_model(simcontrol.PSFSettings(zernike_modes={4:1.5})) + dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol, show_status=False) + MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') + + msc = simui_wx.MiniSimPanel(MainFrame, scope.simcontrol) + MainFrame.camPanels.append((msc, 'Simulation')) + + from PYME.simulation import pointsets + scope.simcontrol.point_gen = simcontrol.RandomDistribution(n_instances=25,region_size=70e3, force_at_origin=True, + generator=simcontrol.Group(generators=[pointsets.WiglyFibreSource(), + simcontrol.AssignChannel(channel=1, generator=pointsets.SHNucleusSource(point_spacing=5e2)) + ])) + scope.simcontrol.generate_fluorophores() + + scope.dsc = dsc + +@init_gui('Camera controls') +def cam_controls(MainFrame, scope): + from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame + scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope) + MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False)) + + MainFrame.AddMenuItem('Camera', 'Set Multiview', + lambda e: scope.state.setItem('Multiview.ActiveViews', [0, 1, 2, 3])) + MainFrame.AddMenuItem('Camera', 'Clear Multiview', + lambda e: scope.state.setItem('Multiview.ActiveViews', [])) + + +cm.join() + +@init_gui('Multiview Selection') +def multiview_selection(MainFrame, scope): + from PYME.Acquire.ui import multiview_select + + ms = multiview_select.MultiviewSelect(MainFrame.toolPanel, scope) + MainFrame.time1.register_callback(ms.update) + MainFrame.camPanels.append((ms, 'Multiview Selection')) + +@init_hardware('Lasers') +def lasers(scope): + from PYME.Acquire.Hardware import lasers + scope.l642 = lasers.FakeLaser('l642',scope.cam,1, initPower=1) + scope.l642.register(scope) + scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10) + scope.l405.register(scope) + + +@init_gui('Laser controls') +def laser_controls(MainFrame, scope): + from PYME.Acquire.ui import lasersliders + + #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) + #MainFrame.time1.register_callback(lcf.update) + #MainFrame.camPanels.append((lcf, 'Laser Control')) + + lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) + MainFrame.time1.register_callback(lsf.update) + MainFrame.camPanels.append((lsf, 'Laser Control')) + +@init_gui('Focus Keys') +def focus_keys(MainFrame, scope): + from PYME.Acquire.Hardware import focusKeys + fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0]) + +@init_gui('Sample Metadata') +def sample_metadata(main_frame, scope): + from PYME.Acquire.sampleInformation import SimpleSampleInfoPanel + sampanel = SimpleSampleInfoPanel(main_frame) + main_frame.camPanels.append((sampanel, 'Sample Metadata')) + # Prefill the data for our simulated structure + sampanel.slide.SetValue('HTSMS_Sim01') + sampanel.notes.SetValue('Chan0: WiglyFibre, Chan1: SHNucleus') + +@init_gui('Action manager') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import actionUI + + ap = actionUI.ActionPanel(MainFrame, scope.actions, scope) + MainFrame.AddPage(ap, caption='Queued Actions') + + +@init_hardware('Tiling') +def tiling(scope): + from PYME.Acquire.Utils import tiler + scope.spoolController.register_acquisition_type('Tiling', tiler.TileAcquisition) + + from PYME.Acquire import xyztc + scope.spoolController.register_acquisition_type('ZTiling', xyztc.TiledZStackAcquisition) + +@init_gui('Tiling') +def tiling(MainFrame, scope): + from PYME.Acquire.ui import tilesettingsui + + ts = tilesettingsui.TileSettingsUI(MainFrame, scope) + MainFrame.register_acquisition_ui('Tiling', (ts, 'Tiling')) + + ts2 = tilesettingsui.ZTileSettingsUI(MainFrame, scope) + MainFrame.register_acquisition_ui('ZTiling', (ts2, 'Tiled Z Stack')) + +@init_gui('Automated analysis') +def chained_analysis(main_frame, scope): + from PYME.Acquire.htsms import rule_ui_v2 + + rule_ui_v2.plug(main_frame, scope) + + +#must be here!!! +joinBGInit() #wait for anything which was being done in a separate thread + + +scope.initDone = True diff --git a/PYME/Acquire/Scripts/init_sim_main.py b/PYME/Acquire/Scripts/init_sim_main.py new file mode 100644 index 000000000..5d9670d7d --- /dev/null +++ b/PYME/Acquire/Scripts/init_sim_main.py @@ -0,0 +1,229 @@ +#!/usr/bin/python + +################## +# init.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## + +#!/usr/bin/python +from PYME.Acquire.ExecTools import joinBGInit, HWNotPresent, init_gui, init_hardware +from PYME import config +import scipy +import time + +# Set a microscope name which describes this hardware configuration (e.g. a room number or similar) +# Used with the splitting ratio database and in other places where a microscope identifier is required. +scope.microscope_name = 'PYMESimulator' + +# set some defaults for PYMEAcquire +# uncomment the line below for high-thoughput style directory hashing +# config.config['acquire-spool_subdirectories'] = True + +@init_hardware('Fake Piezos') +def pz(scope): + from PYME.Acquire.Hardware.Simulator import fakePiezo + from PYME.Acquire.Hardware.Piezos import offsetPiezoREST + + scope._fakePiezo = fakePiezo.FakePiezo(100) + #scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + scope.fakePiezo = offsetPiezoREST.server_class()(scope._fakePiezo) + scope.register_piezo(scope.fakePiezo, 'z', needCamRestart=True) + + + scope.fakeXPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeXPiezo, 'x') + + scope.fakeYPiezo = fakePiezo.FakePiezo(10000) + scope.register_piezo(scope.fakeYPiezo, 'y') + +pz.join() #piezo must be there before we start camera + + +@init_hardware('Fake Camera') +def cm(scope): + import numpy as np + from PYME.Acquire.Hardware.Simulator import fakeCam + cam = fakeCam.FakeCamera(256, #70*np.arange(0.0, 4*256.0), + 256, #70*np.arange(0.0, 256.0), + fakeCam.NoiseMaker(), + scope.fakePiezo, xpiezo = scope.fakeXPiezo, + ypiezo = scope.fakeYPiezo, + pixel_size_nm=70., + ) + cam.SetEMGain(150) + scope.register_camera(cam,'Fake Camera') + +#scope.EnableJoystick = 'foo' + +#InitBG('Should Fail', """ +#raise Exception, 'test error' +#time.sleep(1) +#""") +# +#InitBG('Should not be there', """ +#raise HWNotPresent, 'test error' +#time.sleep(1) +#""") + + +# @init_gui('Simulation UI') +# def sim_controls(MainFrame, scope): +# from PYME.Acquire.Hardware.Simulator import dSimControl +# dsc = dSimControl.dSimControl(MainFrame, scope) +# MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') +# +# scope.dsc = dsc + + +@init_gui('Simulation UI') +def sim_controls(MainFrame, scope): + from PYME.Acquire.Hardware.Simulator import simcontrol, simui_wx + scope.simcontrol = simcontrol.SimController(scope) + dsc = simui_wx.dSimControl(MainFrame, scope.simcontrol) + MainFrame.AddPage(page=dsc, select=False, caption='Simulation Settings') + + scope.dsc = dsc + +@init_gui('Camera controls') +def cam_controls(MainFrame, scope): + from PYME.Acquire.Hardware.AndorIXon import AndorControlFrame + scope.camControls['Fake Camera'] = AndorControlFrame.AndorPanel(MainFrame, scope.cam, scope) + MainFrame.camPanels.append((scope.camControls['Fake Camera'], 'EMCCD Properties', False)) + +@init_gui('Sample database') +def samp_db(MainFrame, scope): + from PYME.Acquire import sampleInformation + from PYME.IO import MetaDataHandler + + MetaDataHandler.provideStartMetadata.append(lambda mdh: sampleInformation.getSampleDataFailsafe(MainFrame, mdh)) + + sampPan = sampleInformation.slidePanel(MainFrame) + MainFrame.camPanels.append((sampPan, 'Current Slide')) + +# @init_gui('Analysis settings') +# def anal_settings(MainFrame, scope): +# from PYME.Acquire.ui import AnalysisSettingsUI +# AnalysisSettingsUI.Plug(scope, MainFrame) + +@init_gui('Fake DMD') +def fake_dmd(MainFrame, scope): + from PYMEnf.Hardware import FakeDMD + from PYME.Acquire.Hardware import DMDGui + scope.LC = FakeDMD.FakeDMD(scope) + + LCGui = DMDGui.DMDPanel(MainFrame,scope.LC, scope) + MainFrame.camPanels.append((LCGui, 'DMD Control', False)) + + +#InitGUI(""" +#from PYME.Acquire.Hardware import ccdAdjPanel +##import wx +##f = wx.Frame(None) +#snrPan = ccdAdjPanel.sizedCCDPanel(notebook1, scope, acf) +#notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') +##camPanels.append((snrPan, 'SNR etc ...')) +##f.Show() +##time1.register_callback(snrPan.ccdPan.draw) +#""") + +cm.join() + +@init_hardware('Lasers') +def lasers(scope): + from PYME.Acquire.Hardware import lasers + scope.l488 = lasers.FakeLaser('l488',scope.cam,1, initPower=10) + scope.l488.register(scope) + scope.l405 = lasers.FakeLaser('l405',scope.cam,0, initPower=10) + scope.l405.register(scope) + + +@init_gui('Laser controls') +def laser_controls(MainFrame, scope): + from PYME.Acquire.ui import lasersliders + + #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) + #MainFrame.time1.register_callback(lcf.update) + #MainFrame.camPanels.append((lcf, 'Laser Control')) + + lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) + MainFrame.time1.register_callback(lsf.update) + MainFrame.camPanels.append((lsf, 'Laser Control')) + +@init_gui('Focus Keys') +def focus_keys(MainFrame, scope): + from PYME.Acquire.Hardware import focusKeys + fk = focusKeys.FocusKeys(MainFrame, scope.piezos[0]) + + +#InitGUI(""" +#from PYME.Acquire.Hardware import splitter +#splt = splitter.Splitter(MainFrame, None, scope, scope.cam) +#""") + +@init_gui('Action manager') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import actionUI + + ap = actionUI.ActionPanel(MainFrame, scope.actions, scope) + MainFrame.AddPage(ap, caption='Queued Actions') + + +@init_gui('Tiling') +def action_manager(MainFrame, scope): + from PYME.Acquire.ui import tile_panel + + ap = tile_panel.TilePanel(MainFrame, scope) + MainFrame.aqPanels.append((ap, 'Tiling')) + +@init_gui('Drift tracking') +def drift_tracking(MainFrame, scope): + import subprocess + import sys + import time + from PYME.Acquire import PYMEAcquire + from PYME.Acquire import acquire_client + import wx + + + #def _drift_init(): + #scope.p_drift = subprocess.Popen('%s "%s" -i init_drift_tracking.py -t "Drift Tracking" -m "compact"' % (sys.executable, PYMEAcquire.__file__), shell=True) + scope.p_drift = subprocess.Popen('%s "%s" -i init_sim_drift_tracking.py -s -p 8155 -t "Drift Tracking"' % (sys.executable, PYMEAcquire.__file__), shell=True)#, creationflags=subprocess.CREATE_NEW_CONSOLE) + + #time.sleep(15) + #_drift_init() + + # create a client for the drift tracking server process + # this should be safe to do here, as the connection to the server is only made on demand + scope.remote_acquire_instance = acquire_client.AcquireClient('localhost', 8155) + + +#must be here!!! +joinBGInit() #wait for anyhting which was being done in a separate thread + +#import numpy +#psf = numpy.load(r'd:\psf647.npy') +#psf = numpy.maximum(psf, 0.) +#from PYME.Analysis import MetaData +#fakeCam.rend_im.setModel(psf, MetaData.TIRFDefault) + +#time.sleep(.5) +scope.initDone = True + + diff --git a/PYME/Acquire/Scripts/init_sim_min.py b/PYME/Acquire/Scripts/init_sim_min.py index d84f24d94..11b915eef 100755 --- a/PYME/Acquire/Scripts/init_sim_min.py +++ b/PYME/Acquire/Scripts/init_sim_min.py @@ -121,7 +121,7 @@ def cm(scope): #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -148,11 +148,11 @@ def simcontrol(scope): # from PYME.Acquire.ui import lasersliders # # #lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) -# #MainFrame.time1.WantNotification.append(lcf.update) +# #MainFrame.time1.register_callback(lcf.update) # #MainFrame.camPanels.append((lcf, 'Laser Control')) # # lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) -# MainFrame.time1.WantNotification.append(lsf.update) +# MainFrame.time1.register_callback(lsf.update) # MainFrame.camPanels.append((lsf, 'Laser Control')) # @init_gui('Focus Keys') diff --git a/PYME/Acquire/Scripts/init_sim_rem.py b/PYME/Acquire/Scripts/init_sim_rem.py index 0ed0293c2..3b4f9f231 100644 --- a/PYME/Acquire/Scripts/init_sim_rem.py +++ b/PYME/Acquire/Scripts/init_sim_rem.py @@ -111,7 +111,7 @@ class chaninfo: #notebook1.AddPage(page=snrPan, select=False, caption='Image SNR') ##camPanels.append((snrPan, 'SNR etc ...')) ##f.Show() -##time1.WantNotification.append(snrPan.ccdPan.draw) +##time1.register_callback(snrPan.ccdPan.draw) #""") cm.join() @@ -124,7 +124,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -time1.WantNotification.append(lcf.refresh) +time1.register_callback(lcf.refresh) #lcf.Show() camPanels.append((lcf, 'Laser Control')) """) @@ -142,7 +142,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) InitGUI(""" diff --git a/PYME/Acquire/Scripts/init_smi1.py b/PYME/Acquire/Scripts/init_smi1.py index 4a0e72d18..b09e0e045 100755 --- a/PYME/Acquire/Scripts/init_smi1.py +++ b/PYME/Acquire/Scripts/init_smi1.py @@ -60,7 +60,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[-1]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) @@ -82,7 +82,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) @@ -92,7 +92,7 @@ class chaninfo: import wx scope.step = SMI1.CStepOp() -time1.WantNotification.append(scope.step.ContIO) +time1.register_callback(scope.step.ContIO) mb = wx.MessageDialog(sh.GetParent(), 'Continue with Calibration of stage?\\nPLEASE CHECK that the slide holder has been removed\\n(and then press OK)', 'Stage Callibration', wx.YES_NO|wx.NO_DEFAULT) ret = mb.ShowModal() diff --git a/PYME/Acquire/Scripts/init_spectro.py b/PYME/Acquire/Scripts/init_spectro.py index e2684a9dc..ee9e591bc 100644 --- a/PYME/Acquire/Scripts/init_spectro.py +++ b/PYME/Acquire/Scripts/init_spectro.py @@ -65,7 +65,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -time1.WantNotification.append(lcf.refresh) +time1.register_callback(lcf.refresh) lcf.Show() toolPanels.append((lcf, 'Laser Control')) """) diff --git a/PYME/Acquire/Scripts/init_spim.py b/PYME/Acquire/Scripts/init_spim.py index 3b4ee1ace..6be5f4285 100644 --- a/PYME/Acquire/Scripts/init_spim.py +++ b/PYME/Acquire/Scripts/init_spim.py @@ -101,11 +101,11 @@ def laser_controls(MainFrame, scope): from PYME.Acquire.ui import lasersliders # lcf = lasersliders.LaserToggles(MainFrame.toolPanel, scope.state) - # MainFrame.time1.WantNotification.append(lcf.update) + # MainFrame.time1.register_callback(lcf.update) # MainFrame.camPanels.append((lcf, 'Lasers', False, False)) lsf = lasersliders.LaserSliders(MainFrame.toolPanel, scope.state) - MainFrame.time1.WantNotification.append(lsf.update) + MainFrame.time1.register_callback(lsf.update) MainFrame.camPanels.append((lsf, 'Lasers', False, False)) #must be here!!! diff --git a/PYME/Acquire/Scripts/init_twophoton.py b/PYME/Acquire/Scripts/init_twophoton.py index ef8ef1869..dd1f36347 100755 --- a/PYME/Acquire/Scripts/init_twophoton.py +++ b/PYME/Acquire/Scripts/init_twophoton.py @@ -62,7 +62,7 @@ class chaninfo: InitGUI(""" from PYME.Acquire.Hardware import focusKeys fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0]) -time1.WantNotification.append(fk.refresh) +time1.register_callback(fk.refresh) """) @@ -72,7 +72,7 @@ class chaninfo: import wx scope.step = SMI1.CStepOp() -time1.WantNotification.append(scope.step.ContIO) +time1.register_callback(scope.step.ContIO) mb = wx.MessageDialog(sh.GetParent(), 'Continue with Calibration of stage?\\nPLEASE CHECK that the slide holder has been removed\\n(and then press OK)', 'Stage Callibration', wx.YES_NO|wx.NO_DEFAULT) ret = mb.ShowModal() @@ -97,7 +97,7 @@ class chaninfo: if 'lasers'in dir(scope): from PYME.Acquire.Hardware import LaserControlFrame lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) - time1.WantNotification.append(lcf.refresh) + time1.register_callback(lcf.refresh) toolPanels.append((lcf, 'Laser Control')) """) diff --git a/PYME/Acquire/Scripts/init_uc480.py b/PYME/Acquire/Scripts/init_uc480.py index aba0ffb42..18b8419c4 100644 --- a/PYME/Acquire/Scripts/init_uc480.py +++ b/PYME/Acquire/Scripts/init_uc480.py @@ -113,7 +113,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #""") #splitter @@ -142,7 +142,7 @@ class chaninfo: # #TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) #toolPanels.append((TiPanel, 'Nikon Ti')) -#time1.WantNotification.append(TiPanel.SetSelections) +#time1.register_callback(TiPanel.SetSelections) # #MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) #MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -151,7 +151,7 @@ class chaninfo: #InitGUI(""" #from PYME.Acquire.Hardware import focusKeys #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -#time1.WantNotification.append(fk.refresh) +#time1.register_callback(fk.refresh) #""") #from PYME.Acquire.Hardware import frZStage @@ -228,7 +228,7 @@ class chaninfo: #if 'lasers'in dir(scope): # from PYME.Acquire.Hardware import LaserControlFrame # lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -# time1.WantNotification.append(lcf.refresh) +# time1.register_callback(lcf.refresh) # toolPanels.append((lcf, 'Laser Control')) #""") # diff --git a/PYME/Acquire/Scripts/init_ueye.py b/PYME/Acquire/Scripts/init_ueye.py index 40874dfc0..4a57b12ca 100644 --- a/PYME/Acquire/Scripts/init_ueye.py +++ b/PYME/Acquire/Scripts/init_ueye.py @@ -112,7 +112,7 @@ class chaninfo: #pt = positionTracker.PositionTracker(scope, time1) #pv = positionTracker.TrackerPanel(MainFrame, pt) #MainFrame.AddPage(page=pv, select=False, caption='Track') -#time1.WantNotification.append(pv.draw) +#time1.register_callback(pv.draw) #''') #splitter @@ -141,7 +141,7 @@ class chaninfo: # #TiPanel = NikonTiGUI.TiPanel(MainFrame, scope.dichroic, scope.lightpath) #toolPanels.append((TiPanel, 'Nikon Ti')) -#time1.WantNotification.append(TiPanel.SetSelections) +#time1.register_callback(TiPanel.SetSelections) # #MetaDataHandler.provideStartMetadata.append(scope.dichroic.ProvideMetadata) #MetaDataHandler.provideStartMetadata.append(scope.lightpath.ProvideMetadata) @@ -150,7 +150,7 @@ class chaninfo: #InitGUI(''' #from PYME.Acquire.Hardware import focusKeys #fk = focusKeys.FocusKeys(MainFrame, menuBar1, scope.piezos[0], scope=scope) -#time1.WantNotification.append(fk.refresh) +#time1.register_callback(fk.refresh) #''') #from PYME.Acquire.Hardware import frZStage @@ -227,7 +227,7 @@ class chaninfo: #if 'lasers'in dir(scope): # from PYME.Acquire.Hardware import LaserControlFrame # lcf = LaserControlFrame.LaserControlLight(MainFrame,scope.lasers) -# time1.WantNotification.append(lcf.refresh) +# time1.register_callback(lcf.refresh) # toolPanels.append((lcf, 'Laser Control')) #''') # diff --git a/PYME/Acquire/SpoolController.py b/PYME/Acquire/SpoolController.py index ad053e08c..eea72dfff 100644 --- a/PYME/Acquire/SpoolController.py +++ b/PYME/Acquire/SpoolController.py @@ -7,11 +7,10 @@ #import datetime -#from PYME.Acquire import HDFSpooler, QueueSpooler -from PYME.Acquire import HTTPSpooler +from PYME.IO import acquisition_backends # TODO: change to use a metadata handler / provideStartMetadata hook -# MetaDataHandler.provideStartMetadata from the init file when -# loading the sampleinfo interface, see Acquire/Scripts/init.py +# MetaDataHandler.provideStartMetadata from the init file when +# loading the sampleinfo interface, see Acquire/Scripts/init.py try: from PYME.Acquire import sampleInformation sampInf = True @@ -21,12 +20,16 @@ #import win32api from PYME.IO.FileUtils import nameUtils from PYME.IO.FileUtils.nameUtils import numToAlpha, getRelFilename, genHDFDataFilepath -from PYME.IO import unifiedIO +from PYME.IO import unifiedIO, MetaDataHandler + +from PYME.Acquire.protocol_acquisition import ProtocolAcquisition +from PYME.Acquire.xyztc import XYZTCAcquisition, ZStackAcquisition #import PYME.Acquire.Protocols import PYME.Acquire.protocol as prot from PYME.Acquire.ui import preflight +from PYME.Acquire import stackSettings from PYME import config from PYME.misc import hybrid_ns @@ -41,7 +44,7 @@ import queue except ImportError: # py2, remove this when we can - import Queue as queue + import Queue as queue # type: ignore from PYME.contrib import dispatch @@ -50,6 +53,93 @@ from PYME.util import webframework +class ProtocolAcquisitionSettings(object): + ''' + Manages settings which are specific to protocol-based acquisitions + ''' + def __init__(self) -> None: + self.protocol = prot.NullProtocol + self.protocolZ = prot.NullZProtocol + self.z_stepped = False # z-step during acquisition + self.z_dwell = 100 # time to spend at each z level (if z_stepped == True) + + def set_protocol(self, protocolName=None, reloadProtocol=True): + """ + Set the current protocol. + + Parameters + ---------- + protocolName: str + path to protocol file including extension + reloadProtocol : bool + currently ignored; protocol module is reinitialized regardless. + + See also: PYME.Acquire.Protocols. + """ + + if (protocolName is None) or (protocolName == ''): + self.protocol = prot.NullProtocol + self.protocolZ = prot.NullZProtocol + else: + #pmod = __import__('PYME.Acquire.Protocols.' + protocolName.split('.')[0],fromlist=['PYME', 'Acquire','Protocols']) + + #if reloadProtocol: + # reload(pmod) #force module to be reloaded so that changes in the protocol will be recognised + pmod = prot.get_protocol(protocol_name=protocolName, reloadProtocol=reloadProtocol) + + self.protocol = pmod.PROTOCOL + self.protocol.filename = protocolName + + self.protocolZ = pmod.PROTOCOL_STACK + self.protocolZ.filename = protocolName + self.z_dwell = self.protocolZ.dwellTime + + def get_protocol_for_acquisition(self, settings={}): + stack = settings.get('z_stepped', self.z_stepped) + stack_settings = settings.get('stack_settings', None) + + # try stack settings for z_dwell, then aq settings. + # precedence is settings > stack_settings > self.z_dwell + # The reasoning for allowing the dwell time to be set in either the spooling or stack settings is to allow + # API users to choose which is most coherent for their use case (it would seem logical to put dwell time with + # the other stack settings, but this becomes problematic when sharing stack settings across modalities - e.g. + # PALM/STORM and widefield stacks which are likely to share most of the stack settings but have greatly different + # z dwell times). PYMEAcquire specifies it in the spooling/series settings by default to allow shared usage + # between modalities. + if stack_settings: + if isinstance(stack_settings, dict): + z_dwell = stack_settings.get('DwellFrames', self.z_dwell) + else: + # have a StackSettings object + # TODO - fix this to be a bit more sane and not use private attributes etc ... + z_dwell = stack_settings._dwell_frames + # z_dwell defaults to -1 (with a meaning of ignore) in StackSettings objects if not value is not + # explicitly provided. In this case, use our internal value instead. The reason for the 'ignore' + # special value is to allow the same StackSettings object to be used for widefield stacks and + # localization series (where sharing everything except dwell time makes sense). + if z_dwell < 1: + z_dwell = self.z_dwell + else: + z_dwell = self.z_dwell + + z_dwell = settings.get('z_dwell', z_dwell) + protocol_name = settings.get('protocol_name', None) + + if protocol_name is None: + protocol, protocol_z = self.protocol, self.protocolZ + else: + pmod = prot.get_protocol(protocol_name) + protocol, protocol_z = pmod.PROTOCOL, pmod.PROTOCOL_STACK + + if stack: + protocol = protocol_z + protocol.dwellTime = z_dwell + #print(protocol) + else: + protocol = protocol + + return protocol + class SpoolController(object): def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(month)d_series'): """Initialise the spooling controller. @@ -74,10 +164,6 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont #else default to file self.spoolType = 'File' - #dtn = datetime.datetime.now() - - #dateDict = {'username' : win32api.GetUserName(), 'day' : dtn.day, 'month' : dtn.month, 'year':dtn.year} - self._base_dir = nameUtils.get_local_data_directory() self._dirname = os.sep.join([self._base_dir, ] + nameUtils.get_spool_subdir()) self._cluster_dirname = self.get_cluster_dirname(self._dirname) @@ -87,12 +173,23 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont self.seriesCounter = 0 self._series_name = None - self.protocol = prot.NullProtocol - self.protocolZ = prot.NullZProtocol + self.acquisition_types = { + 'ZStackAcquisition': ZStackAcquisition, + 'ProtocolAcquisition': ProtocolAcquisition, + } + + + self.acquisition_type='ProtocolAcquisition' + self.protocol_settings = ProtocolAcquisitionSettings() self.onSpoolProgress = dispatch.Signal() self.onSpoolStart = dispatch.Signal() - self.onSpoolStop = dispatch.Signal() + self.on_stop = dispatch.Signal() + + self.analysis_mode = 'interactive' # 'interactive' or 'rule-based' + self.analysis_rule_name = 'default' + self.analysis_launch_mode = 'triggered' # 'triggered' or 'series-end + self._analysis_launchers = queue.Queue(3) @@ -100,10 +197,9 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont #settings which were managed by GUI self.hdf_compression_level = 2 # zlib compression level that pytables should use (spool to file and queue) - self.z_stepped = False # z-step during acquisition - self.z_dwell = 100 # time to spend at each z level (if z_stepped == True) + self.cluster_h5 = False # spool to h5 on cluster (cluster of one) - self.pzf_compression_settings=HTTPSpooler.defaultCompSettings # only for cluster spooling + self.pzf_compression_settings=acquisition_backends.ClusterBackend.default_compression_settings # only for cluster spooling #check to see if we have a cluster self._N_data_servers = len(hybrid_ns.getNS('_pyme-http').get_advertised_services()) @@ -116,21 +212,14 @@ def __init__(self, scope, defDir=genHDFDataFilepath(), defSeries='%(day)d_%(mont @property def available_spool_methods(self): - if int(sys.version[0]) < 3: - return ['File', 'Queue', 'Cluster'] - else: + if False:#self.acquisition_type == 'ProtocolAcquision': return ['File', 'Cluster'] + else: + return ['File', 'Cluster', 'Memory', 'Tiff folder'] def get_info(self): - info = {'settings' : {'method' : self.spoolType, - 'hdf_compression_level': self.hdf_compression_level, - 'z_stepped' : self.z_stepped, - 'z_dwell' : self.z_dwell, - 'cluster_h5' : self.cluster_h5, - 'pzf_compression_settings' : self.pzf_compression_settings, - 'protocol_name' : self.protocol.filename, - 'series_name' : self.seriesName - }, + info = {'settings' : self.get_settings(), + 'series_name' : self.seriesName, 'available_spool_methods' : self.available_spool_methods } @@ -141,6 +230,9 @@ def get_info(self): return info + def register_acquisition_type(self, name, cls): + self.acquisition_types[name] = cls + def update_settings(self, settings): """ Sets the state of the `SpoolController` by calling set-methods or by @@ -167,12 +259,14 @@ def update_settings(self, settings): preferred for PYMEClusterOfOne. pzf_compression_settings : dict Compression settings relevant for 'Cluster' `method` if - `cluster_h5` is False. See HTTPSpooler.defaultCompSettings. + `cluster_h5` is False. See acquisition_backends.ClusterBackend.default_compression_settings. protocol_name : str Note that passing the protocol name will force a (re)load of the protocol file (even if it is already selected). Notable keys which are not supported through this method include 'series_name', 'seriesName' and 'dirname'. + + """ method = settings.pop('method', None) if method and method != self.spoolType: @@ -180,15 +274,23 @@ def update_settings(self, settings): protocol_name = settings.pop('protocol_name', None) if protocol_name: - self.SetProtocol(protocol_name) + self.protocol_settings.SetProtocol(protocol_name) + pzf_settings = settings.pop('pzf_compression_settings', None) if pzf_settings: self.pzf_compression_settings = dict(pzf_settings) for k, v in settings.items(): - setattr(self, k, v) - + if k in ['z_stepped', 'z_dwell']: + # these settings belong to the protocol + # TODO - rename to protocol.xx??? + setattr(self.protocol_settings, k, v) + else: + setattr(self, k, v) + + with self._status_changed_condition: + self._status_changed_condition.notify_all() @@ -203,7 +305,7 @@ def _sep(self): def dirname(self): return self.get_dirname() - def get_dirname(self, subdirectory=None): + def get_dirname(self, subdirectory=None, spoolType=None): """ Get the current directory name, including any subdirectories from chunking or additional spec. @@ -213,12 +315,19 @@ def get_dirname(self, subdirectory=None): Directory within current set directory to spool this series. The directory will be created if it doesn't already exist. + spoolType : str, optional + Used when using get_dirname externally (e.g. in tiling) to over-ride the spool type. + Returns ------- str spool directory name """ - dir = self._dirname if self.spoolType != 'Cluster' else self._cluster_dirname + + if spoolType is None: + spoolType = self.spoolType + + dir = self._dirname if spoolType.lower() != 'cluster' else self._cluster_dirname if subdirectory != None: dir = dir + self._sep + subdirectory.replace(os.sep, self._sep) @@ -259,16 +368,18 @@ def _GenSeriesName(self): def _checkOutputExists(self, fn): if self.spoolType == 'Cluster': - from PYME.Acquire import HTTPSpooler + #FIXME - remove dependance on HTTPSpooler + from PYME.IO import HTTPSpooler_v2 as HTTPSpooler # special case for HTTP spooling. Make sure 000\series.pcs -> 000/series.pcs pyme_cluster = self.dirname + '/' + fn.replace('\\', '/') logger.debug('Looking for %s (.pcs or .h5) on cluster' % pyme_cluster) - return HTTPSpooler.exists(pyme_cluster + '.pcs') or HTTPSpooler.exists(pyme_cluster + '.h5') + return HTTPSpooler.exists(pyme_cluster + '.pcs') or HTTPSpooler.exists(pyme_cluster + '.h5') or HTTPSpooler.exists(pyme_cluster + '.tiles') #return (fn + '.h5/') in HTTPSpooler.clusterIO.listdir(self.dirname) else: - local_h5 = os.sep.join([self.dirname, fn + '.h5']) - logger.debug('Looking for %s on local machine' % local_h5) - return os.path.exists(local_h5) + local_stub = os.sep.join([self.dirname, fn]) + local_h5 = local_stub + '.h5' + logger.debug('Looking for %s on local machine' % local_stub) + return os.path.exists(local_h5) or os.path.exists(local_stub + '.pcs') or os.path.exists(local_stub + '.tiles') def get_free_space(self): """ @@ -288,7 +399,7 @@ def get_free_space(self): else: from PYME.IO.FileUtils.freeSpace import get_free_space # avoid dirname property here so we can differ building - # 'acquire-spool_subdirectories' to `StartSpooling` + # 'acquire-spool_subdirectories' to `start_spooling` return get_free_space(self._dirname)/1e9 def _update_series_counter(self): @@ -301,7 +412,7 @@ def _update_series_counter(self): def SetSpoolDir(self, dirname): """Set the directory we're spooling into""" - print('setting spool dir: %s' % dirname) + logger.info('Setting spool dir: %s' % dirname) self._dirname = dirname self._cluster_dirname = self.get_cluster_dirname(dirname) #if we've had to quit for whatever reason start where we left off @@ -312,6 +423,10 @@ def _ProgressUpate(self, **kwargs): self._status_changed_condition.notify_all() self.onSpoolProgress.send(self) + + @property + def acquisition_cls(self): + return self.acquisition_types[self.acquisition_type] def _get_queue_name(self, fn, pcs=False, subdirectory=None): """ Get fully resolved uri to spool to @@ -336,14 +451,14 @@ def _get_queue_name(self, fn, pcs=False, subdirectory=None): ext = '.pcs' else: ext = '.h5' + + # allow acquisition types (e.g. tiling) to specify their own extension + ext = getattr(self.acquisition_cls, 'FILE_EXTENSION', ext) return self._sep.join([self.get_dirname(subdirectory), fn + ext]) - def StartSpooling(self, fn=None, stack=None, compLevel=None, - zDwellTime=None, doPreflightCheck=True, - maxFrames=sys.maxsize, pzf_compression_settings=None, - cluster_h5=None, protocol=None, subdirectory=None): + def start_spooling(self, fn=None, settings={}, preflight_mode='interactive'): """ Parameters @@ -351,51 +466,58 @@ def StartSpooling(self, fn=None, stack=None, compLevel=None, fn : str, optional fn can be hardcoded here, otherwise differs to the seriesName property which will create one if need-be. - stack : bool, optional - toggle z-stepping during acquisition. By default None, which differs - to current `SpoolController` state. - compLevel : int, optional - zlib compression level for pytables. Not relevant for `Cluster` - spool method unless `cluster_h5` is True. By default None, which - differs to current `SpoolController` state. - zDwellTime : int, optional - frames per z-step. By default None, which differs to current - `SpoolController` state. - doPreflightCheck : bool, optional - toggle performing pre-flights specified in the acquisition protocol, - by default True. - maxFrames : int, optional - point at which to end the series automatically, by default - sys.maxsize - pzf_compression_settings : dict, optional - Compression settings relevant for 'Cluster' `method` if `cluster_h5` - is False. See HTTPSpooler.defaultCompSettings. By default None, - which differs to current `SpoolController` state. - cluster_h5 : bool, optional - Toggle spooling to single h5 file on cluster rather than pzf file - per frame. Only applicable to 'Cluster' `method` and preferred for - PYMEClusterOfOne. By default None, which differs to current - `SpoolController` state. - protocol : str, optional - path to acquisition protocol. By default None which differs to - current `SpoolController` state. - subdirectory : str, optional - Directory within current set directory to spool this series. The - directory will be created if it doesn't already exist. + settings : dict + keys should be `SpoolController` attributes or properties with + setters. Not all keys must be present, and example keys include: + method : str + One of 'File', 'Cluster', or 'Queue'(py2 only) + hdf_compression_level: int + zlib compression level that pytables should use (spool to + file and queue) + z_stepped : bool + toggle z-stepping during acquisition + z_dwell : int + number of frames to acquire at each z level (predicated on + `SpoolController.z_stepped` being True) + cluster_h5 : bool + Toggle spooling to single h5 file on cluster rather than pzf + file per frame. Only applicable to 'Cluster' `method` and + preferred for PYMEClusterOfOne. + pzf_compression_settings : dict + Compression settings relevant for 'Cluster' `method` if + `cluster_h5` is False. See acquisition_backends.ClusterBackend.defaultCompSettings. + protocol_name : str + Note that passing the protocol name will force a (re)load of + the protocol file (even if it is already selected). + max_frames : int, optional + point at which to end the series automatically, by default + sys.maxsize + subdirectory : str, optional + Directory within current set directory to spool this series. The + directory will be created if it doesn't already exist. + extra_metadata : dict, optional + metadata to supplement this series for entries known prior to + acquisition which do not have handlers to hook start metadata + preflight_mode : str (default='interactive') + What to do when the preflight check fails. Options are 'interactive', 'warn', 'abort' and 'skip' which will + display a dialog and prompt the user, log a warning and continue, and log an error and abort, or skip completely. + The former is suitable for interactive acquisition, whereas one of the latter modes is likely better for automated spooling + via the action manager. + """ + # these settings were managed by the GUI, but are now managed by the # controller, still allow them to be passed in, but default to internals + + acquisition_type = settings.get('acquisition_type', self.acquisition_type) + fn = self.seriesName if fn in ['', None] else fn - stack = self.z_stepped if stack is None else stack - compLevel = self.hdf_compression_level if compLevel is None else compLevel - z_dwell = self.z_dwell if zDwellTime is None else zDwellTime - pzf_compression_settings = self.pzf_compression_settings if pzf_compression_settings is None else pzf_compression_settings - cluster_h5 = self.cluster_h5 if cluster_h5 is None else cluster_h5 - if protocol is None: - protocol, protocol_z = self.protocol, self.protocolZ - else: - pmod = prot.get_protocol(protocol) - protocol, protocol_z = pmod.PROTOCOL, pmod.PROTOCOL_STACK + + #compLevel = settings.get('hdf_compression_level', self.hdf_compression_level) + #pzf_compression_settings = settings.get('pzf_compression_settings', self.pzf_compression_settings) + cluster_h5 = settings.get('cluster_h5', self.cluster_h5) + + subdirectory = settings.get('subdirectory', None) # make directories as needed, makedirs(dir, exist_ok=True) once py2 support is dropped if (self.spoolType != 'Cluster') and (not os.path.exists(self.get_dirname(subdirectory))): @@ -406,71 +528,173 @@ def StartSpooling(self, fn=None, stack=None, compLevel=None, self.seriesName = self._GenSeriesName() raise IOError('A series with the same name already exists') - - if stack: - protocol = protocol_z - protocol.dwellTime = z_dwell - print(protocol) - else: - protocol = protocol - - if doPreflightCheck and not preflight.ShowPreflightResults(None, protocol.PreflightCheck()): - return #bail if we failed the pre flight check, and the user didn't choose to continue - - - #fix timing when using fake camera - if self.scope.cam.__class__.__name__ == 'FakeCamera': - fakeCycleTime = self.scope.cam.GetIntegTime() - else: - fakeCycleTime = None - frameShape = (self.scope.cam.GetPicWidth(), self.scope.cam.GetPicHeight()) - - if self.spoolType == 'Queue': - from PYME.Acquire import QueueSpooler - self.queueName = getRelFilename(self._get_queue_name(fn, subdirectory=subdirectory)) - self.spooler = QueueSpooler.Spooler(self.queueName, self.scope.frameWrangler.onFrame, - frameShape = frameShape, protocol=protocol, - guiUpdateCallback=self._ProgressUpate, complevel=compLevel, - fakeCamCycleTime=fakeCycleTime, maxFrames=maxFrames) - elif self.spoolType == 'Cluster': - from PYME.Acquire import HTTPSpooler + # update launch analysis settings + self.analysis_mode = settings.get('analysis_mode', self.analysis_mode) + self.analysis_rule_name = settings.get('analysis_rule_name', self.analysis_rule_name) + self.analysis_launch_mode = settings.get('analysis_launch_mode', self.analysis_launch_mode) + + if self.spoolType == 'Cluster': self.queueName = self._get_queue_name(fn, pcs=(not cluster_h5), subdirectory=subdirectory) - self.spooler = HTTPSpooler.Spooler(self.queueName, self.scope.frameWrangler.onFrame, - frameShape = frameShape, protocol=protocol, - guiUpdateCallback=self._ProgressUpate, - fakeCamCycleTime=fakeCycleTime, maxFrames=maxFrames, - compressionSettings=pzf_compression_settings, aggregate_h5=cluster_h5) - else: - from PYME.Acquire import HDFSpooler - self.spooler = HDFSpooler.Spooler(self._get_queue_name(fn, subdirectory=subdirectory), - self.scope.frameWrangler.onFrame, - frameShape = frameShape, protocol=protocol, - guiUpdateCallback=self._ProgressUpate, complevel=compLevel, - fakeCamCycleTime=fakeCycleTime, maxFrames=maxFrames) - - #TODO - sample info is probably better handled with a metadata hook - #if sampInf: - # try: - # sampleInformation.getSampleData(self, self.spooler.md) - # except: - # #the connection to the database will timeout if not present - # #FIXME: catch the right exception (or delegate handling to sampleInformation module) - # pass - + self.queueName = self._get_queue_name(fn, subdirectory=subdirectory) + + + from PYME.IO import acquisition_backends + backends = {'File': acquisition_backends.HDFBackend, + 'Cluster': acquisition_backends.ClusterBackend, + 'Memory': acquisition_backends.MemoryBackend, + 'Tiff folder': acquisition_backends.TiffFolderBackend,} + + backend_kwargs = {} + backend_kwargs['dtype'] = self.scope.cam.dtype + if self.spoolType == 'Cluster': + backend_kwargs['cluster_h5'] = settings.get('cluster_h5', self.cluster_h5) + backend_kwargs['compression_settings'] = settings.get('pzf_compression_settings', self.pzf_compression_settings) + elif self.spoolType == 'File': + backend_kwargs['complevel'] = settings.get('hdf_compression_level', self.hdf_compression_level) + + # put preflight mode into settings so we can pass it to the protocol acquisition + settings['preflight_mode'] = preflight_mode + + + + try: + self.spooler = self.acquisition_cls.from_spool_settings(self.scope, settings, backend=backends[self.spoolType], backend_kwargs=backend_kwargs, series_name=self.queueName, spool_controller=self) + except KeyError: + raise RuntimeError('Unknown acquisition type %s' % acquisition_type) + + self.spooler.on_progress.connect(self._ProgressUpate) + + + extra_metadata = settings.get('extra_metadata') + if extra_metadata is not None: + self.spooler.md.mergeEntriesFrom(MetaDataHandler.DictMDHandler(extra_metadata)) + + # NOTE - stopping and starting the framewrangler has moved to the spooler .start() method + #self.scope.frameWrangler.stop() + + # log idle state and un-idle camera before starting spool + self._cam_was_idle = self.scope.cam.GetIdle() + self.scope.cam.SetIdle(False) + try: - self.spooler.onSpoolStop.connect(self.SpoolStopped) - self.spooler.StartSpool() + self.spooler.on_stop.connect(self.SpoolStopped) + self.spooler.start() except: self.spooler.abort() raise + + # restart frame wrangler + #self.scope.frameWrangler.Prepare() + #self.scope.frameWrangler.start() self.onSpoolStart.send(self) + + if self.spoolType == 'Memory': + # open a viewer window for the data that is being acquired + self._display_image() #return a function which can be called to indicate if we are done - return lambda : not self.spooler.spoolOn + return lambda : self.spooler.spool_complete + + def estimate_spool_time(self, settings={}, **kwargs): + """ + Estimate the time to spool a series based on the current settings + + used by queued actions to set timeouts etc ... if in doubt, we should + overestimate. + + Returns + ------- + float + estimated time in seconds + + FIXME - these are extremely rough estimates + FIXME - defer to acquisition type + """ + + acquisition_type = settings.get('acquisition_type', self.acquisition_type) + + if acquisition_type == 'ProtocolAcquisition': + #FIXME - this is a very rough estimate + n_frames = settings.get('max_frames', 100000) + + try: + return 1.25 * n_frames / self.scope.cam.GetFPS() # per series + except NotImplementedError: + # specifically the simulated camera here, which has a non-predictable frame rate + # use a conservative default of 10 s/frame (should not matter as simulation will generally not be doing 10s of thousands of series) + return 10*n_frames + + + else: + # 30 minutes for all other acquisition types + # TODO - does this need to be longer for tiling?? + return 30*60 + + def get_settings(self, method_only=False): + """Get the current settings for the spool controller + + Used when adding actions to the action manager - this should freeze + the relevant settings for the acquisition type and method. + """ + settings = {'method' : self.spoolType, + } + + if self.spoolType == 'File': + settings['hdf_compression_level'] = self.hdf_compression_level + + if self.spoolType == 'Cluster': + settings['cluster_h5'] = self.cluster_h5 + settings['pzf_compression_settings'] = self.pzf_compression_settings + + if method_only: + return settings + + else: + settings['acquisition_type'] = self.acquisition_type + + settings['analysis_mode'] = self.analysis_mode + settings['analysis_launch_mode'] = self.analysis_launch_mode + settings['analysis_rule_name'] = self.analysis_rule_name + + settings.update(self.acquisition_types[self.acquisition_type].get_frozen_settings(self.scope, self)) + + return settings + + + def _display_image(self): + ''' Display the image in a viewer (for memory backend) + ''' + try: + assert isinstance(self.spooler.storage, acquisition_backends.MemoryBackend) + + import wx + if not wx.App.IsMainLoopRunning(): + logger.debug("No wx app, can't show image") + return + + from PYME.DSView import ViewIm3D + self._view = ViewIm3D(self.spooler.storage.image) + self.scope.frameWrangler.onFrameGroup.connect(self._update_display) + except: + logger.exception('Error displaying image') + + def _update_display(self, *args, **kwargs): + ''' Update the displayed image (for memory backend) + ''' + import wx + wx.CallAfter(self._view.view.Redraw) + + def _unlink_display(self, *args, **kwargs): + ''' Unlink the display from the frameWrangler (for memory backend) + ''' + try: + self.scope.frameWrangler.onFrameGroup.disconnect(self._update_display) + except: + pass @property def display_dirname(self): @@ -491,13 +715,29 @@ def display_dirname(self): def StopSpooling(self, **kwargs): """GUI callback to stop spooling.""" - self.spooler.StopSpool() + self.spooler.stop() def SpoolStopped(self, **kwargs): self.seriesCounter +=1 self.seriesName = self._GenSeriesName() + + logger.info('Spooling stopped') + + self.scope.cam.SetIdle(self._cam_was_idle) + + self.on_stop.send(self) + + if self.analysis_launch_mode == 'series-end': + self.LaunchAnalysis() - self.onSpoolStop.send(self) + + try: + self.spooler.on_progress.disconnect(self._ProgressUpate) + self._ProgressUpate() + self._unlink_display() + except AttributeError: + pass + @property def autostart_analysis(self): @@ -508,72 +748,112 @@ def autostart_analysis(self): def LaunchAnalysis(self): + from warnings import warn + warn('LaunchAnalysis is deprecated, use launch_analysis instead', DeprecationWarning) + self.launch_analysis() + + def launch_analysis(self): """Launch analysis """ - from PYME.Acquire import QueueSpooler, HTTPSpooler - - dh5view_cmd = 'dh5view' - if sys.platform == 'win32': - dh5view_cmd = 'dh5view.exe' - - if self.autostart_analysis: - dh5view_cmd += ' -g' - - if isinstance(self.spooler, QueueSpooler.Spooler): #queue or not - subprocess.Popen('%s -q %s QUEUE://%s' % (dh5view_cmd, self.spooler.tq.URI, self.queueName), shell=True) - elif isinstance(self.spooler, HTTPSpooler.Spooler): #queue or not - if self.autostart_analysis: - # launch analysis in a separate thread - t = threading.Thread(target=self.launch_cluster_analysis) + import posixpath + + try: + if not self.spoolType == 'Cluster': + from PYME import pyme_warnings as warnings + warnings.warn('Analysis is only supported for cluster spooling', category=RuntimeWarning) + + if self.analysis_mode == 'interactive': + subprocess.Popen('%s %s' % (self.dh5view_cmd, self.spooler.getURL()), shell=True) + + elif self.analysis_mode == 'rule-based': + seriesName = self.spooler.getURL() + + try: + # we have the chained analysis module loaded + rule_factory = self.scope.analysis_rules[self.analysis_rule_name].rule_factories[0] + + context = { + 'seriesName': seriesName, + 'inputs': {'input': seriesName}, # needed for recipes + 'output_dir': posixpath.split(seriesName)[0], + 'spooler': self.spooler.storage, # for SpoolLocalLocalization rule completeness check + } + + rule = rule_factory.get_rule(context=context) + # launch analysis in a separate thread + + except AttributeError: + # we don't have the chained analysis module loaded + from PYME.cluster import rules + import warnings + warnings.warn('using legacy automated localisation rule - please add the chained analysis module to your init and use this instead', category=RuntimeWarning) + rule = rules.LocalisationRule(seriesName=seriesName, analysisMetadata=self.scope.analysisSettings.analysisMDH) + + except KeyError: + raise RuntimeError('Analysis rule %s not found' % self.analysis_rule_name) + + t = threading.Thread(target=rule.push) t.start() # keep track of a couple launching threads to make sure they have ample time to finish before joining if self._analysis_launchers.full(): self._analysis_launchers.get().join() self._analysis_launchers.put(t) - else: - subprocess.Popen('%s %s' % (dh5view_cmd, self.spooler.getURL()), shell=True) - - def launch_cluster_analysis(self): - from PYME.cluster import rules - - seriesName = self.spooler.getURL() - try: - #HTTPRulePusher.launch_localize(self.scope.analysisSettings.analysisMDH, seriesName) - rules.LocalisationRule(seriesName=seriesName, analysisMetadata=self.scope.analysisSettings.analysisMDH).push() - except: - logger.exception('Error launching analysis for %s' % seriesName) + self._rule_outputs = rule.output_files + except: + logger.exception('Error launching analysis') - def SetProtocol(self, protocolName=None, reloadProtocol=True): - """ - Set the current protocol. - - Parameters - ---------- - protocolName: str - path to protocol file including extension - reloadProtocol : bool - currently ignored; protocol module is reinitialized regardless. + # TODO - do these belong here? + @property + def pymevis_cmd(self): + if sys.platform == 'win32': + return 'PYMEVis.exe' + else: + return 'PYMEVis' - See also: PYME.Acquire.Protocols. - """ - - if (protocolName is None) or (protocolName == ''): - self.protocol = prot.NullProtocol - self.protocolZ = prot.NullZProtocol + @property + def pymeimage_cmd(self): + if sys.platform == 'win32': + return 'PYMEImage.exe' else: - #pmod = __import__('PYME.Acquire.Protocols.' + protocolName.split('.')[0],fromlist=['PYME', 'Acquire','Protocols']) - - #if reloadProtocol: - # reload(pmod) #force module to be reloaded so that changes in the protocol will be recognised - pmod = prot.get_protocol(protocol_name=protocolName, reloadProtocol=reloadProtocol) + return 'PYMEImage' + + dh5view_cmd = pymeimage_cmd + + def open_analysis(self): + """Open the currenly running analysis in PYMEVis""" + import subprocess + + output = self._rule_outputs.get('results') + + # get the URL + if output.endswith('.h5r'): + uri = output + '?live' + subprocess.Popen('%s %s' % (self.pymevis_cmd, uri), shell=True) + elif output.endswith('.h5') or output.endswith('.tif'): + uri = output + subprocess.Popen('%s %s' % (self.pymeimage_cmd, uri), shell=True) + + def open_view(self): + if hasattr(self.spooler, '_launch_viewer'): + # TODO - make less special case - maybe defer normal launch to the spooler as well + self.spooler._launch_viewer() + else: + subprocess.Popen('%s %s' % (self.pymeimage_cmd, self.spooler.getURL()), shell=True) + - self.protocol = pmod.PROTOCOL - self.protocol.filename = protocolName - self.protocolZ = pmod.PROTOCOL_STACK - self.protocolZ.filename = protocolName - self.z_dwell = self.protocolZ.dwellTime + + # def launch_cluster_analysis(self): + # from PYME.cluster import rules + + # seriesName = self.spooler.getURL() + # try: + # #HTTPRulePusher.launch_localize(self.scope.analysisSettings.analysisMDH, seriesName) + # rules.LocalisationRule(seriesName=seriesName, analysisMetadata=self.scope.analysisSettings.analysisMDH).push() + # except: + # logger.exception('Error launching analysis for %s' % seriesName) + def SetSpoolMethod(self, method): """Set the spooling method @@ -605,6 +885,7 @@ def info(self): @webframework.register_endpoint('/info_longpoll', output_is_json=False) def info_longpoll(self): with self.spool_controller._status_changed_condition: + self.spool_controller._status_changed_condition.wait() return self.spool_controller.get_info() @webframework.register_endpoint('/settings', output_is_json=False) @@ -623,18 +904,22 @@ def stop_spooling(self): return 'OK' @webframework.register_endpoint('/start_spooling', output_is_json=False) - def start_spooling(self, filename=None, stack=None, hdf_comp_level=None, - z_dwell=None, preflight_check=True, - max_frames=sys.maxsize, pzf_compression_settings=None, - cluster_h5=None, protocol=None, subdirectory=None): + def start_spooling(self, body, filename=None, preflight_mode='abort'): """ + See also SpoolController.start_spooling() Parameters ---------- filename : str, optional fn can be hardcoded here, otherwise differs to the seriesName property which will create one if need-be. - stack : bool, optional + preflight_mode : str, default == 'abort' + One of 'warn', 'abort', 'skip, or 'interactive'. Note that 'interactive' requires an active wx.App + + The majority of parameters are passed in the request body, which should be a json-formatted dictionary with the + the following keys (see also `settings` parameter to `SpoolController.start_spooling` + + z_stepped : bool, optional toggle z-stepping during acquisition. By default None, which differs to current `SpoolController` state. hdf_comp_level : int, optional @@ -644,16 +929,13 @@ def start_spooling(self, filename=None, stack=None, hdf_comp_level=None, z_dwell : int, optional frames per z-step. By default None, which differs to current `SpoolController` state. - preflight_check : bool, optional - toggle performing pre-flights specified in the acquisition protocol, - by default True. max_frames : int, optional point at which to end the series automatically, by default sys.maxsize pzf_compression_settings : dict, optional Compression settings relevant for 'Cluster' `method` if `cluster_h5` - is False. See HTTPSpooler.defaultCompSettings. By default None, - which differs to current `SpoolController` state. + is False. See acquisition_backends.ClusterBackend.defaultCompSettings. By default None, + which defers to current `SpoolController` state. cluster_h5 : bool, optional Toggle spooling to single h5 file on cluster rather than pzf file per frame. Only applicable to 'Cluster' `method` and preferred for @@ -665,10 +947,21 @@ def start_spooling(self, filename=None, stack=None, hdf_comp_level=None, subdirectory : str, optional Directory within current set directory to spool this series. The directory will be created if it doesn't already exist. + extra_metadata : dict, optional + metadata to supplement this series for entries known prior to + acquisition which do not have handlers to hook start metadata + stack_settings : dict, optional + The stack settings. See PYME.Acquire.stackSettings.StackSettings. By default the global StackSettings instance + is used. + """ - self.spool_controller.StartSpooling(filename, stack, hdf_comp_level, - z_dwell, preflight_check, - max_frames, - pzf_compression_settings, - cluster_h5, protocol, subdirectory) + import json + # FIXME - do some sanity checks on filename (this can't be as simple as urlescaping, as we need to support + # URIs as well as filenames). In practice this is best dealt with by enforcing authentication and only using on a + # trusted network. + if len(body) > 0: + # have settings in message body + self.spool_controller.start_spooling(filename, settings=json.loads(body), preflight_mode=preflight_mode) + else: + self.spool_controller.start_spooling(filename, preflight_mode=preflight_mode) return 'OK' diff --git a/PYME/Acquire/Utils/failsafe.py b/PYME/Acquire/Utils/failsafe.py new file mode 100644 index 000000000..06f8d4f99 --- /dev/null +++ b/PYME/Acquire/Utils/failsafe.py @@ -0,0 +1,157 @@ + +import weakref +from PYME.util import webframework +import threading +import logging + +logger = logging.getLogger(__name__) + + +class Failsafe(object): + def __init__(self, microscope, email_info=None): + """ + + Parameters + ---------- + microscope : PYME.Acquire.microscope.Microscope + email_info : dict + sender : str + email address to send from + password : str + password for sender + receiver : str + destination email address + """ + self.scope = weakref.ref(microscope) + self.email_info = email_info + + # Uncomment the following after merge of #799 + # Register as a server endpoint + # from PYME.Acquire import webui + # webui.add_endpoints(self, '/failsafe') + + @webframework.register_endpoint('/kill', output_is_json=False) + def kill(self, message=''): + """ + kill the lasers + + Parameters + ---------- + message : str + something about why you needed to kill using the interlock + + TODOs: + - require authentication for this endpoint + - make more generic + - wrap the try-except stuff into a common function, + - find a way of passing list of "actions" / supplementary actions in constructor + - move non-standard stuff which is not guaranteed to be present (piFoc, focus_lock, etc into this list) + - find a way of doing email/notification which is a) multi-user aware and b) does not require plaintext passwords + """ + try: # kill the lasers + self.scope().turnAllLasersOff() + except Exception as e: + logger.error(str(e)) + + logger.error('interlock activated: %s' % message) + + try: # pause the action queue + self.scope().action_manager.paused = True + except Exception as e: + logger.error(str(e)) + + try: # stop spooling + self.scope().spoolController.StopSpooling() + except Exception as e: + logger.error(str(e)) + + try: # unlock focus lock + self.scope().focus_lock.DisableLock() + except Exception as e: + logger.error(str(e)) + + try: # lower the objective + self.scope().piFoc.MoveTo(0, self.scope().piFoc.GetMin(0)) + except Exception as e: + logger.error(str(e)) + + # call home + if self.email_info is not None: + try: + import smtplib, ssl + context = ssl.create_default_context() + with smtplib.SMTP_SSL("smtp.gmail.com", context=context) as server: + server.login(self.email_info['sender'], + self.email_info['password']) + server.sendmail(self.email_info['sender'], + self.email_info['receiver'], message) + except Exception as e: + logger.error(str(e)) + +class FailsafeServer(webframework.APIHTTPServer, Failsafe): + def __init__(self, microscope, email_info=None, port=9119, + bind_address=''): + """ + + NOTE - this will likely not be around long, as it would be preferable to + add the interlock endpoints to `PYME.acquire_server.AcquireHTTPServer` + and run a single server process on the microscope computer. + + Parameters + ---------- + microscope: PYME.Acquire.microscope.Microscope + email_info : dict + sender : str + email address to send from + password : str + password for sender + receiver : str + destination email address + port : int + port to listen on + bind_address : str, optional + specifies ip address to listen on, by default '' will bind to local + host. + """ + webframework.APIHTTPServer.__init__(self, (bind_address, port)) + Failsafe.__init__(self, microscope, email_info) + + self.daemon_threads = True + self._server_thread = threading.Thread(target=self._serve) + self._server_thread.daemon_threads = True + self._server_thread.start() + + def _serve(self): + try: + logger.info('Starting Interlock server on %s:%s' % (self.server_address[0], + self.server_address[1])) + self.serve_forever() + finally: + logger.info('Shutting down Interlock server ...') + self.shutdown() + self.server_close() + + +class FailsafeClient(object): + """ + For systems running two PYMEAcquire instances, e.g. one for drift + tracking, this allows you to define a scope.interlock in both systems to + kill the lasers in the main PYMEAcquire. + + TODO: Do we really need an explicit python client?? Just giving the focus lock code the HTTP endpoint coded as a string and + having it do a requests.get() on the endpoint (if defined) might be sufficient and result in less spaghetti. Clients + are needed for the remote piezos as we are substituting them for a python object with a previously defined interface, + but it might be better to just call the REST interface the official one in cases like this. + """ + def __init__(self, host='127.0.0.1', port=9119, name='interlock'): + import requests + + self.host = host + self.port = port + self.name = name + + self.base_url = 'http://%s:%d' % (host, port) + self._session = requests.Session() + + def kill(self, message=''): + return self._session.get(self.base_url + '/kill?message=%s' % message) diff --git a/PYME/Acquire/Utils/fastTiler.py b/PYME/Acquire/Utils/fastTiler.py index 5e0004c67..9662d0626 100644 --- a/PYME/Acquire/Utils/fastTiler.py +++ b/PYME/Acquire/Utils/fastTiler.py @@ -78,7 +78,7 @@ def OnTick(self, sender, frameData, **kwargs): self.scope.frameWrangler.stop() self.runInProgress=False self.i = -1 - print('foo') + #print('foo') if len(self.startPositions) > 0: nextX, nextY = self.startPositions.pop(0) self.scope.stage.MoveTo(0, nextX) @@ -99,7 +99,7 @@ def OnTick(self, sender, frameData, **kwargs): self.j = int((xp - self.rect[0])/self.pixelsize) self.i = int((yp - self.rect[1])/self.pixelsize) - print((self.i, self.j, self.data.shape)) + #print((self.i, self.j, self.data.shape)) nextY = self.endYPositions.pop(0) @@ -129,7 +129,7 @@ def __init__(self, scope, rect, xstep = .00005, ystep=10.5, pixelsize=PIXELSIZE) self.pixelsize = pixelsize fastTiler.__init__(self, scope, ystep=ystep, pixelsize=pixelsize) - print((self.startPositions)) + #print((self.startPositions)) def GetBoundingRect(self): return self.rect diff --git a/PYME/Acquire/Utils/pointScanner.py b/PYME/Acquire/Utils/pointScanner.py index ae40b0aad..28050b083 100644 --- a/PYME/Acquire/Utils/pointScanner.py +++ b/PYME/Acquire/Utils/pointScanner.py @@ -31,38 +31,29 @@ import logging logger = logging.getLogger(__name__) -class PointScanner(object): - def __init__(self, scope, pixels = 10, pixelsize=0.1, dwelltime = 1, background=0, avg=True, evtLog=False, sync=False, - trigger=False, stop_on_complete=False, return_to_start=True): + +class Scanner(object): + def __init__(self, scope, pixels = 10, pixelsize=0.1, evtLog=False, stop_on_complete=False, **kwargs): """ :param return_to_start: bool Flag to toggle returning home at the end of the scan. False leaves scope position as-is on scan completion. """ self.scope = scope - #self.xpiezo = xpiezo - #self.ypiezo = ypiezo - - self.trigger = trigger - - self.dwellTime = dwelltime - self.background = background - self.avg = avg + self.pixels = pixels self.pixelsize = pixelsize + self._stop_on_complete = stop_on_complete - self._return_to_start = return_to_start + if np.isscalar(pixelsize): self.pixelsize = np.array([pixelsize, pixelsize]) self.evtLog = evtLog - self.sync = sync - self._rlock = threading.Lock() - + + self._rlock = threading.RLock() self.running = False - self._uuid = uuid.uuid4() - self.on_stop = dispatch.Signal() def genCoords(self): self.currPos = self.scope.GetPos() @@ -87,70 +78,34 @@ def genCoords(self): #self.currPos = (self.xpiezo[0].GetPos(self.xpiezo[1]), self.ypiezo[0].GetPos(self.ypiezo[1])) self.imsize = self.nx*self.ny + + @classmethod + def n_tiles(cls, pixels=10, **kwargs): + if np.isscalar(pixels): + return (pixels+1)**2 + elif np.isscalar(pixels[0]): + return (pixels[0]+1) * (pixels[1]+1) + else: + return (len(pixels[0])+1) * (len(pixels[1])+1) # +1 to match the number of tiles due to np.arrange + @property + def num_tiles(self): + return self.n_tiles(self.pixels) - def start(self): + def init_scan(self): self.running = True - #pixels = np.array(pixels) - -# if np.isscalar(self.pixels): -# #constant - use as number of pixels -# #center on current piezo position -# self.xp = self.pixelsize*np.arange(-self.pixels/2, self.pixels/2 +1) + self.xpiezo[0].GetPos(self.xpiezo[1]) -# self.yp = self.pixelsize*np.arange(-self.pixels/2, self.pixels/2 +1) + self.ypiezo[0].GetPos(self.ypiezo[1]) -# elif np.isscalar(self.pixels[0]): -# #a 1D array - numbers in either direction centered on piezo pos -# self.xp = self.pixelsize*np.arange(-self.pixels[0]/2, self.pixels[0]/2 +1) + self.xpiezo[0].GetPos(self.xpiezo[1]) -# self.yp = self.pixelsize*np.arange(-self.pixels[1]/2, self.pixels[1]/2 +1) + self.ypiezo[0].GetPos(self.ypiezo[1]) -# else: -# #actual pixel positions -# self.xp = self.pixels[0] -# self.yp = self.pixels[1] -# -# self.nx = len(self.xp) -# self.ny = len(self.yp) -# -# self.imsize = self.nx*self.ny - self.genCoords() - self.callNum = 0 - - if self.avg: - self.image = np.zeros((self.nx, self.ny)) - - #self.ds = scope.frameWrangler.currentFrame - - self.view = View3D(self.image) - - #self.xpiezo[0].MoveTo(self.xpiezo[1], self.xp[0]) - #self.ypiezo[0].MoveTo(self.ypiezo[1], self.yp[0]) + self.pos_idx = 0 - #self.scope.SetPos(x=self.xp[0], y = self.yp[0]) - self.scope.frameWrangler.stop() - self.scope.state.setItems({'Positioning.x' : self.xp[0], 'Positioning.y' : self.yp[0]}, stopCamera = True) - if self.trigger: - self.scope.cam.SetAcquisitionMode(self.scope.cam.MODE_SOFTWARE_TRIGGER) - self.scope.frameWrangler.start() - - #if self.sync: - # while not self.xpiezo[0].IsOnTarget(): #wait for stage to move - # time.sleep(.05) + with self.scope.frameWrangler.spooling_stopped(): + self.scope.state.setItems({'Positioning.x' : self.xp[0], 'Positioning.y' : self.yp[0]}, stopCamera = True) if self.evtLog: - eventLog.logEvent('ScannerXPos', '%3.6f' % self.xp[0]) - eventLog.logEvent('ScannerYPos', '%3.6f' % self.yp[0]) - - - #self.scope.frameWrangler.WantFrameNotification.append(self.tick) - self.scope.frameWrangler.onFrame.connect(self.tick, dispatch_uid=self._uuid) - - if self.trigger: - self.scope.cam.FireSoftwareTrigger() + eventLog.logEvent('ScannerXPos', '%3.6f' % self.xp[0]) + eventLog.logEvent('ScannerYPos', '%3.6f' % self.yp[0]) - #if self.sync: - # self.scope.frameWrangler.HardwareChecks.append(self.onTarget) def onTarget(self): #FIXME @@ -172,24 +127,103 @@ def _position_for_index(self, callN): return new_x, new_y - def tick(self, frameData, **kwargs): + def next_pos(self, idx=None, **kwargs): with self._rlock: if not self.running: - return + return False + + if idx is None: + idx = self.pos_idx + 1 - try: - cam_trigger = self.scope.cam.GetAcquisitionMode() == self.scope.cam.MODE_SOFTWARE_TRIGGER - except AttributeError: - cam_trigger = False + if idx >= self.imsize: + # we've acquired the last frame + if self._stop_on_complete: + self._stop() + return False + + #move piezo + new_x, new_y = self._position_for_index(idx) + + self.scope.state.setItems({'Positioning.x' : new_x, + 'Positioning.y' : new_y + })#, stopCamera = not cam_trigger) + + if self.evtLog: + eventLog.logEvent('ScannerXPos', '%3.6f' % self.scope.state['Positioning.x']) + eventLog.logEvent('ScannerYPos', '%3.6f' % self.scope.state['Positioning.y']) + + self.pos_idx += 1 + return True + + def _stop(self): + self.running = False + + def return_home(self): + logger.debug('Returning home : %s' % self.currPos) + self.scope.state.setItems({'Positioning.x': self.currPos['x'], + 'Positioning.y': self.currPos['y'], + }, stopCamera=True) + + + def stop(self): + with self._rlock: + self._stop() + + +class PointScanner(Scanner): + def __init__(self, scope, pixels = 10, pixelsize=0.1, dwelltime = 1, background=0, avg=True, evtLog=False, sync=False, + trigger=False, stop_on_complete=False, return_to_start=True): + """ + :param return_to_start: bool + Flag to toggle returning home at the end of the scan. False leaves scope position as-is on scan completion. + """ + Scanner.__init__(self, scope, pixels, pixelsize, evtLog, stop_on_complete) + + self.trigger = trigger + + self.dwellTime = dwelltime + self.background = background + self.avg = avg + + self._return_to_start = return_to_start + + + self.evtLog = evtLog + self.sync = sync - #logger.debug('Cam_trigger: %s' % repr(cam_trigger)) + #self._rlock = threading.Lock() + + #self.running = False + self._uuid = uuid.uuid4() + self.on_stop = dispatch.Signal() + + + def start(self): + with self.scope.frameWrangler.spooling_stopped(): + if self.trigger: + self.scope.cam.SetAcquisitionMode(self.scope.cam.MODE_SOFTWARE_TRIGGER) + + self.init_scan() + self.scope.frameWrangler.onFrame.connect(self.on_frame, dispatch_uid=self._uuid) + + self.callNum = 0 + + if self.avg: + self.image = np.zeros((self.nx, self.ny)) + self.view = View3D(self.image) + + + def on_frame(self, frameData, **kwargs): + with self._rlock: + if not self.running: + return #print self.callNum if (self.callNum % self.dwellTime) == 0: #record pixel in overview callN = int(self.callNum/self.dwellTime) if self.avg: - self.image[callN % self.nx, int((callN % (self.image.size))/self.nx)] = self.scope.currentFrame.mean() - self.background + self.image[callN % self.nx, int((callN % (self.image.size))/self.nx)] = frameData.mean() - self.background self.view.Refresh() if self.callNum >= self.dwellTime * self.imsize - 1: @@ -198,28 +232,17 @@ def tick(self, frameData, **kwargs): self._stop() return + if ((self.callNum +1) % self.dwellTime) == 0: - #move piezo - callN = int((self.callNum+1)/self.dwellTime) - new_x, new_y = self._position_for_index(callN) - - self.scope.state.setItems({'Positioning.x' : new_x, - 'Positioning.y' : new_y - }, stopCamera = not cam_trigger) + with self.scope.frameWrangler.spooling_paused(): + self.next_pos(callN) - #print 'SetP' - - if self.evtLog: - #eventLog.logEvent('ScannerXPos', '%3.6f' % self.xp[callN % self.nx]) - #eventLog.logEvent('ScannerYPos', '%3.6f' % self.yp[(callN % (self.imsize))/self.nx]) - eventLog.logEvent('ScannerXPos', '%3.6f' % self.scope.state['Positioning.x']) - eventLog.logEvent('ScannerYPos', '%3.6f' % self.scope.state['Positioning.y']) - - if cam_trigger: + elif self.scope.cam.GetAcquisitionMode() == self.scope.cam.MODE_SOFTWARE_TRIGGER: + # make sure triggering still works when dwelltime > 1 + # TODO - this is a bit hacky #logger.debug('Firing camera trigger') self.scope.cam.FireSoftwareTrigger() - if self.evtLog: - eventLog.logEvent('StartAq',"") + # # #if self.sync: @@ -228,15 +251,12 @@ def tick(self, frameData, **kwargs): self.callNum += 1 - def _stop(self): + def _stop(self, send_stop=True): self.running = False - #self.xpiezo[0].MoveTo(self.xpiezo[1], self.currPos[0]) - #self.ypiezo[0].MoveTo(self.ypiezo[1], self.currPos[1]) - - #self.scope.SetPos(**self.currPos) + try: #self.scope.frameWrangler.WantFrameNotification.remove(self.tick) - self.scope.frameWrangler.onFrame.disconnect(self.tick, dispatch_uid=self._uuid) + self.scope.frameWrangler.onFrame.disconnect(self.on_frame, dispatch_uid=self._uuid) #if self.sync: # self.scope.frameWrangler.HardwareChecks.remove(self.onTarget) except: @@ -244,13 +264,13 @@ def _stop(self): self.scope.frameWrangler.stop() - self.on_stop.send(self) + if send_stop: + # optionally defer sending the stop signal until after a derived class _stop + # method has run + self.on_stop.send(self) if self._return_to_start: - logger.debug('Returning home : %s' % self.currPos) - self.scope.state.setItems({'Positioning.x': self.currPos['x'], - 'Positioning.y': self.currPos['y'], - }, stopCamera=True) + self.return_home() self.scope.turnAllLasersOff() @@ -266,7 +286,10 @@ def stop(self): self._stop() -class CircularPointScanner(PointScanner): +class CircularScanner(Scanner): + def __init__(self, scope, pixels = 10, pixelsize=0.1, evtLog=False, stop_on_complete=False): + Scanner.__init__(self, scope, pixels, pixelsize, evtLog, stop_on_complete) + def genCoords(self): """ Generate coordinates for square ROIs evenly distributed within a circle. Order them first by radius, and then diff --git a/PYME/Acquire/Utils/sarcSpacing.py b/PYME/Acquire/Utils/sarcSpacing.py index 34ea22364..1579c7b1e 100755 --- a/PYME/Acquire/Utils/sarcSpacing.py +++ b/PYME/Acquire/Utils/sarcSpacing.py @@ -32,7 +32,7 @@ class SarcomereChecker: def __init__(self, parent, menu, scope, key = 'F12'): self.scope = scope - idSarcCheck = wx.NewId() + idSarcCheck = wx.NewIdRef() self.menu = wx.Menu(title = '') diff --git a/PYME/Acquire/Utils/tiler.py b/PYME/Acquire/Utils/tiler.py index 655322f93..0bb25e63c 100644 --- a/PYME/Acquire/Utils/tiler.py +++ b/PYME/Acquire/Utils/tiler.py @@ -4,50 +4,124 @@ import time from PYME.IO import MetaDataHandler import os +import uuid +import datetime from PYME.contrib import dispatch import logging logger = logging.getLogger(__name__) -class Tiler(pointScanner.PointScanner): - def __init__(self, scope, tile_dir, n_tiles = 10, tile_spacing=None, dwelltime = 1, background=0, evtLog=False, - trigger=False, base_tile_size=256, return_to_start=True): +from PYME.Acquire.acquisition_base import AcquisitionBase +from PYME.IO import acquisition_backends +from PYME.Acquire import eventLog, microscope + +class TileAcquisition(AcquisitionBase): + FILE_EXTENSION = '.tiles' # TODO - make .zarr compatable and use .zarr instead + + def __init__(self, scope : microscope.Microscope, tile_dir : str, n_tiles = 10, tile_spacing=None, dwelltime = 1, background=0, evtLog=None, + trigger='auto', base_tile_size=256, return_to_start=True, save_raw=False, backend='file', backend_kwargs={}): """ :param return_to_start: bool Flag to toggle returning home at the end of the scan. False leaves scope position as-is on scan completion. """ + if trigger == 'auto': + trigger = scope.cam.supports_software_trigger + self._trigger = trigger + + self._return_to_start = return_to_start + + if evtLog is None: + if save_raw: + evtLog = True + else: + evtLog = False + + fs = np.array(scope.frameWrangler.currentFrame.shape[:2]) + if tile_spacing is None: - fs = np.array(scope.frameWrangler.currentFrame.shape[:2]) #calculate tile spacing such that there is 20% overlap. - tile_spacing = 0.8*fs*np.array(scope.GetPixelSize()) + tile_spacing = 0.8 + + tile_spacing_um = tile_spacing*fs*np.array(scope.GetPixelSize()) - pointScanner.PointScanner.__init__(self, scope=scope, pixels=n_tiles, pixelsize=tile_spacing, - dwelltime=dwelltime, background=background, avg=False, evtLog=evtLog, - trigger=trigger, stop_on_complete=True, return_to_start=return_to_start) + # pointScanner.PointScanner.__init__(self, scope=scope, pixels=n_tiles, pixelsize=tile_spacing_um, + # dwelltime=dwelltime, background=background, avg=False, evtLog=evtLog, + # trigger=trigger, stop_on_complete=True, return_to_start=return_to_start) + + self._scanner = pointScanner.Scanner(scope, pixels=n_tiles, pixelsize=tile_spacing_um, evtLog=evtLog, stop_on_complete=True) + self.scope = scope self._tiledir = tile_dir self._base_tile_size = base_tile_size + + self._background = background self._flat = None #currently not used + + self._uuid = uuid.uuid4() # for dispatch self._last_update_time = 0 + + if backend is acquisition_backends.ClusterBackend: + self._backend_type='cluster' + elif backend is acquisition_backends.HDFBackend: + self._backend_type='file' + else: + raise ValueError('Unknown backend') + + # save the raw frames as well as the constructed pyramid - useful if we want to do something + # fancy like cross-correlation based alignment of raw frames before stitching + self._save_raw = save_raw + if save_raw: + if (self._backend_type == 'cluster') and not backend_kwargs.get('cluster_h5', False): + fn = 'raw_frames.pcs' + else: + fn = 'raw_frames.h5' + + + os.makedirs(tile_dir, exist_ok=True) + self.storage = backend(os.path.join(tile_dir, fn), **backend_kwargs) + - self.on_stop = dispatch.Signal() - self.progress = dispatch.Signal() + #self.on_stop = dispatch.Signal() + #self.on_progress = dispatch.Signal() + AcquisitionBase.__init__(self) + @classmethod + def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None): + '''Create an Acquisition object from settings and a backend.''' + + tiling_settings = { + 'tile_dir': series_name, + } + + tiling_settings.update(settings.get('tiling_settings', scope.tile_settings)) + + #fix timing when using fake camera + #TODO - move logic into backend? + if scope.cam.__class__.__name__ == 'FakeCamera': + backend_kwargs['spoof_timestamps'] = True + backend_kwargs['cycle_time'] = scope.cam.GetIntegTime() + + return cls(scope=scope, backend=backend, backend_kwargs=backend_kwargs, **tiling_settings) + + @classmethod + def get_frozen_settings(cls, scope, spool_controller=None): + return {'tiling_settings': getattr(scope, 'tile_settings', {})} - def _gen_weights(self): - sh = self.scope.frameWrangler.currentFrame.shape[:2] - self._weights = np.ones(sh) + @classmethod + def get_tiled_area(cls, scope, settings): + fs = np.array(scope.frameWrangler.currentFrame.shape[:2]) + #calculate tile spacing such that there is 20% overlap. + tile_spacing = settings.get(0.8*fs*np.array(scope.GetPixelSize())) + + nx, ny = settings.get('n_tiles', (10, 10)) + return nx * fs[0] * tile_spacing[0], ny * fs[1] * tile_spacing[1] - edgeRamp = min(100, int(.25 * sh[0])) - self._weights[:edgeRamp, :] *= np.linspace(0, 1, edgeRamp)[:, None] - self._weights[-edgeRamp:, :,] *= np.linspace(1, 0, edgeRamp)[:, None] - self._weights[:, :edgeRamp] *= np.linspace(0, 1, edgeRamp)[None, :] - self._weights[:, -edgeRamp:] *= np.linspace(1, 0, edgeRamp)[None, :] def start(self): - self._gen_weights() - self.genCoords() + #self._weights =tile_pyramid.ImagePyramid.frame_weights(self.scope.frameWrangler.currentFrame.shape[:2]).squeeze() + + self._scanner.genCoords() #metadata handling self.mdh = MetaDataHandler.NestedClassMDHandler() @@ -58,11 +132,11 @@ def start(self): for mdgen in MetaDataHandler.provideStartMetadata: mdgen(self.mdh) - self._x0 = np.min(self.xp) # get the upper left corner of the scan, regardless of shape/fill/start - self._y0 = np.min(self.yp) + self._x0 = np.min(self._scanner.xp) # get the upper left corner of the scan, regardless of shape/fill/start + self._y0 = np.min(self._scanner.yp) self._pixel_size = self.mdh.getEntry('voxelsize.x') - self.background = self.mdh.getOrDefault('Camera.ADOffset', self.background) + self._background = self.mdh.getOrDefault('Camera.ADOffset', self._background) # calculate origin independent of the camera ROI setting to store in # metadata for use in e.g. SupertileDatasource.DataSource.tile_coords_um @@ -71,73 +145,195 @@ def start(self): x0 = self._x0 + self._pixel_size*x0_cam # offset in [um] y0 = self._y0 + self._pixel_size*y0_cam - self.P = tile_pyramid.ImagePyramid(self._tiledir, self._base_tile_size, x0=x0, y0=y0, + if self._backend_type == 'cluster': + from PYME.Analysis import distributed_pyramid + self.P = distributed_pyramid.DistributedImagePyramid(self._tiledir, self._base_tile_size, x0=x0, y0=y0, + pixel_size=self._pixel_size) + else: + self.P = tile_pyramid.ImagePyramid(self._tiledir, self._base_tile_size, x0=x0, y0=y0, pixel_size=self._pixel_size) - pointScanner.PointScanner.start(self) + if self._save_raw: + eventLog.register_event_handler(self.storage.event_logger) - def tick(self, frameData, **kwargs): + + with self.scope.frameWrangler.spooling_stopped(): + if self._trigger: + self.scope.cam.SetAcquisitionMode(self.scope.cam.MODE_SOFTWARE_TRIGGER) + + self._scanner.init_scan() + self.scope.frameWrangler.onFrame.connect(self.on_frame, dispatch_uid=self._uuid) + + self.dtStart = datetime.datetime.now() + if self._save_raw: + self.storage.initialise() + + self.frame_num = 0 + + def on_frame(self, frameData, **kwargs): pos = self.scope.GetPos() - pointScanner.PointScanner.tick(self, frameData, **kwargs) + + with self.scope.frameWrangler.spooling_paused(): + #pointScanner.PointScanner.on_frame(self, frameData, **kwargs) + finished = self._scanner.next_pos() d = frameData.astype('f').squeeze() - if not self.background is None: - d = d - self.background + if not self._background is None: + d = d - self._background if not self._flat is None: d = d * self._flat x_i = np.round(((pos['x'] - self._x0)/self._pixel_size)).astype('i') y_i = np.round(((pos['y'] - self._y0) / self._pixel_size)).astype('i') - print(pos['x'], pos['y'], x_i, y_i, d.min(), d.max()) + #print(pos['x'], pos['y'], x_i, y_i, d.min(), d.max()) + + self.P.update_base_tiles_from_frame(x_i, y_i, d) + + if self._save_raw: + self.storage.store_frame(self.frame_num, frameData) + + self.frame_num += 1 - self.P.add_base_tile(x_i, y_i, self._weights*d, self._weights) + if self._scanner.running: + t = time.time() + if t > (self._last_update_time + 1): + self._last_update_time = t + self.on_progress.send(self) + + else: + self._finalise() + + + def _finalise(self): + # this does the finalisation steps that need to be done after the scan is complete. + # It is separate from _stop(), as _stop() is called by PointScanner.on_frame() **before** the final frame + # has been saved. We want to have the final frame saved before we do the finalisation steps. + + # disconnect the frame handler + try: + self.scope.frameWrangler.onFrame.disconnect(self.on_frame, dispatch_uid=self._uuid) + except: + logger.exception('Could not disconnect pointScanner tick from frameWrangler.onFrame') + + with self.scope.frameWrangler.spooling_stopped(): + if self._return_to_start: + self._scanner.return_home() + + self.scope.turnAllLasersOff() + + if self._trigger: + self.scope.cam.SetAcquisitionMode(self.scope.cam.MODE_CONTINUOUS) + - t = time.time() - if t > (self._last_update_time + 1): - self._last_update_time = t - self.progress.send(self) + self.on_progress.send(self) + t_ = time.time() + + if self._save_raw: + try: + eventLog.remove_event_handler(self.storage.event_logger) + except ValueError: + pass + + self.storage.mdh.update(self.mdh) + self.storage.finalise() - def _stop(self): - pointScanner.PointScanner._stop(self) + logger.info('Finished tile acquisition') + if self._backend_type == 'cluster': + logger.info('Waiting for spoolers to empty and for base levels to be built') + self.P.finish_base_tiles() + if self._backend_type == 'cluster': + logger.info('Base tiles built') + + logger.info('Completing pyramid (dt = %3.2f)' % (time.time()-t_)) self.P.update_pyramid() - with open(os.path.join(self._tiledir, 'metadata.json'), 'w') as f: - f.write(self.P.mdh.to_JSON()) + if self._backend_type == 'cluster': + from PYME.IO import clusterIO + clusterIO.put_file(self.P.base_dir + '/metadata.json', self.P.mdh.to_JSON().encode()) + else: + with open(os.path.join(self._tiledir, 'metadata.json'), 'w') as f: + f.write(self.P.mdh.to_JSON()) + + logger.info('Pyramid complete (dt = %3.2f)' % (time.time()-t_)) + + self.spool_complete = True self.on_stop.send(self) - self.progress.send(self) + self.on_progress.send(self) -class CircularTiler(Tiler, pointScanner.CircularPointScanner): - def __init__(self, scope, tile_dir, max_radius_um=100, tile_spacing=None, dwelltime=1, background=0, evtLog=False, - trigger=False, base_tile_size=256, return_to_start=True): - """ - :param return_to_start: bool - Flag to toggle returning home at the end of the scan. False leaves scope position as-is on scan completion. - """ - if tile_spacing is None: - fs = np.array(scope.frameWrangler.currentFrame.shape[:2]) - # calculate tile spacing such that there is ~30% overlap. - tile_spacing = (1/np.sqrt(2)) * fs * np.array(scope.GetPixelSize()) - # take the pixel size to be the same or at least similar in both directions - pixel_radius = int(max_radius_um / tile_spacing.mean()) - logger.debug('Circular tiler target radius in units of (overlapped) FOVs: %d' % pixel_radius) - - pointScanner.CircularPointScanner.__init__(self, scope, pixel_radius, - tile_spacing, dwelltime, background, - False, evtLog, trigger=trigger, - stop_on_complete=True, - return_to_start=return_to_start) + def stop(self): + self._scanner.stop() + self._finalise() # TODO - abort?? (main differences are return home, and logging messages) ) + + def md(self): + return self.mdh + + def _launch_viewer(self): + # TODO - move to a UI module + import subprocess + import sys + import webbrowser + import time + import requests + import os + + # abs path the tile dir + tiledir = self._tiledir + if not os.path.isabs(tiledir): + # TODO - should we be doing the `.isabs()` check on the parent directory instead? + from PYME.IO.FileUtils import nameUtils + tiledir = nameUtils.getFullFilename(tiledir) - self._tiledir = tile_dir - self._base_tile_size = base_tile_size - self._flat = None #currently not used + try: # if we already have a tileviewer serving, change the directory + requests.get('http://127.0.0.1:8979/set_tile_source?tile_dir=%s' % tiledir) + except requests.ConnectionError: # start a new process + try: + pargs = {'creationflags': subprocess.CREATE_NEW_CONSOLE} + except AttributeError: # not on windows + pargs = {'shell': True} + + self._gui_proc = subprocess.Popen('%s -m PYME.tileviewer.tileviewer %s' % (sys.executable, tiledir), **pargs) + time.sleep(3) + + webbrowser.open('http://127.0.0.1:8979/') + + +def Tiler(*args, **kwargs): + logger.warning('Tiler is deprecated, please use TileAcquisition') + return TileAcquisition(*args, **kwargs) + + +# class CircularTiler(Tiler, pointScanner.CircularPointScanner): +# def __init__(self, scope, tile_dir, max_radius_um=100, tile_spacing=None, dwelltime=1, background=0, evtLog=False, +# trigger=False, base_tile_size=256, return_to_start=True): +# """ +# :param return_to_start: bool +# Flag to toggle returning home at the end of the scan. False leaves scope position as-is on scan completion. +# """ +# if tile_spacing is None: +# fs = np.array(scope.frameWrangler.currentFrame.shape[:2]) +# # calculate tile spacing such that there is ~30% overlap. +# tile_spacing = (1/np.sqrt(2)) * fs * np.array(scope.GetPixelSize()) +# # take the pixel size to be the same or at least similar in both directions +# pixel_radius = int(max_radius_um / tile_spacing.mean()) +# logger.debug('Circular tiler target radius in units of (overlapped) FOVs: %d' % pixel_radius) + +# pointScanner.CircularPointScanner.__init__(self, scope, pixel_radius, +# tile_spacing, dwelltime, background, +# False, evtLog, trigger=trigger, +# stop_on_complete=True, +# return_to_start=return_to_start) - self._last_update_time = 0 +# self._tiledir = tile_dir +# self._base_tile_size = base_tile_size +# self._flat = None #currently not used + +# self._last_update_time = 0 - self.on_stop = dispatch.Signal() - self.progress = dispatch.Signal() +# self.on_stop = dispatch.Signal() +# self.on_progress = dispatch.Signal() class MultiwellCircularTiler(object): diff --git a/PYME/Acquire/__init__.py b/PYME/Acquire/__init__.py index 6277005d1..ae514193b 100755 --- a/PYME/Acquire/__init__.py +++ b/PYME/Acquire/__init__.py @@ -31,8 +31,8 @@ - :py:mod:`PYME.Acquire.microscope` : a handler / collection point for all the hardware and microscope state - :py:mod:`PYME.Acquire.frameWrangler` : controls the flow of data from the camera(s) -Most of the additional modules serve a supporting role. Of special note are the spoolers (:py:mod:`PYME.Acquire.HDFSpooler`, -:py:mod:`PYME.Acquire.QueueSpooler`, and :py:mod:`PYME.Acquire.HTTPSpooler`) which are the backends for spooling data acquisition. +Most of the additional modules serve a supporting role. Of special note are the spoolers (:py:mod:`PYME.IO.HDFSpooler`, +:py:mod:`PYME.IO.QueueSpooler`, and :py:mod:`PYME.IO.HTTPSpooler`) which are the backends for spooling data acquisition. Drivers for different pieces of experimental hardware are found in :py:mod:`PYME.Acquire.Hardware` diff --git a/PYME/Acquire/acquire_app.py b/PYME/Acquire/acquire_app.py new file mode 100644 index 000000000..275f24365 --- /dev/null +++ b/PYME/Acquire/acquire_app.py @@ -0,0 +1,14 @@ +"""PYME Acquire application entry point script.""" + +from PYME.util.uilaunch import ensure_macos_framework_build + +def main(): + # make sure we have a framework build on macOS so that ui elements work properly + # run this before importing any ui elements (or anything that might take time to import) + ensure_macos_framework_build() + + from PYME.Acquire import PYMEAcquire + PYMEAcquire.main() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/PYME/Acquire/acquire_client.py b/PYME/Acquire/acquire_client.py new file mode 100644 index 000000000..f4ea86aa2 --- /dev/null +++ b/PYME/Acquire/acquire_client.py @@ -0,0 +1,90 @@ +import requests + + +class AcquireClient(object): + """ + Client for the Acquire server. Offers a pythonic interface to the PYMEAcquire REST API. + """ + + def __init__(self, url='127.0.0.1', port=8999): + self.url = url + self.port = port + self.base_url = 'http://{}:{}'.format(self.url, self.port) + self._state = None + + + def _poll_state(self): + """ + Polls the state of the server. Uses the long-polling endpoint. + """ + while True: + self._state = requests.get(self.base_url + '/scope_state_longpoll').json() + + def _start_polling(self): + """ + Starts polling the server state. + """ + import threading + t = threading.Thread(target=self._poll_state) + t.daemon = True + t.start() + + def _check_response(self, response): + """ + Handles the response from the server. Raises an exception if the response is not + successful. + """ + if not response.ok: + raise Exception('Error: {}'.format(response.text)) + + return response + + @property + def state(self): + """ + Returns the current state of the server (as given by long-polling). Starts polling + the first time it is called. + """ + if self._state is None: + self._state = self._get_scope_state() # get initiial state + self._start_polling() + + return self._state + + + def _get_scope_state(self): + """ + Returns the current state of the scope as a dictionary. + """ + return self._check_response(requests.get(self.base_url + '/get_scope_state')).json() + + def update_scope_state(self, state:dict): + """ + Updates the scope state with the provided dictionary. + """ + self._check_response(requests.post(self.base_url + '/update_scope_state', json=state)) + + def start_spooling(self, filename='', preflight_mode='abort', settings={}): + """ + Starts spooling images to disk. If filename is not provided, the default filename will be used. + """ + + self._check_response(requests.post(self.base_url + f'/spool_controller/start_spooling?filename={filename}&preflight_mode={preflight_mode}', json=settings)) + + return self.spooling_finished + + def spooling_info(self): + """ + Returns information about the current spooling. + + TODO - Use the long-polling endpoint??? + """ + return self._check_response(requests.get(self.base_url + '/spool_controller/info')).json() + + def spooling_finished(self): + """ + Returns True if the spooling is finished, False otherwise. + """ + return self.spooling_info()['status']['spool_complete'] + + \ No newline at end of file diff --git a/PYME/Acquire/acquire_server.py b/PYME/Acquire/acquire_server.py index 0cbae8784..f4b3f8fff 100755 --- a/PYME/Acquire/acquire_server.py +++ b/PYME/Acquire/acquire_server.py @@ -26,124 +26,21 @@ #import matplotlib #import before we start logging to avoid log messages in debug import os -import time import logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) -from PYME.Acquire import microscope -from PYME.Acquire import protocol - -from PYME.IO import MetaDataHandler -import six - from PYME.util import webframework import threading -from PYME.Acquire import event_loop - -class PYMEAcquireServer(event_loop.EventLoop): - def __init__(self, options = None): - event_loop.EventLoop.__init__(self) - self.options = options - - self.snapNum = 0 - - self.MainFrame = self #reference to this window for use in scripts etc... - protocol.MainFrame = self - - self.initDone = False - self.postInit = [] #for protocol compat - - self.scope = microscope.microscope() - - self.roi_on = False - self.bin_on = False - - #functions to call in each polling iteration - # Replaces time1 in GUI version - self._want_loop_notification = [] - - self._is_running = False - - # variables to facilitate long-polling for frame updates - self._current_frame = None - self._new_frame_condition = threading.Condition() - - self._state_valid = False - self._state_updated_condition = threading.Condition() - - self.scope.state.stateChanged.connect(self._on_scope_state_change) - - - - - def main_loop(self): - """ - Infinite loop which polls hardware - - Returns - ------- - - """ - #self._is_running = True +#from PYME.Acquire import webui - logger.debug('Starting initialisation') - self._initialize_hardware() - #poll to see if the init script has run - self._wait_for_init_complete() - - - logger.debug('Starting post-init') - - if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)): - self._start_polling_camera() - - self._want_loop_notification.append(self.scope.actions.Tick) - self.initDone = True +from PYME.Acquire.acquirebase import PYMEAcquireBase - logger.debug('Finished post-init') - - try: - logger.debug('Starting event loop') - self.loop_forever() - except: - logger.exception('Exception in event loop') - finally: - logger.debug('Shutting down') - self._shutdown() - - - def _initialize_hardware(self): - """ - Launch microscope hardware initialization and start polling for completion - - """ - #this spawns a new thread to run the initialization script - self.scope.initialize(self.options.initFile, self.__dict__) - - logger.debug('Init run, waiting on background threads') - - def _wait_for_init_complete(self): - self.scope.wait_for_init() - logger.debug('Backround initialization done') - - def _on_frame_group(self, *args, **kwargs): - #logger.debug('_on_frame_group') - with self._new_frame_condition: - self._current_frame = self.scope.frameWrangler.currentFrame - #logger.debug(repr(self.scope.frameWrangler.currentFrame)) - self._new_frame_condition.notify() - - def _on_scope_state_change(self, *args, **kwargs): - with self._state_updated_condition: - self._state_valid = False - self._state_updated_condition.notify() - - def _start_polling_camera(self): - self.scope.startFrameWrangler(event_loop=self) - self.scope.frameWrangler.onFrameGroup.connect(self._on_frame_group) +class PYMEAcquireServerMixin(object): + def __init__(self, *args, **kwargs): + pass @webframework.register_endpoint('/get_frame_pzf', mimetype='image/pzf') def get_frame_pzf(self): @@ -285,54 +182,26 @@ def update_scope_state(self, body=''): return 'OK' #TODO - check for errors - def OnMCamSetPixelSize(self, event): - from PYME.Acquire.ui import voxelSizeDialog - - dlg = voxelSizeDialog.VoxelSizeDialog(self, self.scope) - dlg.ShowModal() - - - - def _shutdown(self): - self.scope.frameWrangler.stop() - - if 'cameras' in dir(self.scope): - for c in self.scope.cameras.values(): - c.Shutdown() - else: - self.scope.cam.Shutdown() - - for f in self.scope.CleanupFunctions: - f() - - logger.info('All cleanup functions called') - - time.sleep(1) - - import threading - msg = 'Remaining Threads:\n' - for t in threading.enumerate(): - if six.PY3: - msg += '%s, %s\n' % (t.name, t._target) - else: - msg += '%s, %s\n' % (t, t._Thread__target) - - logger.info(msg) - from PYME.Acquire import webui from PYME.Acquire import SpoolController -class AcquireHTTPServer(webframework.APIHTTPServer, PYMEAcquireServer): - def __init__(self, options, port, bind_addr=''): - PYMEAcquireServer.__init__(self, options) +class AcquireHTTPServerMixin(webframework.APIHTTPServer, PYMEAcquireServerMixin): + def __init__(self, port, bind_addr=None): + PYMEAcquireServerMixin.__init__(self) + + if bind_addr is None: + bind_addr = 'localhost' # bind to localhost by default in an attempt to make this safer server_address = (bind_addr, port) webframework.APIHTTPServer.__init__(self, server_address) self.daemon_threads = True self.add_endpoints(SpoolController.SpoolControllerWrapper(self.scope.spoolController), '/spool_controller') + #self.add_endpoints(self.scope.stackSettings, '/stack_settings') self.add_static_handler('static', webframework.StaticFileHandler(os.path.join(os.path.dirname(__file__), 'webui', 'static'))) + webui.set_server(self) + self._main_page = webui.load_template('PYMEAcquire.html') @webframework.register_endpoint('/do_login') @@ -376,12 +245,18 @@ def run(self): try: self.serve_forever() finally: - self.stop() + self.evt_loop.stop() #logger.info('Shutting down ...') #self.distributor.shutdown() logger.info('Closing server ...') self.server_close() +class AcquireHTTPServer(PYMEAcquireBase, AcquireHTTPServerMixin): + """Server without wx GUI""" + def __init__(self, options, port, bind_addr=None, evt_loop=None): + PYMEAcquireBase.__init__(self, options, evt_loop=evt_loop) + AcquireHTTPServerMixin.__init__(self, port, bind_addr) + def main(): import os @@ -397,6 +272,7 @@ def main(): parser.add_option("-i", "--init-file", dest="initFile", help="Read initialisation from file [defaults to init.py]", metavar="FILE", default='init.py') + parser.add_option('-b', '--reuse-browser', dest="browser", default=True, action="store_false") (options, args) = parser.parse_args() @@ -414,12 +290,13 @@ def main(): logger.info('using initialization script %s' % init_file) server = AcquireHTTPServer(options, 8999) - ns = dict(scope=server.scope) + ns = dict(scope=server.scope, server=server) print('namespace:', ns) ipy.launch_ipy_server_thread(user_ns=ns) - import webbrowser - webbrowser.open('http://localhost:8999') #FIXME - delay this until server is up + if options.browser: + import webbrowser + webbrowser.open('http://localhost:8999') #FIXME - delay this until server is up server.run() diff --git a/PYME/Acquire/acquirebase.py b/PYME/Acquire/acquirebase.py new file mode 100644 index 000000000..37a1dbd16 --- /dev/null +++ b/PYME/Acquire/acquirebase.py @@ -0,0 +1,139 @@ +''' Base class for both wx and HTML based acquisition interfaces''' +import threading +import time + +from PYME.Acquire import microscope +from PYME.Acquire import protocol +from PYME.Acquire import event_loop + +import logging +logger = logging.getLogger(__name__) + +class PYMEAcquireBase(object): + def __init__(self, options=None, evt_loop=None) -> None: + if evt_loop is None: + self.evt_loop = event_loop.EventLoop() + else: + self.evt_loop = evt_loop + + self.options = options + + self.snapNum = 0 + + self.MainFrame = self #reference to this window for use in scripts etc... + protocol.MainFrame = self + + self.initDone = False + self.postInit = [] #for protocol compat + + self.scope = microscope.Microscope() + + self.roi_on = False + self.bin_on = False + + # non-GUI timer (replaces time1 for non-GUI sceduled events) + self._timer0 = self.evt_loop.MultiTargetTimer() + self._timer0.start(50) + + self._is_running = False + + # variables to facilitate long-polling for frame updates + self._current_frame = None + self._new_frame_condition = threading.Condition() + + self._state_valid = False + self._state_updated_condition = threading.Condition() + + self.scope.state.stateChanged.connect(self._on_scope_state_change) + + def main_loop(self): + """ + Infinite loop which polls hardware + + Returns + ------- + + """ + #self._is_running = True + + logger.debug('Starting initialisation') + self._initialize_hardware() + #poll to see if the init script has run + self._wait_for_init_complete() + + + logger.debug('Starting post-init') + + self.non_gui_post_init() + + logger.debug('Finished post-init') + + try: + logger.debug('Starting event loop') + self.evt_loop.loop_forever() + except: + logger.exception('Exception in event loop') + finally: + logger.debug('Shutting down') + self._shutdown() + + def non_gui_post_init(self): + if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)): + self._start_polling_camera() + + self._timer0.register_callback(self.scope.actions.Tick) + self.initDone = True + + + def _initialize_hardware(self): + """ + Launch microscope hardware initialization and start polling for completion + + """ + #this spawns a new thread to run the initialization script + self.scope.initialize(self.options.initFile, self.__dict__) + + logger.debug('Init run, waiting on background threads') + + def _wait_for_init_complete(self): + self.scope.wait_for_init() + logger.debug('Backround initialization done') + + def _on_frame_group(self, *args, **kwargs): + #logger.debug('_on_frame_group') + with self._new_frame_condition: + self._current_frame = self.scope.frameWrangler.currentFrame + #logger.debug(repr(self.scope.frameWrangler.currentFrame)) + self._new_frame_condition.notify() + + def _on_scope_state_change(self, *args, **kwargs): + with self._state_updated_condition: + self._state_valid = False + self._state_updated_condition.notify() + + def _start_polling_camera(self): + self.scope.startFrameWrangler(event_loop=self.evt_loop) + self.scope.frameWrangler.onFrameGroup.connect(self._on_frame_group) + + def _shutdown(self): + self.scope.frameWrangler.stop() + + if 'cameras' in dir(self.scope): + for c in self.scope.cameras.values(): + c.Shutdown() + else: + self.scope.cam.Shutdown() + + for f in self.scope.CleanupFunctions: + f() + + logger.info('All cleanup functions called') + + time.sleep(1) + + import threading + msg = 'Remaining Threads:\n' + for t in threading.enumerate(): + msg += '%s, %s\n' % (t.name, t._target) + + logger.info(msg) \ No newline at end of file diff --git a/PYME/Acquire/acquiremainframe.py b/PYME/Acquire/acquiremainframe.py index 7c7d4c75f..942695679 100755 --- a/PYME/Acquire/acquiremainframe.py +++ b/PYME/Acquire/acquiremainframe.py @@ -31,7 +31,7 @@ import PYME.ui.manualFoldPanel as afp import wx import wx.lib.agw.aui as aui -import wx.py.shell +import PYME.ui.shell logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) @@ -47,7 +47,7 @@ from PYME.Acquire.ui import seqdialog from PYME.Acquire.ui import selectCameraPanel from PYME.Acquire.ui import splashScreen -from PYME.Acquire.ui import HDFSpoolFrame +from PYME.Acquire.ui import spool_panel from PYME.Acquire import microscope from PYME.Acquire import protocol @@ -58,11 +58,13 @@ from PYME.ui.AUIFrame import AUIFrame +from PYME.Acquire import acquirebase + def create(parent, options = None): return PYMEMainFrame(parent, options) -class PYMEMainFrame(AUIFrame): +class PYMEMainFrame(acquirebase.PYMEAcquireBase, AUIFrame): def _create_menu(self): #self._menus = {} #self.menubar = wx.MenuBar() @@ -101,31 +103,26 @@ def __init__(self, parent, options = None): parent=parent, pos=wx.Point(20, 20), size=wx.Size(600, 800), style=wx.DEFAULT_FRAME_STYLE, title= getattr(options, 'window_title', 'PYME Acquire')) - self._create_menu() - self.options = options - - self.snapNum = 0 + acquirebase.PYMEAcquireBase.__init__(self, options, evt_loop=mytimer) - self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + self._create_menu() - self.MainFrame = self #reference to this window for use in scripts etc... - protocol.MainFrame = self + self.Bind(wx.EVT_CLOSE, self.OnCloseWindow) + # create list of UI panels to be added after init + # Will be populated buy init script self.toolPanels = [] self.camPanels = [] self.aqPanels = [] self.anPanels = [] - self.postInit = [] - - self.initDone = False - self.scope = microscope.microscope() + self._aq_uis = {} self.splash = splashScreen.SplashScreen(self, self.scope) self.splash.Show() - self.sh = wx.py.shell.Shell(id=-1, + self.sh = PYME.ui.shell.Shell(id=-1, parent=self, size=wx.Size(-1, -1), style=0, locals=self.__dict__, introText='PYMEAcquire - note that help, license, etc. below is for Python, not PYME\n\n') self.AddPage(self.sh, caption='Shell') @@ -133,34 +130,24 @@ def __init__(self, parent, options = None): self.CreateToolPanel(getattr(options, 'gui_mode', 'default')) self.SetSize((1030, 895)) - - self.roi_on = False - self.bin_on = False self.time1 = mytimer.mytimer() - self.time1.Start(50) - - wx.CallAfter(self._initialize_hardware) - - #self.time1.WantNotification.append(self.runInitScript) - self.time1.WantNotification.append(self.splash.Tick) + if self.options.threaded_event_loop: + wx.CallAfter(self.run) + else: + wx.CallAfter(self._initialize_hardware) - def _initialize_hardware(self): - """ - Launch microscope hardware initialization and start polling for completion - - """ - #this spawns a new thread to run the initialization script - self.scope.initialize(self.options.initFile, self.__dict__) - - logger.debug('Init run, waiting on background threads') - #poll to see if the init script has run - self.time1.WantNotification.append(self._check_init_done) + self.time1.register_callback(self._check_init_done) + self.time1.register_callback(self.splash.Tick) - + def run(self): + import threading + self._poll_thread = threading.Thread(target=self.main_loop) + self._poll_thread.start() + def _check_init_done(self): if self.scope.initialized == True and self._check_init_done in self.time1.WantNotification: @@ -173,12 +160,13 @@ def _check_init_done(self): self.time1.Start(500) def _refreshDataStack(self): - if 'vp' in dir(self): - if not (self.vp.do.ds.data is self.scope.frameWrangler.currentFrame): - self.vp.SetDataStack(self.scope.frameWrangler.currentFrame) - - def _start_polling_camera(self): - self.scope.startFrameWrangler() + try: + if not (self.view.do.ds.data is self.scope.frameWrangler.currentFrame): + self.view.SetDataStack(self.scope.frameWrangler.currentFrame) + except AttributeError: + + pass + def _add_live_view(self): """Gets called once during post-init to start pulling data from the @@ -187,28 +175,27 @@ def _add_live_view(self): """ if self.scope.cam.GetPicHeight() > 1: - if 'vp' in dir(self): - self.vp.SetDataStack(self.scope.frameWrangler.currentFrame) - else: - self.vp = arrayViewPanel.ArrayViewPanel(self, self.scope.frameWrangler.currentFrame) - self.vp.crosshairs = False - self.vp.showScaleBar = False - self.vp.do.leftButtonAction = self.vp.do.ACTION_SELECTION - self.vp.do.showSelection = True - self.vp.CenteringHandlers.append(self.scope.PanCamera) + try: + self.view.SetDataStack(self.scope.frameWrangler.currentFrame) + except AttributeError: + self._view = arrayViewPanel.ArrayViewPanel(self, self.scope.frameWrangler.currentFrame, initial_overlays=[]) + self.view.crosshairs = False + self.view.showScaleBar = False + self.view.do.leftButtonAction = self.view.do.ACTION_SELECTION + self.view.do.showSelection = True + self.view.CenteringHandlers.append(self.scope.PanCamera) - self.vsp = disppanel.dispSettingsPanel2(self, self.vp) + self.vsp = disppanel.dispSettingsPanel2(self, self.view, self.scope) - self.time1.WantNotification.append(self.vsp.RefrData) - self.time1.WantNotification.append(self._refreshDataStack) + self.time1.register_callback(self.vsp.RefrData) + self.time1.register_callback(self._refreshDataStack) - self.AddPage(page=self.vp, select=True,caption='Preview') + self.AddPage(page=self.view, select=True,caption='Preview') self.AddCamTool(self.vsp, 'Display') - #self.scope.frameWrangler.WantFrameGroupNotification.append(self.vp.Redraw) - self.scope.frameWrangler.onFrameGroup.connect(self.vp.Redraw) + self.scope.frameWrangler.onFrameGroup.connect(self.view.Redraw) else: #1d data - use graph instead @@ -229,8 +216,8 @@ def _add_live_view(self): def doPostInit(self): logger.debug('Starting post-init') - if self.scope.cam.CamReady():# and ('chaninfo' in self.scope.__dict__)): - self._start_polling_camera() + if not self.options.threaded_event_loop: + self.non_gui_post_init() for cm in self.postInit: logger.debug('Loading GUI component for %s' %cm.name) @@ -239,10 +226,6 @@ def doPostInit(self): except Exception as e: logger.exception('Error whilst initializing %s GUI' % cm.name) - #if len(self.scope.piezos) > 0.5: - # self.piezo_sl = psliders.PiezoSliders(self.scope.piezos, self, self.scope.joystick) - # self.time1.WantNotification.append(self.piezo_sl.update) - logger.debug('Run all custom GUI init tasks') @@ -264,30 +247,50 @@ def doPostInit(self): - self.time1.WantNotification.append(self.StatusBarUpdate) + self.time1.register_callback(self.StatusBarUpdate) for t in self.camPanels: #print(t) self.AddCamTool(*t) if len(self.scope.positioning.keys()) > 0.5: - self.pos_sl = positionUI.PositionPanel(self.scope, self, self.scope.joystick) - self.time1.WantNotification.append(self.pos_sl.update) + self.pos_sl = positionUI.PositionPanelV2(self.scope, self, self.scope.joystick) + self.time1.register_callback(self.pos_sl.update) self.AddTool(self.pos_sl, 'Positioning') - self.seq_d = seqdialog.seqPanel(self, self.scope) - self.AddAqTool(self.seq_d, 'Z-Stack', pinned=False) + self.seq_d = seqdialog.seqPanel(self, self.scope, mode='compact') + self.register_acquisition_ui('ZStackAcquisition', (self.seq_d, 'Z-Stack')) + #self.AddAqTool(self.seq_d, 'Z-Stack', pinned=False) #self.seq_d.Show() + + # TODO - this is done in the init script for now - should this be automatic? + # if 'x' in self.scope.positioning.keys() and 'y' in self.scope.positioning.keys(): + # # we can tile, register the tiling UI + # from PYME.Acquire.ui import tilesettingsui + # self.tileUI = tilesettingsui.TileSettingsUI(self, self.scope) + # self.register_acquisition_ui('TileAcquisition', (self.tileUI, 'Tile Settings')) for t in self.toolPanels: #print(t) self.AddTool(*t) if self.scope.cam.CamReady(): - self.pan_spool = HDFSpoolFrame.PanSpool(self, self.scope) - self.AddAqTool(self.pan_spool, 'Time/Blinking series', pinned=False, folded=False) + self.pan_protocol = spool_panel.ProtocolAcquisitionPane(self, self.scope) + self.register_acquisition_ui('ProtocolAcquisition', (self.pan_protocol, 'Time/Blinking series')) + #self.AddAqTool(self.pan_protocol, 'Time/Blinking series', pinned=False, folded=False) + + # for t in self.scope.spoolController.acquisition_types.keys(): + # if t in self._aq_uis: + # self.AddAqTool(*self._aq_uis[t], folded=(t != self.scope.spoolController.acquisition_type)) + # else: + # logger.warning('No UI registered for acquisition type %s' % t) + + + self.pan_spool = spool_panel.SpoolingPane(self, self.scope, acquisition_uis=self._aq_uis) + self.AddAqTool(self.pan_spool, 'Acquisition', pinned=True, folded=False) + for t in self.aqPanels: self.AddAqTool(*t) @@ -298,7 +301,7 @@ def doPostInit(self): #self.splash.Destroy() - self.time1.WantNotification.append(self.scope.actions.Tick) + #self.time1.register_callback(self.scope.actions.Tick) self.initDone = True self._mgr.Update() @@ -373,7 +376,7 @@ def CreateToolPanel(self, mode='default'): self.aqPanel = afp.foldPanel(self, -1, wx.DefaultPosition, - wx.Size(240,1000), single_active_pane=True) + wx.Size(240,1000), single_active_pane=True, constrain_children=True) if mode == 'compact': self._mgr.AddPane(self.toolPanel, aui.AuiPaneInfo(). @@ -412,7 +415,7 @@ def AddTool(self, pane, title, pinned=True, folded=True, panel=None): panel.AddPane(pane) else: # a normal wx.Panel / wx.Window - print(panel, title, pinned, folded) + #print(panel, title, pinned, folded) item = afp.foldingPane(panel, -1, caption=title, pinned=pinned, folded=folded) pane.Reparent(item) item.AddNewElement(pane, priority=1) @@ -436,7 +439,7 @@ def AddCamTool(self, pane, title, pinned=True, folded=True): self.AddTool(pane, title, pinned=pinned, panel=self.camPanel, folded=folded) - def AddAqTool(self, pane, title, pinned=True, folded=True): + def AddAqTool(self, pane, title, pinned=False, folded=True): """Adds a pane to the Acquisition section of the GUI Parameters @@ -448,11 +451,17 @@ def AddAqTool(self, pane, title, pinned=True, folded=True): """ self.AddTool(pane, title, pinned=pinned, panel=self.aqPanel, folded=folded) + def register_acquisition_ui(self, acquisition_type, panel): + """Registers a panel as a UI for a particular acquisition type""" + pane, title = panel + #self.aqPanels.append((pane, title)) + pane._aq_type = acquisition_type + self._aq_uis[acquisition_type] = (pane, title) def OnFileOpenStack(self, event): #self.dv = dsviewer.DSViewFrame(self) #self.dv.Show() - im = dsviewer.ImageStack() + im = dsviewer.ImageStack(haveGUI=True) dvf = dsviewer.DSViewFrame(im, parent=self, size=(500, 500)) dvf.SetSize((500,500)) dvf.Show() @@ -517,12 +526,12 @@ def OnMCamBin(self, event): logging.debug('Preparing frame wrangler for new ROI size') self.scope.frameWrangler.Prepare() - self.vp.SetDataStack(self.scope.frameWrangler.currentFrame) + self.view.SetDataStack(self.scope.frameWrangler.currentFrame) logging.debug('Restarting frame wrangler with new ROI') self.scope.frameWrangler.start() - self.vp.Refresh() - self.vp.GetParent().Refresh() + self.view.Refresh() + self.view.GetParent().Refresh() def OnMCamSetPixelSize(self, event): from PYME.Acquire.ui import voxelSizeDialog @@ -535,8 +544,6 @@ def OnMCamRoi(self, event): logging.debug('Stopping frame wrangler to set ROI') self.scope.frameWrangler.stop() - #print (self.scope.vp.selection_begin_x, self.scope.vp.selection_begin_y, self.scope.vp.selection_end_x, self.scope.vp.selection_end_y) - if 'validROIS' in dir(self.scope.cam) and self.scope.cam.ROIsAreFixed(): #special case for cameras with restricted ROIs - eg Neo #print('setting ROI') @@ -560,7 +567,7 @@ def OnMCamRoi(self, event): self.scope.state['Camera.ROI'] = (0,0, self.scope.cam.GetCCDWidth(), self.scope.cam.GetCCDHeight()) self.roi_on = False else: - x1, y1, x2, y2 = self.vp.do.GetSliceSelection() + x1, y1, x2, y2 = self.view.do.GetSliceSelection() #if we're splitting colours/focal planes across the ccd, then only allow symetric ROIs if 'splitting' in dir(self.scope.cam): @@ -595,21 +602,19 @@ def OnMCamRoi(self, event): #self.scope.cam.SetCOC() #self.scope.cam.GetStatus() self.scope.frameWrangler.Prepare() - self.vp.SetDataStack(self.scope.frameWrangler.currentFrame) + self.view.SetDataStack(self.scope.frameWrangler.currentFrame) - self.vp.do.SetSelection((x1,y1,0), (x2,y2,0)) + self.view.do.SetSelection((x1,y1,0), (x2,y2,0)) logging.debug('Restarting frame wrangler with new ROI') self.scope.frameWrangler.start() - self.vp.Refresh() - self.vp.GetParent().Refresh() + self.view.Refresh() + self.view.GetParent().Refresh() #event.Skip() def SetCentredRoi(self, event=None, halfwidth=5): self.scope.frameWrangler.stop() - #print (self.scope.vp.selection_begin_x, self.scope.vp.selection_begin_y, self.scope.vp.selection_end_x, self.scope.vp.selection_end_y) - w = self.scope.cam.GetCCDWidth() h = self.scope.cam.GetCCDHeight() @@ -632,20 +637,41 @@ def SetCentredRoi(self, event=None, halfwidth=5): #self.scope.cam.SetCOC() #self.scope.cam.GetStatus() self.scope.frameWrangler.Prepare() - self.vp.SetDataStack(self.scope.frameWrangler.currentFrame) + self.view.SetDataStack(self.scope.frameWrangler.currentFrame) - self.vp.do.SetSelection((x1,y1,0), (x2,y2,0)) + self.view.do.SetSelection((x1,y1,0), (x2,y2,0)) self.scope.frameWrangler.start() - self.vp.Refresh() - self.vp.GetParent().Refresh() + self.view.Refresh() + self.view.GetParent().Refresh() #event.Skip() def OnMDisplayClearSel(self, event): - self.vp.ResetSelection() + self.view.ResetSelection() #event.Skip() + @property + def view(self): + '''Return the current view panel. This is the panel that is used to display the camera data. + + deprecates .vp + + ''' + try: + return self._view + except AttributeError: + raise AttributeError('View panel not yet created. This usually happens if .view is accessed too early in the initialisation process.') from None + @property + def vp(self): + ''' Backwards compatibility for access to the view panel + + Generates a deprecation warning - use .view instead + ''' + import warnings + warnings.warn('The .vp attribute is deprecated. Use .view instead', DeprecationWarning) + return self.view + def OnCloseWindow(self, event): self.scope.frameWrangler.stop() self.time1.Stop() diff --git a/PYME/Acquire/acquirewx.py b/PYME/Acquire/acquirewx.py new file mode 100644 index 000000000..dcb5870e3 --- /dev/null +++ b/PYME/Acquire/acquirewx.py @@ -0,0 +1,61 @@ +#!/usr/bin/python + +################## +# smimainframe.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## +""" +This contains the bulk of the GUI code for the main window of PYMEAcquire. +""" +import logging + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) + +from PYME.Acquire.acquiremainframe import PYMEMainFrame +from PYME.Acquire.acquire_server import AcquireHTTPServerMixin + +def create(parent, options = None): + return AcquireWxHTTPServer(parent, options, port=int(options.port), bind_addr=options.bind_addr) + +class AcquireWxHTTPServer(PYMEMainFrame, AcquireHTTPServerMixin): + """Server with wx GUI""" + def __init__(self, parent, options, port, bind_addr=None): + assert(options.threaded_event_loop) + + PYMEMainFrame.__init__(self, parent, options) + AcquireHTTPServerMixin.__init__(self, port, bind_addr) + + def run(self): + import threading + PYMEMainFrame.run(self) + + if self.options.server: + # only start the server if requested + self._server_thread = threading.Thread(target=self.serve_forever) + self._server_thread.start() + + if self.options.ipy: + # make this a separate config option as port is hard coded so can't run more than one + # process with this option. Also probably not desirable if you just want progamatic + # remote control (through REST API). + from PYME.Acquire.webui import ipy + ns = dict(scope=self.scope, server=self) + print('namespace:', ns) + ipy.launch_ipy_server_thread(user_ns=ns) diff --git a/PYME/Acquire/acquisition_base.py b/PYME/Acquire/acquisition_base.py new file mode 100644 index 000000000..f68cb3234 --- /dev/null +++ b/PYME/Acquire/acquisition_base.py @@ -0,0 +1,148 @@ +""" +Base class for acquisition modules + +This exists to document the interface which acquisition types should implement in order +to be able to be used with spooling etc ... +""" + +import abc +from PYME.contrib import dispatch + + +class AcquisitionBase(abc.ABC): + _backend=None + + @abc.abstractmethod + def __init__(self, *args, **kwargs): + '''Create an Acquisition object''' + + # A signal to be emitted when progress is made in the acquisition + # this triggers a GUI update, and implementations can choose whether to emit this signal + # on every frame, or to throttle it. + self.on_progress = dispatch.Signal() + + # A signal to be emitted when the acquisition is stopped + # this should always be emitted when the acquisition is stopped, regardless of the reason + self.on_stop = dispatch.Signal() + + # A flag to indicate whether the spool is complete. Ths should be set to True once spooling is finished + # (it is watched by the ActionManager to determine if the acquisition is complete and the next action should be executed) + self.spool_complete = False + + @classmethod + @abc.abstractmethod + def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None): + '''Create an Acquisition object from settings and a backend. + + + Parameters + ---------- + + scope : microscope.Microscope + The microscope object to use for acquisition. Supplied in order to allow hardware control and access to the frameWrangler. + + settings : dict + A dictionary of settings for the acquisition + + backed : class + The class of the backend to use for the acquisition. Generally one of PYME.IO.acquisition_backends + + backend_kwargs : dict + A dictionary of keyword arguments to pass to the backend constructor + + series_name : str + The name of the series to create. series_name should be defined unless the MemoryBackend is used, in which case it is ignored. + + spool_controller : object + The spool controller object to use for the acquisition. Generally scope.spoolController. Currently used by ProtocolAcquisition + to access default protocol settings held by the spoolController, but a bit of a legacy hack. May be removed in the future. + + ''' + pass + + @classmethod + def get_frozen_settings(cls, scope, spool_controller=None): + '''Return a dictionary of settings for the acquisition + + Used to "freeze" the state of the spool_controller and/or other settings objects when queueing + acquisitions for subsequent execution via the ActionManager + ''' + + return {} + + @abc.abstractmethod + def start(self): + '''Start the acquisition + + This will usually record any metadata, connect self.on_frame to a frame source (i.e. scope.frameWrangler.onFrame), and initialise + the backend. It should not block, with the bulk of the acquisition logic taking place in frame source handler. + + ''' + pass + + @abc.abstractmethod + def stop(self): + '''Stop the acquisition + + This should disconnect from the frame souce, flush any remaining buffers, return hardware to starting state (if modified), + do any cleanup, and finalise the backend. + + It is desirable to send a final on_progress signal so that the GUI reflects the actual number of frames spooled etc... . + + Once everything is complete, the on_stop signal should be emitted and the spool_complete flag set. + + ''' + pass + + + def abort(self): + '''Abort the acquisition + + This should stop the acquisition as quickly as practical. In many cases this can just be a call to stop(), + but there is no expectation that buffers will be flushed, remaining protocol steps executed or hardware returned + to a starting state. Especially for cases which involve moving translation stages or other hardware it is desirable + that the abort method not result in further movement of hardware unless to a known safe state (e.g. lasers off). + + Abort should, however, finalise the backend and emit the on_stop signal. + TODO - should abort set spool_complete? + ''' + self.stop() + + @abc.abstractmethod + def md(self): + ''' Return acqusition metadata (a PYME.IO.MetaDataHandler object) + + generally just a short wrapper around the backend metadata + + #TODO - rename this to .mdh, or remove entirely and standardise backend access? + ''' + pass + + @abc.abstractmethod + def on_frame(self, sender, frameData, **kwargs): + '''Frame source handler + + This method should be connected to the frame source signal (i.e. scope.frameWrangler.onFrame) and be responsible for + handling incoming frames, passing them off to storage and performing anything that needs to be done (e.g. hardware movements + or protocol task handling) before the next frame. NOTE - this method is called when the frame is retrieved by the frame wrangler + - if the camera is running in continuous mode, this may be several frames after the camera has actually acquired the frame. + + ''' + pass + + @property + def storage(self): + '''Return the storage object for the acquisition + + ''' + return self._backend + + @storage.setter + def storage(self, storage): + '''Set the storage object for the acquisition + + ''' + self._backend = storage + + def getURL(self): + return self.storage.getURL() diff --git a/PYME/Acquire/actions.py b/PYME/Acquire/actions.py new file mode 100644 index 000000000..a743596c8 --- /dev/null +++ b/PYME/Acquire/actions.py @@ -0,0 +1,209 @@ +class Action(object): + ''' + Base Action method - over-ride the __call__ function in derived classes + ''' + + def __init__(self, **kwargs): + self.params = kwargs + + def __call__(self, scope): + pass + + def serialise(self): + '''Convert to a .json serializable dictionary''' + d = dict(self.params) + + then = getattr(self, '_then', None) + if then: + d['then'] = then.serialise() + + return {self.__class__.__name__: d} + + @property + def _repr_then(self): + if self._then is not None: + return '- then -> %s' % repr(self._then) + else: + return '' + + def _estimated_duration(self, scope): + '''Return the estimated duration of the action in seconds''' + return 1.0 + + def estimated_duration(self, scope): + t = self._estimated_duration(scope) + + then = getattr(self, '_then', None) + if then: + t += then.estimated_duration(scope) + + return t + + def finalise(self, scope): + ''' Called after the action has executed''' + # TODO - do then here?? + pass + + + + +class FunctionAction(Action): + '''Legacy action which evals a string. + + Used for handling old -style actions + ''' + + def __init__(self, functionName, args=None): + Action.__init__(self, functionName=functionName, args=args) + self._fcn = functionName + if args is None: + args = {} + self._args = args + + def __call__(self, scope): + fcn = eval('.'.join(['scope', self._fcn])) + #fcn = getattr(scope, self._fcn) + return fcn(**self._args) + + def __repr__(self): + return 'FunctionAction: %s(%s)' % (self._fcn, self._args) + self._repr_then + + +class StateAction(Action): + ''' Base class for actions which modify scope state, with chaining support + + NOTE: we currently do not support chaining off the end of actions (e.g. spooling) which are likely to take some time. + This is because functions such as start_spooling are non-blocking - they return a callback instead. + ''' + + def __init__(self, **kwargs): + self._then = None + Action.__init__(self, **kwargs) + + def then(self, task): + self._then = task + return self + + def _do_then(self, scope): + if self._then is not None: + return self._then(scope) + + +class UpdateState(StateAction): + def __init__(self, state): + self._state = state + StateAction.__init__(self, state=state) + + def __call__(self, scope): + scope.state.update(self._state) + return self._do_then(scope) + + def __repr__(self): + return 'UpdateState: %s' % self._state + self._repr_then + +class MoveTo(StateAction): + """ + Move to a specific position in absolute stage coordinates. + + Most useful when queueing actions to return to a specific + already identified position. + """ + def __init__(self, x, y): + StateAction.__init__(self, x=x, y=y) + self.x, self.y = x, y + + def __call__(self, scope): + scope.SetPos(x=self.x, y=self.y) + return self._do_then(scope) + + def __repr__(self): + return 'MoveTo: %f, %f (x, y)' % (self.x, self.y) + self._repr_then + +class CentreROIOn(StateAction): + """ + Centre the ROI on a specific position in absolute stage coordinates. + + Most useful when queueing actions where target have been automatically + identified. + """ + def __init__(self, x, y): + StateAction.__init__(self, x=x, y=y) + self.x, self.y = x, y + + def __call__(self, scope): + scope.centre_roi_on(self.x, self.y) + return self._do_then(scope) + + def __repr__(self): + return 'CentreROIOn: %f, %f (x, y)' % (self.x, self.y) + self._repr_then + + +class SpoolSeries(Action): + def __init__(self, **kwargs): + self._args = kwargs + Action.__init__(self, **kwargs) + + def __call__(self, scope): + return scope.spoolController.start_spooling(**self._args) + + def __repr__(self): + return 'SpoolSeries(%s)' % ', '.join(['%s = %s' % (k,repr(v)) for k, v in self._args.items()]) + + def _estimated_duration(self, scope): + return scope.spoolController.estimate_spool_time(**self._args) + +class RemoteSpoolSeries(Action): + def __init__(self, remote_instance='remote_acquire_instance', **kwargs): + self._remote_instance = remote_instance + self._args = kwargs + Action.__init__(self, **kwargs) + + def __call__(self, scope): + # let the local spoolController handle the filename incrementing + return getattr(scope,self._remote_instance).start_spooling(filename=scope.spoolController.seriesName, **self._args) + + def __repr__(self): + return 'RemoteSpoolSeries(%s)' % ', '.join(['%s = %s' % (k,repr(v)) for k, v in self._args.items()]) + + def _estimated_duration(self, scope): + return 30*60 # 30 minutes + + def finalise(self, scope): + super().finalise() + scope.spoolController.SpoolStopped() #make sure file name increments + +class SimultaeneousSpoolSeries(Action): + '''Spool local and remote series simultaneously''' + def __init__(self, remote_instance='remote_acquire_instance', local_settings = {}, remote_settings = {}, **kwargs): + self._remote_instance = remote_instance + self._local_settings = local_settings + self._remote_settings = remote_settings + self._args = kwargs + Action.__init__(self, **kwargs) + + def __call__(self, scope): + a = scope.spoolController.start_spooling(settings = self._local_settings, **self._args) + b = getattr(scope,self._remote_instance).start_spooling(filename=scope.spoolController.seriesName + '_B', settings = self._remote_settings, **self._args) + + return lambda : a() and b() + + def __repr__(self): + return 'SimultaeneousSpoolSeries(%s)' % ', '.join(['%s = %s' % (k,repr(v)) for k, v in self._args.items()]) + + def _estimated_duration(self, scope): + return scope.spoolController.estimate_spool_time(**self._args) + + + +def action_from_dict(serialised): + assert (len(serialised) == 1) + act, params = list(serialised.items())[0] + + then = params.pop('then', None) + # TODO - use a slightly less broad dictionary for action lookup (or move actions to a separate module) + a = globals()[act](**params) + + if then: + a.then(action_from_dict(then)) + + return a diff --git a/PYME/Acquire/eventLog.py b/PYME/Acquire/eventLog.py index fb975d3d0..0af750024 100755 --- a/PYME/Acquire/eventLog.py +++ b/PYME/Acquire/eventLog.py @@ -51,6 +51,28 @@ WantEventNotification = [] +def register_event_handler(EventReceiver): + """ + Register an event handler to receive events. + + Parameters + ---------- + EventReceiver : object (PYME.IO.events.EventLogger or subclass thereof) + an object with a `logEvent(eventName, eventDescr = '', timestamp=None)` method + """ + WantEventNotification.append(EventReceiver) + +def remove_event_handler(EventReceiver): + """ + Remove an event handler from the list of event receivers. + + Parameters + ---------- + EventReceiver : object (PYME.IO.events.EventLogger or subclass thereof) + an object with a `logEvent(eventName, eventDescr = '', timestamp=None)` method + """ + WantEventNotification.remove(EventReceiver) + def logEvent(eventName, eventDescr = '', timestamp=None): """ Log an event to all event receivers. diff --git a/PYME/Acquire/event_loop.py b/PYME/Acquire/event_loop.py index 0537436dd..73561fa1f 100644 --- a/PYME/Acquire/event_loop.py +++ b/PYME/Acquire/event_loop.py @@ -17,7 +17,7 @@ class _Timer(object): def __init__(self): self._next_trigger = sys.float_info.max - self._single_shot = True + self._single_shot = False self._delay_s = -1 logger.debug('Created timer') @@ -36,8 +36,11 @@ def check(self, t): finally: if not self._single_shot: self._next_trigger = t + self._delay_s + return True + else: + return False - def start(self, delay_ms, single_shot=True): + def start(self, delay_ms, single_shot=False): self._single_shot = single_shot self._delay_s = 0.001*delay_ms self._next_trigger = time.time() + self._delay_s @@ -89,8 +92,13 @@ def loop_forever(self): except(queue.Empty): #do timer stuff t = time.time() + did_stuff = False for tm in self._timers: - tm.check(t) + did_stuff = did_stuff or tm.check(t) + + if not did_stuff: + # sleep a bit (to limit CPU usage) + time.sleep(0.001) def stop(self): self._loop_active = False diff --git a/PYME/Acquire/frameWrangler.py b/PYME/Acquire/frameWrangler.py index b7eb25e11..69ee642c3 100644 --- a/PYME/Acquire/frameWrangler.py +++ b/PYME/Acquire/frameWrangler.py @@ -53,6 +53,8 @@ import threading #sfrom PYME.ui import mytimer +from contextlib import contextmanager + class FrameWrangler(object): """ Grabs frames from the camera buffers @@ -111,6 +113,7 @@ def __init__(self, _cam, _ds = None, event_loop=None): self.tl=0 self.inNotify = False + self._notify_lock = threading.Lock() #Signals ########################## @@ -122,7 +125,7 @@ def __init__(self, _cam, _ds = None, event_loop=None): self.onFrameGroup = dispatch.Signal() #called on each new frame group (once per polling interval) - use for updateing GUIs etc. self.onStop = dispatch.Signal() self.onStart = dispatch.Signal() - + # should the thread which polls the camera still be running? self._poll_camera = True @@ -137,6 +140,7 @@ def __del__(self): def destroy(self): self._poll_camera = False + def Prepare(self, keepds=True): @@ -172,7 +176,7 @@ def Prepare(self, keepds=True): if not keepds: self.currentFrame = np.zeros([self.cam.GetPicWidth(), self.cam.GetPicHeight(), - 1], dtype = 'uint16', order = self.order) + 1], dtype =self.cam.dtype, order = self.order) self._cf = self.currentFrame @@ -183,7 +187,7 @@ def getFrame(self, colours=None): #logger.debug('acquire _current_frame_lock in getFrame()') with self._current_frame_lock: self._cf = np.empty([1, self.cam.GetPicWidth(), self.cam.GetPicHeight(), - ], dtype = 'uint16', order = self.order) + ], dtype =self.cam.dtype, order = self.order) if getattr(self.cam, 'numpy_frames', False): cs = self._cf[0,:,:] #self.currentFrame[:,:,0] @@ -271,13 +275,17 @@ def _poll_loop(self): bufferOverflowing = False if bufferOverflowing: - self.bufferOverflowed = True - print('Warning: Camera buffer overflowing - purging buffer') - eventLog.logEvent('Camera Buffer Overflow') - #stop the aquisition - we're going to restart after we're read out to purge the buffer - #doing it this way _should_ stop the black frames which I guess are being caused by the reading the frame which is - #currently being written to - self._event_loop.call_in_main_thread(self.cam.StopAq) + with self._notify_lock: + #acquire lock before flagging the buffer as overflowed so that we can be sure that StopAq + # gets called before StartAq in Notify() + + self.bufferOverflowed = True + logger.warning('Camera buffer overflowing - purging buffer') + eventLog.logEvent('Camera Buffer Overflow') + #stop the aquisition - we're going to restart after we're read out to purge the buffer + #doing it this way _should_ stop the black frames which I guess are being caused by the reading the frame which is + #currently being written to + self._event_loop.call_in_main_thread(self.cam.StopAq) #self.needExposureStart = True self.onExpReady() @@ -298,101 +306,102 @@ def Notify(self, event=None): #check to see if we are already running if self.inNotify: - print('Already in notify, skip for now') + logger.debug('Already in notify, skip for now') return - try: - self.inNotify = True - "Should be called on each timer tick" - self.te = time.clock() - #print self.te - self.tl - self.tl = self.te - - if (not self.cam.CamReady()):# and self.piezoReady())): - # Stop the aquisition if there is a hardware error - self.stop() - return - - if getattr(self.cam, 'hardware_overflowed', False): - self.cam.StopAq() - self.bufferOverflowed = True - - - #is there a picture waiting for us? - #if so do the relevant processing - #otherwise do nothing ... - - #nFrames = 0 #number of frames grabbed this pass - - #bufferOverflowed = False - - # while(self.cam.ExpReady()): #changed to deal with multiple frames being ready - # if 'GetNumImsBuffered' in dir(self.cam): - # bufferOverflowing = self.cam.GetNumImsBuffered() >= (self.cam.GetBufferSize() - 1) - # else: - # bufferOverflowing = False - # if bufferOverflowing: - # bufferOverflowed = True - # print('Warning: Camera buffer overflowing - purging buffer') - # eventLog.logEvent('Camera Buffer Overflow') - # #stop the aquisition - we're going to restart after we're read out to purge the buffer - # #doing it this way _should_ stop the black frames which I guess are being caused by the reading the frame which is - # #currently being written to - # self.cam.StopAq() - # #self.needExposureStart = True - # - # self.onExpReady() - # nFrames += 1 - #te= time.clock() + with self._notify_lock: + #lock to prevent _poll_loop setting an overflowed flag while we're in here. + try: + self.inNotify = True + self.te = time.time() + #print self.te - self.tl + self.tl = self.te - #If we can't deal with the data fast enough (e.g. due to file i/o limitations) this can turn into an infinite loop - - #avoid this by bailing out with a warning if nFrames exceeds a certain value. This will probably lead to buffer overflows - #and loss of data, but is arguably better than an unresponsive app. - #This value is (currently) chosen fairly arbitrarily, taking the following facts into account: - #the buffer has enough storage for ~3s when running flat out, - #we're polling at ~5hz, and we should be able to get more frames than would be expected during the polling intervall to - #allow us to catch up following glitches of one form or another, although not too many more. - if ('GetNumImsBuffered' in dir(self.cam)) and (self.n_frames_in_group > self.cam.GetBufferSize()/2): - print(('Warning: not keeping up with camera, giving up with %d frames still in buffer' % self.cam.GetNumImsBuffered())) - - # just copy data to the current frame once per frame group - individual frames don't get copied - # directly calling memcpy is a bit of a cheat, but is significantly faster than the alternatives - with self._current_frame_lock: - memcpy(self.currentFrame.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)), - self._cf.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),self.currentFrame.nbytes) - - - if self.bufferOverflowed: - print('nse') - self.needExposureStart = True - - # See if we need to restart the exposure. This will happen if - # a) we are in single shot mode - # or b) the camera buffer overflowed - if self.needExposureStart and self.checkHardware(): - self.needExposureStart = False - self.bufferOverflowed = False - self.cam.StartExposure() #restart aquisition - this should purge buffer + if (not self.cam.CamReady()):# and self.piezoReady())): + # Stop the aquisition if there is a hardware error + self.stop() + return + + if getattr(self.cam, 'hardware_overflowed', False): + self.cam.StopAq() + self.bufferOverflowed = True + + + #is there a picture waiting for us? + #if so do the relevant processing + #otherwise do nothing ... - - if self.n_frames_in_group > 0: - #we got some frames, record timing info and let any listeners know - self.n_Frames += self.n_frames_in_group + #nFrames = 0 #number of frames grabbed this pass - self.tLastFrame = self.tThisFrame - self.nFrames = self.n_frames_in_group - self.n_frames_in_group = 0 - self.tThisFrame = time.clock() + #bufferOverflowed = False + + # while(self.cam.ExpReady()): #changed to deal with multiple frames being ready + # if 'GetNumImsBuffered' in dir(self.cam): + # bufferOverflowing = self.cam.GetNumImsBuffered() >= (self.cam.GetBufferSize() - 1) + # else: + # bufferOverflowing = False + # if bufferOverflowing: + # bufferOverflowed = True + # print('Warning: Camera buffer overflowing - purging buffer') + # eventLog.logEvent('Camera Buffer Overflow') + # #stop the aquisition - we're going to restart after we're read out to purge the buffer + # #doing it this way _should_ stop the black frames which I guess are being caused by the reading the frame which is + # #currently being written to + # self.cam.StopAq() + # #self.needExposureStart = True + # + # self.onExpReady() + # nFrames += 1 + #te= time.clock() + + #If we can't deal with the data fast enough (e.g. due to file i/o limitations) this can turn into an infinite loop - + #avoid this by bailing out with a warning if nFrames exceeds a certain value. This will probably lead to buffer overflows + #and loss of data, but is arguably better than an unresponsive app. + #This value is (currently) chosen fairly arbitrarily, taking the following facts into account: + #the buffer has enough storage for ~3s when running flat out, + #we're polling at ~5hz, and we should be able to get more frames than would be expected during the polling intervall to + #allow us to catch up following glitches of one form or another, although not too many more. + if ('GetNumImsBuffered' in dir(self.cam)) and (self.n_frames_in_group > self.cam.GetBufferSize()/2): + logger.warning(('Not keeping up with camera, giving up with %d frames still in buffer' % self.cam.GetNumImsBuffered())) - self.onFrameGroup.send(self) - - except: - traceback.print_exc() - finally: - self.inNotify = False - - #restart the time so we get called again - self.timer.start(self.tiint) + # just copy data to the current frame once per frame group - individual frames don't get copied + # directly calling memcpy is a bit of a cheat, but is significantly faster than the alternatives + with self._current_frame_lock: + memcpy(self.currentFrame.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)), + self._cf.ctypes.data_as(ctypes.POINTER(ctypes.c_uint8)),self.currentFrame.nbytes) + + + if self.bufferOverflowed: + logger.debug('Setting needStartExposure flag') + self.needExposureStart = True + + # See if we need to restart the exposure. This will happen if + # a) we are in single shot mode + # or b) the camera buffer overflowed + if self.needExposureStart and self.checkHardware(): + self.needExposureStart = False + self.bufferOverflowed = False + self.cam.StartExposure() #restart aquisition - this should purge buffer + + + if self.n_frames_in_group > 0: + #we got some frames, record timing info and let any listeners know + self.n_Frames += self.n_frames_in_group + + self.tLastFrame = self.tThisFrame + self.nFrames = self.n_frames_in_group + self.n_frames_in_group = 0 + self.tThisFrame = time.time() + + self.onFrameGroup.send(self) + + except: + traceback.print_exc() + finally: + self.inNotify = False + + #restart the time so we get called again + self.timer.start(self.tiint, single_shot=True) @property @@ -412,14 +421,13 @@ def checkHardware(self): free-running mode.""" for callback in self.HardwareChecks: if not callback(): - print('Waiting for hardware') + logger.debug('Waiting for hardware') return False return True def stop(self): "Stop sequence aquisition" - self.timer.stop() self.aqOn = False @@ -455,8 +463,58 @@ def start(self, tiint = 100): self.n_Frames = 0 #start our timer, this will call Notify - self.timer.start(self.tiint) + self.timer.start(self.tiint, single_shot=True) return True + + + @contextmanager + def spooling_stopped(self, auto_trigger=True): + """ Context manager to ensure that spooling is stopped while performing a task, + returning to the original state when the task is complete.""" + + was_running = self.aqOn + if was_running: + self.stop() + try: + yield + finally: + if was_running: + self.start() + + if auto_trigger and self.cam.GetAcquisitionMode() == self.cam.MODE_SOFTWARE_TRIGGER: + self.cam.FireSoftwareTrigger() + + @contextmanager + def spooling_paused(self): + """ Context manager for use within on_frame handlers to ensure that spooling is paused while a task executes. + The behavior depends on the camera acquisition mode: + + - If the camera is in continuous mode, the camera will be stopped and restarted after the task is complete. + - If the camera is in single-shot mode, we do nothing (as we automatically restart when the frame handler completes). + - If the camera is in software-triggered mode, we fire a software trigger on completion. + + TODO - single shot and triggered are currently a bit inconsistent - should we actually be always firing a software + trigger in triggered mode where we would restart in single shot mode? + + TODO - Hardware triggers??? + + TODO - add keyword to disable firing the trigger for timelapse??? + """ + + was_running = self.aqOn + if was_running: + if self.cam.GetAcquisitionMode() == self.cam.MODE_CONTINUOUS: + self.stop() + try: + yield + finally: + if was_running: + cam_mode = self.cam.GetAcquisitionMode() + + if cam_mode == self.cam.MODE_CONTINUOUS: + self.start() + elif cam_mode == self.cam.MODE_SOFTWARE_TRIGGER: + self.cam.FireSoftwareTrigger() def isRunning(self): diff --git a/PYME/Acquire/htsms/__init__.py b/PYME/Acquire/htsms/__init__.py new file mode 100644 index 000000000..1d6ce9ba3 --- /dev/null +++ b/PYME/Acquire/htsms/__init__.py @@ -0,0 +1,8 @@ +""" +Submodule for htsms microscope related components. This contains code that is either very specific to the htsms microscope, +or which should be maintained in it's existing form for continuity whilst the more general platform evolves. + +It also includes some prototype features which are expedient to have available, but where the final implementations are +expected to have a substantially different interface. + +""" \ No newline at end of file diff --git a/PYME/Acquire/htsms/rule_ui.py b/PYME/Acquire/htsms/rule_ui.py new file mode 100644 index 000000000..7cc72dcdd --- /dev/null +++ b/PYME/Acquire/htsms/rule_ui.py @@ -0,0 +1,970 @@ + +import wx +# from PYME.ui import manualFoldPanel +from PYME.cluster.rules import LocalisationRuleFactory as LocalizationRuleFactory +from PYME.cluster.rules import RecipeRuleFactory +from collections import OrderedDict +#import queue +import os +import posixpath +import logging +from PYME.contrib import dispatch, wxPlotPanel +from PYME.recipes.traits import HasTraits, Enum, Float, CStr +import textwrap +import numpy as np +import matplotlib.pyplot as plt +import threading + +logger = logging.getLogger(__name__) + +POST_CHOICES = ['off', 'spool start', 'spool stop'] + +def get_protocol_list(): + """version of PYME.Acquire.protocol.get_protocol_list which uses 'default' + instead of '' and drops all '.py' extensions + """ + from PYME.Acquire.protocol import _get_protocol_dict + protocol_list = ['default', ] + sorted(list(_get_protocol_dict().keys())) + return [os.path.splitext(p)[0] for p in protocol_list] + +class RuleTile(HasTraits): + task_timeout = Float(60 * 10) + rule_timeout = Float(60 * 10) + + def get_params(self): + editable = self.class_editable_traits() + return editable + + @property + def default_view(self): + if wx.GetApp() is None: + return None + from traitsui.api import View, Item + + return View([Item(tn) for tn in self.get_params()], buttons=['OK']) + + def default_traits_view(self): + """ This is the traits stock method to specify the default view""" + return self.default_view + +def get_rule_tile(rule_factory_class): + class _RuleTile(RuleTile, rule_factory_class): + def __init__(self, **kwargs): + RuleTile.__init__(self) + rule_factory_class.__init__(self, **kwargs) + return _RuleTile + + +class RuleChain(HasTraits): + post_on = Enum(POST_CHOICES) + protocol = CStr('') + + def __init__(self, rule_factories=None, *args, **kwargs): + if rule_factories is None: + rule_factories = list() + self.rule_factories = rule_factories + HasTraits.__init__(self, *args, **kwargs) + + +class ProtocolRules(OrderedDict): + """ + Container for associating sets of analysis rules with specific acquisition + protocols + + Notes + ----- + use ordered dict for reproducibility with listctrl displays + """ + def __init__(self, spool_controller, posting_thread_queue_size=5): + """ + Parameters + ---------- + posting_thread_queue_size : int, optional + sets the size of a queue to hold rule posting threads to ensure they + have time to execute, by default 5. .. seealso:: modules :py:mod:`PYME.cluster.rules` + """ + import queue + + OrderedDict.__init__(self) + self.active = True + self._spool_controller = spool_controller + self.posting_thread_queue = queue.Queue(posting_thread_queue_size) + self._updated = dispatch.Signal() + self._updated.connect(self.update) + + self['default'] = RuleChain() + + self._spool_controller.onSpoolStart.connect(self.on_spool_start) + self._spool_controller.on_stop.connect(self.on_spool_stop) + + def on_spool_start(self, **kwargs): + self.on_spool_event('spool start') + + def on_spool_stop(self, **kwargs): + self.on_spool_event('spool stop') + + def on_spool_event(self, event): + """ + pipe input series name into rule chain and post them all + + Parameters + ---------- + kwargs: dict + present here to allow us to call this method through a dispatch.Signal.send + """ + if not self.active: + logger.info('inactive, check "active" to turn on auto analysis') + return + spooler = self._spool_controller.spooler + try: + spooler.getURL + except AttributeError: + logger.exception('Rule-based analysis chaining currently requires spooling to cluster, not to file') + raise + prot_filename = spooler.protocol.filename + prot_filename = '' if prot_filename is None else prot_filename + protocol_name = os.path.splitext(os.path.split(prot_filename)[-1])[0] + logger.info('protocol name : %s' % protocol_name) + + try: + rule_factory_chain = self[protocol_name] + except KeyError: + rule_factory_chain = self['default'] + + if rule_factory_chain.post_on != event: + # not the right trigger for this protocol + return + + if len(rule_factory_chain.rule_factories) == 0: + logger.info('no rules in chain') + return + + # set the context based on the input series + series_uri = spooler.getURL() + spool_dir, series_stub = posixpath.split(series_uri) + series_stub = posixpath.splitext(series_stub)[0] + context = { + 'spool_dir': spool_dir, # do we need this? or typo in rule docs + 'series_stub': series_stub, # do we need this? or typo in rule docs + 'seriesName': series_uri, # Localization + 'inputs': {'input': [series_uri]}, # Recipe + 'output_dir': posixpath.join(spool_dir, 'analysis'), # Recipe + 'spooler': spooler} # SpoolLocalLocalization + + # rule chain is already linked, add context and push + rule = rule_factory_chain.rule_factories[0].get_rule(context=context) + t = threading.Thread(target=rule.push) + t.start() + if self.posting_thread_queue.full(): + self.posting_thread_queue.get_nowait().join() + self.posting_thread_queue.put_nowait(t) + + + def update(self, *args, **kwargs): + for p in self.keys(): + factories = self[p].rule_factories + for ind in range(len(factories) - 1): + factories[ind].chain(factories[ind + 1]) + + +class ProtocolRuleFactoryListCtrl(wx.ListCtrl): + def __init__(self, protocol_rules, wx_parent): + """ + Parameters + ---------- + protocol_rules: dict + acquisition protocols (keys) and their associated rule factory + chains + wx_parent + """ + wx.ListCtrl.__init__(self, wx_parent, style=wx.LC_REPORT | wx.BORDER_SUNKEN | wx.LC_VIRTUAL | wx.LC_VRULES) + + self._protocol_rules = protocol_rules + + self.InsertColumn(0, 'Protocol', width=75) + self.InsertColumn(1, '# Rules', width=50) + self.InsertColumn(2, 'Post', width=75) + + self.update_list() + self._protocol_rules._updated.connect(self.update_list) + + def OnGetItemText(self, item, col): + """ + Note that this is overriding the wxListCtrl method as required for wxLC_VIRTUAL style + + Parameters + ---------- + item : long + wx list item + col : long + column specifier for wxListCtrl + Returns + ------- + str : Returns string of column 'col' for item 'item' + """ + if col == 0: + return list(self._protocol_rules.keys())[item] + if col == 1: + chains = list(self._protocol_rules.values()) + return str(len(chains[item].rule_factories)) + if col == 2: + chains = list(self._protocol_rules.values()) + return chains[item].post_on + + def update_list(self, sender=None, **kwargs): + self.SetItemCount(len(self._protocol_rules.keys())) + self.Update() + self.Refresh() + + def delete_rule_chains(self, indices=None): + selected_indices = self.get_selected_items() if indices is None else indices + + for ind in reversed(sorted(selected_indices)): # delete in reverse order so we can pop without changing indices + if self.GetItemText(ind, col=0) == 'default': + logger.error('Cannot delete the default rule chain') + continue # try to keep people from deleting the default chain + self._protocol_rules.popitem(ind) + self.DeleteItem(ind) + + self._protocol_rules._updated.send(self) + + def get_selected_items(self): + selection = [] + current = -1 + next = 0 + while next != -1: + next = self.get_next_selected(current) + if next != -1: + selection.append(next) + current = next + return selection + + def get_next_selected(self, current): + return self.GetNextItem(current, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) + + +class RulePlotPanel(wxPlotPanel.PlotPanel): + def __init__(self, parent, protocol_rules, **kwargs): + self.protocol_rules = protocol_rules + self.parent = parent + wxPlotPanel.PlotPanel.__init__(self, parent, **kwargs) + self.figure.canvas.mpl_connect('pick_event', self.parent.OnPick) + + def draw(self): + if not self.IsShownOnScreen(): + return + + if not hasattr( self, 'ax' ): + self.ax = self.figure.add_axes([0, 0, 1, 1]) + + self.ax.cla() + + rule_factories = self.parent.rule_chain.rule_factories + if len(rule_factories) < 1: + self.canvas.draw() + return + width = 1 # size of tile to draw + height = 0.5 + nodes_x = np.arange(0, len(rule_factories) * 1.5 * width, 1.5 * width) + nodes_y = np.ones_like(nodes_x) + + + axis_width = self.ax.get_window_extent().width + n_cols = max([1] + nodes_x.tolist()) + pix_per_col = axis_width / n_cols + + font_size = max(6, min(10, 10 * pix_per_col / 100)) + + TW = textwrap.TextWrapper(width=max(int(1.8 * pix_per_col / font_size), 10), + subsequent_indent=' ') + TW2 = textwrap.TextWrapper(width=max(int(1.3 * pix_per_col / font_size), 10), + subsequent_indent=' ') + + cols = {} + + # plot connecting lines + for ind in range(1, len(rule_factories)): + self.ax.plot([nodes_x[ind - 1] + width, nodes_x[ind]], + [nodes_y[ind - 1] + 0.5 * height, + nodes_y[ind] + 0.5 * height], lw=2) + + #plot the boxes and the labels + for ind in range(len(rule_factories)): + # draw a box + s = rule_factories[ind]._type + fc = [.8,.8, 1] + + rect = plt.Rectangle([nodes_x[ind], nodes_y[ind]], width, height, + ec='k', lw=2, fc=fc, picker=True) + rect._data = s + self.ax.add_patch(rect) + + s = TW2.wrap(s) + if len(s) == 1: + self.ax.text(nodes_x[ind] + .05, nodes_y[ind] + .18 , s[0], size=font_size, weight='bold') + else: + self.ax.text(nodes_x[ind] + .05, nodes_y[ind] + .18 - .05*(len(s) - 1) , '\n'.join(s), size=font_size, weight='bold') + + self.ax.set_ylim(0, 2) + self.ax.set_xlim(-0.5 * width, nodes_x[-1] + 1.5 * width) + + self.ax.axis('off') + self.ax.grid() + + self.canvas.draw() + +class ChainedAnalysisPage(wx.Panel): + def __init__(self, parent, protocol_rules, recipe_manager, + spool_controller, default_pairings=None): + """ + + Parameters + ---------- + parent : wx something + protocol_rules : dict + [description] + recipe_manager : PYME.recipes.recipeGui.RecipeManager + [description] + default_pairings : dict + protocol keys with lists of RuleFactorys as values to prepopulate + panel on start up + """ + wx.Panel.__init__(self, parent, -1) + self._protocol_rules = protocol_rules + self._selected_protocol = list(protocol_rules.keys())[0] + self._recipe_manager = recipe_manager + + self._editor_panels = [] + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + + # associated protocol choicebox (default / others on top, then rest) + hsizer.Add(wx.StaticText(self, -1, 'Associated Protocol:'), 0, + wx.LEFT|wx.RIGHT, 5) + self.c_protocol = wx.Choice(self, -1, choices=get_protocol_list()) + self.c_protocol.SetSelection(0) + self.c_protocol.Bind(wx.EVT_CHOICE, self.OnProtocolChoice) + hsizer.Add(self.c_protocol, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + # post on choicebox + hsizer.Add(wx.StaticText(self, -1, 'Post On:'), 0, + wx.LEFT|wx.RIGHT, 5) + self.c_post = wx.Choice(self, -1, choices=POST_CHOICES) + self.c_post.SetSelection(0) + self.c_post.Bind(wx.EVT_CHOICE, self.OnPostChoice) + hsizer.Add(self.c_post, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + vsizer.Add(hsizer, 0) + + # rule plot + self.rule_plot = RulePlotPanel(self, self._protocol_rules, + size=(-1, 400)) + vsizer.Add(self.rule_plot, 1, wx.ALL|wx.EXPAND, 5) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.b_clear = wx.Button(self, -1, 'Clear Chain') + hsizer.Add(self.b_clear, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_clear.Bind(wx.EVT_BUTTON, self.OnClear) + + self.b_add_recipe = wx.Button(self, -1, 'Add Recipe') + hsizer.Add(self.b_add_recipe, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_add_recipe.Bind(wx.EVT_BUTTON, self.OnAddRecipe) + + vsizer.Add(hsizer, 0, wx.EXPAND, 0) + self.SetSizerAndFit(vsizer) + self._protocol_rules._updated.connect(self.update) + + def OnClear(self, wx_event=None): + self._protocol_rules[self._selected_protocol] = RuleChain() + self._protocol_rules._updated.send(self) + + def OnAddRecipe(self, wx_event=None): + self._rpan.Show() + self.Layout() + #self._recipe_manager.OnAddRecipeRule() + + def OnProtocolChoice(self, wx_event=None): + self.select_rule_chain(self.c_protocol.GetStringSelection()) + + def select_rule_chain(self, protocol='default'): + if protocol not in self._protocol_rules.keys(): + self._protocol_rules[protocol] = RuleChain() + self._protocol_rules._updated.send(self) + self._selected_protocol = protocol + # force a redraw, even though we might just have done so if we added + self.update() + + def OnPostChoice(self, wx_event=None): + self._protocol_rules[self._selected_protocol].post_on = self.c_post.GetStringSelection() + self._protocol_rules._updated.send(self) + + def _set_text_styling(self): + from wx import stc + ed = self.tRecipeText + ed.SetLexer(wx.stc.STC_LEX_YAML) + + # Enable folding + ed.SetProperty("fold", "1") + + # Highlight tab/space mixing (shouldn't be any) + ed.SetProperty("tab.timmy.whinge.level", "1") + + # Set left and right margins + ed.SetMargins(2, 2) + + # Set up the numbers in the margin for margin #1 + ed.SetMarginType(1, wx.stc.STC_MARGIN_NUMBER) + # Reasonable value for, say, 4-5 digits using a mono font (40 pix) + ed.SetMarginWidth(1, 40) + + # Indentation and tab stuff + ed.SetIndent(4) # Proscribed indent size for wx + ed.SetIndentationGuides(True) # Show indent guides + ed.SetBackSpaceUnIndents(True)# Backspace unindents rather than delete 1 space + ed.SetTabIndents(True) # Tab key indents + ed.SetTabWidth(4) # Proscribed tab size for wx + ed.SetUseTabs(False) # Use spaces rather than tabs, or + # TabTimmy will complain! + # White space + ed.SetViewWhiteSpace(False) # Don't view white space + + # EOL: Since we are loading/saving ourselves, and the + # strings will always have \n's in them, set the STC to + # edit them that way. + ed.SetEOLMode(wx.stc.STC_EOL_LF) + ed.SetViewEOL(False) + + # No right-edge mode indicator + ed.SetEdgeMode(stc.STC_EDGE_NONE) + + # Setup a margin to hold fold markers + ed.SetMarginType(2, stc.STC_MARGIN_SYMBOL) + ed.SetMarginMask(2, stc.STC_MASK_FOLDERS) + ed.SetMarginSensitive(2, True) + ed.SetMarginWidth(2, 12) + + # and now set up the fold markers + ed.MarkerDefine(stc.STC_MARKNUM_FOLDEREND, stc.STC_MARK_BOXPLUSCONNECTED, "white", "black") + ed.MarkerDefine(stc.STC_MARKNUM_FOLDEROPENMID, stc.STC_MARK_BOXMINUSCONNECTED, "white", "black") + ed.MarkerDefine(stc.STC_MARKNUM_FOLDERMIDTAIL, stc.STC_MARK_TCORNER, "white", "black") + ed.MarkerDefine(stc.STC_MARKNUM_FOLDERTAIL, stc.STC_MARK_LCORNER, "white", "black") + ed.MarkerDefine(stc.STC_MARKNUM_FOLDERSUB, stc.STC_MARK_VLINE, "white", "black") + ed.MarkerDefine(stc.STC_MARKNUM_FOLDER, stc.STC_MARK_BOXPLUS, "white", "black") + ed.MarkerDefine(stc.STC_MARKNUM_FOLDEROPEN, stc.STC_MARK_BOXMINUS, "white", "black") + + # Global default style + if wx.Platform == '__WXMSW__': + ed.StyleSetSpec(stc.STC_STYLE_DEFAULT, + 'fore:#000000,back:#FFFFFF,face:Courier New') + elif wx.Platform == '__WXMAC__': + # TODO: if this looks fine on Linux too, remove the Mac-specific case + # and use this whenever OS != MSW. + ed.StyleSetSpec(stc.STC_STYLE_DEFAULT, + 'fore:#000000,back:#FFFFFF,face:Monaco') + else: + defsize = wx.SystemSettings.GetFont(wx.SYS_ANSI_FIXED_FONT).GetPointSize() + ed.StyleSetSpec(stc.STC_STYLE_DEFAULT, + 'fore:#000000,back:#FFFFFF,face:Courier,size:%d' % defsize) + + # Clear styles and revert to default. + ed.StyleClearAll() + + # Following style specs only indicate differences from default. + # The rest remains unchanged. + + # Line numbers in margin + ed.StyleSetSpec(wx.stc.STC_STYLE_LINENUMBER, 'fore:#000000,back:#99A9C2') + # Highlighted brace + ed.StyleSetSpec(wx.stc.STC_STYLE_BRACELIGHT, 'fore:#00009D,back:#FFFF00') + # Unmatched brace + ed.StyleSetSpec(wx.stc.STC_STYLE_BRACEBAD, 'fore:#00009D,back:#FF0000') + # Indentation guide + ed.StyleSetSpec(wx.stc.STC_STYLE_INDENTGUIDE, "fore:#CDCDCD") + + # YAML styles + ed.StyleSetSpec(wx.stc.STC_YAML_DEFAULT, 'fore:#000000') + ed.StyleSetSpec(wx.stc.STC_YAML_COMMENT, 'fore:#008000,back:#F0FFF0') + ed.StyleSetSpec(wx.stc.STC_YAML_NUMBER, 'fore:#0080F0') + ed.StyleSetSpec(wx.stc.STC_YAML_IDENTIFIER, 'fore:#80000') + ed.StyleSetSpec(wx.stc.STC_YAML_DOCUMENT, 'fore:#E0E000') #what is this? + ed.StyleSetSpec(wx.stc.STC_YAML_KEYWORD, 'fore:#000080,bold') + ed.StyleSetSpec(wx.stc.STC_YAML_ERROR, 'fore:#FE2020') + ed.StyleSetSpec(wx.stc.STC_YAML_OPERATOR, 'fore:#0000A0') + ed.StyleSetSpec(wx.stc.STC_YAML_REFERENCE, 'fore:#E0E000') + ed.StyleSetSpec(wx.stc.STC_YAML_TEXT, 'fore:#E0E000') + + # Caret color + ed.SetCaretForeground("BLUE") + # Selection background + ed.SetSelBackground(1, '#66CCFF') + + def update(self, *args, **kwargs): + self.rule_plot.draw() + + def OnPick(self, event): + # FIXME - open rule in respective editing tab + raise NotImplementedError + from PYME.IO import tabular + k = event.artist._data + if not (isinstance(k, six.string_types)): + self.configureModule(k) + else: + outp = self.recipes.activeRecipe.namespace[k] + if isinstance(outp, ImageStack): + if not 'dsviewer' in dir(self.recipes): + dv = ViewIm3D(outp, mode='lite') + else: + if self.recipes.dsviewer.mode == 'visGUI': + mode = 'visGUI' + else: + mode = 'lite' + + dv = ViewIm3D(outp, mode=mode, glCanvas=self.recipes.dsviewer.glCanvas) + + elif isinstance(outp, tabular.TabularBase): + from PYME.ui import recArrayView + f = recArrayView.ArrayFrame(outp, parent=self, title='Data table - %s' % k) + f.Show() + + @property + def rule_chain(self): + return self._protocol_rules[self._selected_protocol] + + def add_tile(self, rule_tile): + self.rule_chain.rule_factories.append(rule_tile) + self._protocol_rules._updated.send(self) + + for e in self._editor_panels: + e.Hide() + self.Layout() + + +class SMLMChainedAnalysisPage(ChainedAnalysisPage): + def __init__(self, parent, protocol_rules, recipe_manager, + localization_panel=None, default_pairings=None, localization_settings=None): + """ + + Parameters + ---------- + parent : wx something + protocol_rules : dict + [description] + recipe_manager : PYME.recipes.recipeGui.RecipeManager + [description] + default_pairings : dict + protocol keys with lists of RuleFactorys as values to prepopulate + panel on start up + """ + wx.Panel.__init__(self, parent, -1) + self._protocol_rules = protocol_rules + self._selected_protocol = list(protocol_rules.keys())[0] + self._recipe_manager = recipe_manager + self._localization_panel = localization_panel + + self._editor_panels = [] + + v_sizer = wx.BoxSizer(wx.VERTICAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + # associated protocol choicebox (default / others on top, then rest) + hsizer.Add(wx.StaticText(self, -1, 'Associated Protocol:'), 0, + wx.LEFT|wx.RIGHT, 5) + self.c_protocol = wx.Choice(self, -1, choices=get_protocol_list()) + self.c_protocol.SetSelection(0) + self.c_protocol.Bind(wx.EVT_CHOICE, self.OnProtocolChoice) + hsizer.Add(self.c_protocol, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + # post on choicebox + hsizer.Add(wx.StaticText(self, -1, 'Post On:'), 0, + wx.LEFT|wx.RIGHT, 5) + self.c_post = wx.Choice(self, -1, choices=POST_CHOICES) + self.c_post.SetSelection(0) + self.c_post.Bind(wx.EVT_CHOICE, self.OnPostChoice) + hsizer.Add(self.c_post, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + vsizer.Add(hsizer, 0) + + # rule plot + self.rule_plot = RulePlotPanel(self, self._protocol_rules, + size=(-1, 200)) + vsizer.Add(self.rule_plot, 1, wx.ALL|wx.EXPAND, 5) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.b_clear = wx.Button(self, -1, 'Clear Chain') + hsizer.Add(self.b_clear, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_clear.Bind(wx.EVT_BUTTON, self.OnClear) + + self.b_add_recipe = wx.Button(self, -1, 'Add Recipe') + hsizer.Add(self.b_add_recipe, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_add_recipe.Bind(wx.EVT_BUTTON, self.OnAddRecipe) + + self.b_add_localization = wx.Button(self, -1, 'Add Localization') + hsizer.Add(self.b_add_localization, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_add_localization.Bind(wx.EVT_BUTTON, self.OnAddLocalization) + + vsizer.Add(hsizer, 0, wx.EXPAND, 0) + v_sizer.Add(vsizer, 0, wx.EXPAND, 0) + + if self._localization_panel is None and not localization_settings is None: + self._lpan = wx.Panel(self, -1) + self._localization_panel = LocalizationSettingsPanel(self._lpan,localization_settings, localization_settings.onMetadataChanged) + self._localization_panel.chained_analysis_page = self + sbsizer = wx.StaticBoxSizer(wx.StaticBox(self._lpan, -1, 'Localisation settings'), wx.HORIZONTAL) + sbsizer.Add(self._localization_panel, 1, wx.EXPAND, 0) + self._lpan.SetSizerAndFit(sbsizer) + v_sizer.Add(self._lpan, 1, wx.EXPAND|wx.ALL, 5) + self._lpan.Hide() + self._editor_panels.append(self._lpan) + + self._rpan = wx.Panel(self, -1) + self._recipe_view = RuleRecipeView(self._rpan, self._recipe_manager) + sbsizer = wx.StaticBoxSizer(wx.StaticBox(self._rpan, -1, 'Recipe Editor'), wx.HORIZONTAL) + sbsizer.Add(self._recipe_view, 1, wx.EXPAND, 0) + self._rpan.SetSizerAndFit(sbsizer) + v_sizer.Add(self._rpan, 1, wx.EXPAND|wx.ALL, 5) + self._rpan.Hide() + self._editor_panels.append(self._rpan) + + + self.SetSizerAndFit(v_sizer) + self._protocol_rules._updated.connect(self.update) + + def OnAddLocalization(self, wx_event=None): + self._lpan.Show() + self.Layout() + #self._localization_panel.OnAddLocalizationRule() + +from PYME.recipes.recipeGui import RecipeView, RecipeManager, RecipePlotPanel +class RuleRecipeView(RecipeView): + def __init__(self, parent, recipes): + wx.Panel.__init__(self, parent, size=(400, 100)) + self.recipes = recipes + recipes.recipeView = self # weird plug + self._editing = False #are we currently editing a recipe module? used for a hack / workaround for a a traits/matplotlib bug to disable click-throughs + hsizer1 = wx.BoxSizer(wx.HORIZONTAL) + + vsizer = wx.BoxSizer(wx.VERTICAL) + + self.recipePlot = RecipePlotPanel(self, recipes, size=(-1, 400)) + vsizer.Add(self.recipePlot, 1, wx.ALL | wx.EXPAND, 5) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.bNewRecipe = wx.Button(self, -1, 'Clear Recipe') + hsizer.Add(self.bNewRecipe, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + self.bNewRecipe.Bind(wx.EVT_BUTTON, self.OnNewRecipe) + + self.bLoadRecipe = wx.Button(self, -1, 'Load Recipe') + hsizer.Add(self.bLoadRecipe, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + self.bLoadRecipe.Bind(wx.EVT_BUTTON, self.recipes.OnLoadRecipe) + + self.bAddModule = wx.Button(self, -1, 'Add Module') + hsizer.Add(self.bAddModule, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + self.bAddModule.Bind(wx.EVT_BUTTON, self.OnAddModule) + + #self.bRefresh = wx.Button(self, -1, 'Refresh') + #hsizer.Add(self.bRefresh, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + self.bSaveRecipe = wx.Button(self, -1, 'Save Recipe') + hsizer.Add(self.bSaveRecipe, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + self.bSaveRecipe.Bind(wx.EVT_BUTTON, self.recipes.OnSaveRecipe) + + self.b_add_recipe_rule = wx.Button(self, -1, + 'Add Recipe to Chained Analysis') + + self.b_add_recipe_rule.Bind(wx.EVT_BUTTON, + self.recipes.OnAddRecipeRule) + vsizer.Add(hsizer, 0, wx.EXPAND, 0) + + hsizer1.Add(vsizer, 1, wx.EXPAND | wx.ALL, 2) + + vsizer = wx.BoxSizer(wx.VERTICAL) + + #self.tRecipeText = wx.TextCtrl(self, -1, '', size=(350, -1), + # style=wx.TE_MULTILINE|wx.TE_PROCESS_ENTER) + self.tRecipeText = wx.stc.StyledTextCtrl(self, -1, size=(400, -1)) + self._set_text_styling() + + + vsizer.Add(self.tRecipeText, 1, wx.ALL, 2) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.bApply = wx.Button(self, -1, 'Apply Text Changes') + self.bApply.Bind(wx.EVT_BUTTON, self.OnApplyText) + hsizer.Add(self.bApply, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + hsizer.Add(self.b_add_recipe_rule, 0, + wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + + vsizer.Add(hsizer, 0, wx.ALL, 2) + + self.bApply.Disable() + self.tRecipeText.Bind(wx.stc.EVT_STC_MODIFIED, lambda e : self.bApply.Enable()) + + hsizer1.Add(vsizer, 0, wx.EXPAND | wx.ALL, 2) + + self.SetSizerAndFit(hsizer1) + + self.recipes.LoadRecipeText('') + + recipes.activeRecipe.recipe_changed.connect(self.update) + recipes.activeRecipe.recipe_executed.connect(self.update) + +class RuleRecipeManager(RecipeManager): + def __init__(self, chained_analysis_page=None): + RecipeManager.__init__(self) + self.chained_analysis_page = chained_analysis_page + + def OnAddRecipeRule(self, wx_event=None): + from PYME.cluster.rules import RecipeRuleFactory + #from PYME.Acquire.htsms.rule_ui import get_rule_tile + if self.chained_analysis_page is None: + logger.error('chained_analysis_page attribute unset') + + rec = get_rule_tile(RecipeRuleFactory)(recipe=self.activeRecipe.toYAML()) + self.chained_analysis_page.add_tile(rec) + +class ChainedAnalysisPanel(wx.Panel): + def __init__(self, parent, protocol_rules, chained_analysis_page, + default_pairings=None): + """ + + Parameters + ---------- + parent : PYME.ui.AUIFrame.AUIFrame + should be the 'main frame' + protocol_rules : dict + [description] + recipe_manager : PYME.recipes.recipeGui.RecipeManager + [description] + default_pairings : dict + protocol keys with RuleChains as values to prepopulate + panel on start up + """ + wx.Panel.__init__(self, parent, -1) + self.parent = parent + self._protocol_rules = protocol_rules + self._page = chained_analysis_page + + v_sizer = wx.BoxSizer(wx.VERTICAL) + h_sizer = wx.BoxSizer(wx.HORIZONTAL) + + self.checkbox_active = wx.CheckBox(self, -1, 'active') + self.checkbox_active.SetValue(True) + self.checkbox_active.Bind(wx.EVT_CHECKBOX, self.OnToggleActive) + h_sizer.Add(self.checkbox_active, 0, wx.ALL, 2) + v_sizer.Add(h_sizer, 0, wx.EXPAND|wx.TOP, 0) + + h_sizer = wx.BoxSizer(wx.HORIZONTAL) + self._protocol_rules_list = ProtocolRuleFactoryListCtrl(self._protocol_rules, self) + h_sizer.Add(self._protocol_rules_list) + v_sizer.Add(h_sizer, 0, wx.EXPAND|wx.TOP, 0) + + h_sizer = wx.BoxSizer(wx.HORIZONTAL) + self.button_del_chain = wx.Button(self, -1, 'Delete pair') + self.button_del_chain.Bind(wx.EVT_BUTTON, self.OnRemoveProtocolRule) + h_sizer.Add(self.button_del_chain, 0, wx.ALL, 2) + + self.button_edit_chain = wx.Button(self, -1, 'Edit in tab') + self.button_edit_chain.Bind(wx.EVT_BUTTON, self.OnEditRuleChain) + h_sizer.Add(self.button_edit_chain, 0, wx.ALL, 2) + v_sizer.Add(h_sizer) + + self.SetSizerAndFit(v_sizer) + + if default_pairings is not None: + self._set_up_defaults(default_pairings) + + def _set_up_defaults(self, pairings): + for protocol_name, rule_chain in pairings.items(): + # add them to the protocol rules dict + self._protocol_rules[protocol_name] = rule_chain + + self._protocol_rules._updated.send(self) + + def OnRemoveProtocolRule(self, wx_event=None): + # make sure that we reset the chained analysis page just in case + # we deleted a rule which was active + self._page.c_protocol.SetStringSelection('default') + self._page.OnProtocolChoice() + self._protocol_rules_list.delete_rule_chains() + + def OnEditRuleChain(self, wx_event=None): + ind = self._protocol_rules_list.get_selected_items()[0] + protocol = self._protocol_rules_list.GetItemText(ind, col=0) + self._page.select_rule_chain(protocol) + self.parent._select_page_by_name('Chained Analysis') + + def OnToggleActive(self, wx_event): + self._protocol_rules.active = self.checkbox_active.GetValue() + + @staticmethod + def plug(main_frame, scope, default_pairings=None): + """ + Adds a ChainedAnalysisPanel to a microscope gui during start-up + + Parameters + ---------- + main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame + microscope gui application + scope : PYME.Acquire.microscope.Microscope + the microscope itself + default_pairings : dict + [optional] protocol keys with lists of RuleFactorys as values to + prepopulate panel on start up. By default, None + + """ + + scope.protocol_rules = ProtocolRules(scope.spoolController) + scope._recipe_manager = RuleRecipeManager() + + main_frame.chained_analysis_page = ChainedAnalysisPage(main_frame, + scope.protocol_rules, + scope._recipe_manager, + default_pairings) + + scope._recipe_manager.chained_analysis_page = main_frame.chained_analysis_page + main_frame.recipe_view = RuleRecipeView(main_frame, scope._recipe_manager) + + main_frame.AddPage(page=main_frame.recipe_view, select=False, caption='Recipe') + main_frame.AddPage(page=main_frame.chained_analysis_page, select=False, + caption='Chained Analysis') + + # add this panel + chained_analysis = ChainedAnalysisPanel(main_frame, + scope.protocol_rules, + main_frame.chained_analysis_page, + default_pairings) + main_frame.anPanels.append((chained_analysis, 'Automatic Analysis', + True)) + + +from PYME.Acquire.ui.AnalysisSettingsUI import AnalysisSettingsPanel, AnalysisDetailsPanel, manualFoldPanel +class LocalizationSettingsPanel(wx.Panel): + def __init__(self, wx_parent, localization_settings, mdh_changed_signal=None, + chained_analysis_page=None): + #from PYME.ui.autoFoldPanel import collapsingPane + #manualFoldPanel.foldingPane.__init__(self, wx_parent, caption='Localization Analysis') + wx.Panel.__init__(self, wx_parent) + + vsizer = wx.BoxSizer(wx.VERTICAL) + + self.localization_settings = localization_settings + self.localization_mdh = localization_settings.analysisMDH + self.mdh_changed_signal = mdh_changed_signal + self.chained_analysis_page = chained_analysis_page + + #clp = collapsingPane(self, caption='settings ...') + + asp = AnalysisSettingsPanel(self,self.localization_settings,self.mdh_changed_signal) + vsizer.Add(asp, 0, wx.EXPAND|wx.ALL, 5) + adp = AnalysisDetailsPanel(self, self.localization_settings,self.mdh_changed_signal) + vsizer.Add(adp, 1, wx.EXPAND|wx.ALL, 5) + + + # add box to propagate rule to rule chain + self.b_add_rule = wx.Button(self, -1, + 'Add Localization to Chained Analysis') + self.b_add_rule.Bind(wx.EVT_BUTTON, self.OnAddLocalizationRule) + vsizer.Add(self.b_add_rule, 0, wx.ALIGN_RIGHT|wx.ALL, 5) + + self.SetSizerAndFit(vsizer) + + + def OnAddLocalizationRule(self, wx_event=None): + from PYME.cluster.rules import SpoolLocalLocalizationRuleFactory + from PYME.IO.MetaDataHandler import DictMDHandler + if self.chained_analysis_page is None: + logger.error('chained_analysis_page attribute unset') + return + + mdh = DictMDHandler(self.localization_mdh) + loc_rule = get_rule_tile(SpoolLocalLocalizationRuleFactory)(analysisMetadata=mdh) + self.chained_analysis_page.add_tile(loc_rule) + +class SMLMChainedAnalysisPanel(ChainedAnalysisPanel): + def __init__(self, wx_parent, protocol_rules, chained_analysis_page, + default_pairings=None): + """ + Parameters + ---------- + wx_parent + localization_settings: PYME.ui.AnalysisSettingsUI.AnalysisSettings + rule_list_ctrl: RuleChainListCtrl + default_pairings : dict + [optional] protocol keys with lists of RuleFactorys as values to + prepopulate panel on start up. By default, None + """ + ChainedAnalysisPanel.__init__(self, wx_parent, protocol_rules, + chained_analysis_page, default_pairings) + + # def OnToggleLiveView(self, wx_event=None): + # if self.checkbox_view_live.GetValue() and 0 in self._rule_list_ctrl.localization_rule_indices: + # self._rule_list_ctrl._rule_chain.posted.connect(self._open_live_view) + # else: + # self._rule_list_ctrl._rule_chain.posted.disconnect(self._open_live_view) + + def _open_live_view(self, **kwargs): + """ + Open PYMEVisualize on a freshly spooled series which is being localized + + Parameters + ---------- + kwargs: dict + present here to allow us to call this method through a dispatch.Signal.send + """ + import subprocess + # get the URL + uri = self._rule_list_ctrl._rule_chain[self._rule_list_ctrl.localization_rule_indices[0]].outputs[0]['input'] + '/live' + subprocess.Popen('visgui %s' % uri, shell=True) + + @staticmethod + def plug(main_frame, scope, default_pairings=None): + """ + Adds a SMLMChainedAnalysisPanel to a microscope gui during start-up + + Parameters + ---------- + main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame + microscope gui application + scope : PYME.Acquire.microscope.Microscope + the microscope itself + default_pairings : dict + [optional] protocol keys with RuleChains as values to + prepopulate panel on start up. By default, None + """ + from PYME.Acquire.ui.AnalysisSettingsUI import AnalysisSettings + + scope.protocol_rules = ProtocolRules(scope.spoolController) + scope._recipe_manager = RuleRecipeManager() + scope._localization_settings = AnalysisSettings() #TODO - we should not need this to be global. + + # localization_settings_pan = LocalizationSettingsPanel(main_frame, + # scope._localization_settings, + # scope._localization_settings.onMetadataChanged) + + main_frame.chained_analysis_page = SMLMChainedAnalysisPage(main_frame, + scope.protocol_rules, + scope._recipe_manager, + None, + default_pairings, localization_settings=scope._localization_settings) + + scope._recipe_manager.chained_analysis_page = main_frame.chained_analysis_page + #main_frame.recipe_view = RuleRecipeView(main_frame, scope._recipe_manager) + #main_frame.localization_settings.chained_analysis_page = main_frame.chained_analysis_page + #main_frame.AddPage(page=main_frame.recipe_view, select=False, caption='Recipe') + #main_frame.AddPage(page=main_frame.localization_settings, select=False, caption='Localization') + main_frame.AddPage(page=main_frame.chained_analysis_page, select=False, + caption='Chained Analysis') + + # add this panel + chained_analysis = SMLMChainedAnalysisPanel(main_frame, + scope.protocol_rules, + main_frame.chained_analysis_page, + default_pairings) + main_frame.anPanels.append((chained_analysis, 'Automatic Analysis', + False)) diff --git a/PYME/Acquire/htsms/rule_ui_v2.py b/PYME/Acquire/htsms/rule_ui_v2.py new file mode 100644 index 000000000..c148d4215 --- /dev/null +++ b/PYME/Acquire/htsms/rule_ui_v2.py @@ -0,0 +1,573 @@ + +import wx +# from PYME.ui import manualFoldPanel +from PYME.cluster.rules import LocalisationRuleFactory as LocalizationRuleFactory +from PYME.cluster.rules import RecipeRuleFactory +from collections import OrderedDict +#import queue +import os +import posixpath +import logging +from PYME.contrib import dispatch, wxPlotPanel +from PYME.recipes.traits import HasTraits, Enum, Float, CStr +import textwrap +import numpy as np +import matplotlib.pyplot as plt +import threading + +from PYME.IO.MetaDataHandler import DictMDHandler + +logger = logging.getLogger(__name__) + +POST_CHOICES = ['off', 'spool start', 'spool stop'] + +class RuleTile(HasTraits): + task_timeout = Float(60 * 10) + rule_timeout = Float(60 * 10) + + def get_params(self): + editable = self.class_editable_traits() + return editable + + @property + def default_view(self): + if wx.GetApp() is None: + return None + from traitsui.api import View, Item + + return View([Item(tn) for tn in self.get_params()], buttons=['OK']) + + def default_traits_view(self): + """ This is the traits stock method to specify the default view""" + return self.default_view + +def get_rule_tile(rule_factory_class): + class _RuleTile(RuleTile, rule_factory_class): + def __init__(self, **kwargs): + RuleTile.__init__(self) + rule_factory_class.__init__(self, **kwargs) + + self._rule_factory_class_name = rule_factory_class.__name__ + + def serialise(self): + return(self._rule_factory_class_name, self._rule_kwargs) + + return _RuleTile + +def get_rule_factory_class(name): + from PYME.cluster import rules + return getattr(rules, name) + + +class RuleChain(HasTraits): + def __init__(self, rule_factories=None, *args, **kwargs): + if rule_factories is None: + rule_factories = list() + self.rule_factories = rule_factories + HasTraits.__init__(self, *args, **kwargs) + + def to_yaml(self): + import yaml + from PYME.recipes.base import MyDumper + return yaml.dump([rf.serialise() for rf in self.rule_factories], Dumper=MyDumper) + + @classmethod + def from_yaml(cls, yaml_str): + import yaml + factories = yaml.safe_load(yaml_str) + return cls([get_rule_tile(get_rule_factory_class(cls))(**kwargs) for cls, kwargs in factories]) + +class RuleDict(OrderedDict): + """ + Container for associating sets of analysis rules with specific acquisition + protocols + + Notes + ----- + use ordered dict for reproducibility with listctrl displays + """ + def __init__(self): + """ + Parameters + ---------- + posting_thread_queue_size : int, optional + sets the size of a queue to hold rule posting threads to ensure they + have time to execute, by default 5. .. seealso:: modules :py:mod:`PYME.cluster.rules` + """ + import queue + from PYME.cluster.rules import SpoolLocalLocalizationRuleFactory + + OrderedDict.__init__(self) + self.active = True + #self._spool_controller = spool_controller + #self.posting_thread_queue = queue.Queue(posting_thread_queue_size) + self._updated = dispatch.Signal() + self._updated.connect(self.update) + + self['default'] = RuleChain() + # TODO - make the default rule chain a 2D Gaussian localization rule + #mdh = DictMDHandler(self._localization_panel.localization_settings.analysisMDH) + #loc_rule = get_rule_tile(SpoolLocalLocalizationRuleFactory)(analysisMetadata=mdh) + + + def load_from_config(self): + """ + Load rule chains stored in the analysis_rules subfolder of the PYME config directory + (typically ~/.PYME/analysis_rules) + + """ + from PYME import config + + chains = config.get_analysis_rulechains() + + for k, fn in chains.items(): + with open(fn, 'r') as f: + self[os.path.splitext(k)[0]] = RuleChain.from_yaml(f.read()) + + + def update(self, *args, **kwargs): + for p in self.keys(): + factories = self[p].rule_factories + for ind in range(len(factories) - 1): + factories[ind].chain(factories[ind + 1]) + + + +class RulePlotPanel(wxPlotPanel.PlotPanel): + def __init__(self, parent, protocol_rules, **kwargs): + self.protocol_rules = protocol_rules + self.parent = parent + wxPlotPanel.PlotPanel.__init__(self, parent, **kwargs) + self.figure.canvas.mpl_connect('pick_event', self.parent.OnPick) + + def draw(self): + if not self.IsShownOnScreen(): + return + + if not hasattr( self, 'ax' ): + self.ax = self.figure.add_axes([0, 0, 1, 1]) + + self.ax.cla() + + rule_factories = self.parent.rule_chain.rule_factories + if len(rule_factories) < 1: + self.canvas.draw() + return + width = 1 # size of tile to draw + height = 1 + nodes_x = np.arange(0, len(rule_factories) * 1.5 * width, 1.5 * width) + nodes_y = 0.5*np.ones_like(nodes_x) + + + axis_width = self.ax.get_window_extent().width + n_cols = max([1] + nodes_x.tolist()) + pix_per_col = axis_width / n_cols + + font_size = max(6, min(10, 10 * pix_per_col / 100)) + + TW = textwrap.TextWrapper(width=max(int(1.8 * pix_per_col / font_size), 10), + subsequent_indent=' ') + TW2 = textwrap.TextWrapper(width=max(int(1.3 * pix_per_col / font_size), 10), + subsequent_indent=' ') + + cols = {} + + # plot connecting lines + for ind in range(1, len(rule_factories)): + self.ax.plot([nodes_x[ind - 1] + width, nodes_x[ind]], + [nodes_y[ind - 1] + 0.5 * height, + nodes_y[ind] + 0.5 * height], lw=2) + + #plot the boxes and the labels + for ind in range(len(rule_factories)): + # draw a box + rule = rule_factories[ind] + s = rule.rule_type + fc = [.8,.8, 1] + + rect = plt.Rectangle([nodes_x[ind], nodes_y[ind]], width, height, + ec='k', lw=2, fc=fc, picker=True) + rect._data = rule + self.ax.add_patch(rect) + + s = TW2.wrap(s) + if len(s) == 1: + self.ax.text(nodes_x[ind] + .05, nodes_y[ind] + .68 , s[0], size=font_size, weight='bold') + else: + self.ax.text(nodes_x[ind] + .05, nodes_y[ind] + .18 - .05*(len(s) - 1) , '\n'.join(s), size=font_size, weight='bold') + + if rule.rule_type == 'localization': + try: + s = TW.wrap(str(rule._rule_kwargs['analysisMetadata']['Analysis.FitModule'])) + self.ax.text(nodes_x[ind] + .05, nodes_y[ind] + .18 - .05*(len(s)) , '\n'.join(s), size=font_size, weight='normal') + except KeyError: + pass + elif rule.rule_type == 'recipe': + try: + r = str(rule._rule_kwargs['recipe']) + s = TW.wrap(r.split('\n')[0] + ' ...') + self.ax.text(nodes_x[ind] + .05, nodes_y[ind] + .18 - .05*(len(s)) , '\n'.join(s), size=font_size, weight='normal') + except KeyError: + pass + + self.ax.set_ylim(0, 2) + self.ax.set_xlim(-0.5 * width, nodes_x[-1] + 1.5 * width) + + self.ax.axis('off') + self.ax.grid() + + self.canvas.draw() + +class ChainedAnalysisPage(wx.Panel): + def __init__(self, parent, rules, recipe_manager, + localization_panel=None, default_pairings=None, localization_settings=None): + """ + + Parameters + ---------- + parent : wx something + protocol_rules : dict + [description] + recipe_manager : PYME.recipes.recipeGui.RecipeManager + [description] + default_pairings : dict + protocol keys with lists of RuleFactorys as values to prepopulate + panel on start up + """ + wx.Panel.__init__(self, parent, -1) + self._loaded_rules = rules + self._selected_rule = list(rules.keys())[0] + self._recipe_manager = recipe_manager + self._localization_panel = localization_panel + + self._displayed_rule_keys = None + + self._editor_panels = [] + + v_sizer = wx.BoxSizer(wx.VERTICAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + # hsizer = wx.BoxSizer(wx.HORIZONTAL) + + # # associated protocol choicebox (default / others on top, then rest) + # hsizer.Add(wx.StaticText(self, -1, 'Rule Chain:'), 0, + # wx.LEFT|wx.RIGHT|wx.ALIGN_CENTER_VERTICAL, 5) + # self.c_rule_selection = wx.Choice(self, -1, choices=list(self._loaded_rules.keys())) + # self.c_rule_selection.SetSelection(0) + # self.c_rule_selection.Bind(wx.EVT_CHOICE, self.OnRuleChoice) + # hsizer.Add(self.c_rule_selection, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + # vsizer.Add(hsizer, 0, wx.EXPAND, 0) + + self.l_analysis_rules = wx.ListCtrl(self, -1, style=wx.LC_REPORT|wx.LC_SINGLE_SEL) + self.l_analysis_rules.InsertColumn(0, 'Rule Name', width=150) + self.l_analysis_rules.InsertColumn(1, 'Summary', width=wx.LIST_AUTOSIZE) + + self.l_analysis_rules.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnSelectRule) + + + #hsizer.AddStretchSpacer() + + vsizer.Add(self.l_analysis_rules, 1, wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self._b_add_rule_chain = wx.Button(self, -1, 'Add') + hsizer.Add(self._b_add_rule_chain, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self._b_add_rule_chain.Bind(wx.EVT_BUTTON, self.OnAddRuleChain) + + self._b_load_rule_chain = wx.Button(self, -1, 'Load') + hsizer.Add(self._b_load_rule_chain, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self._b_load_rule_chain.Bind(wx.EVT_BUTTON, self.OnLoadRuleChain) + + self._b_save_rule_chain = wx.Button(self, -1, 'Save') + hsizer.Add(self._b_save_rule_chain, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self._b_save_rule_chain.Bind(wx.EVT_BUTTON, self.OnSaveRuleChain) + + vsizer.Add(hsizer, 0, wx.TOP|wx.EXPAND, 5) + + # rule plot + self.rule_plot = RulePlotPanel(self, self._loaded_rules, + size=(-1, 100)) + vsizer.Add(self.rule_plot, 1, wx.ALL|wx.EXPAND, 5) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.b_clear = wx.Button(self, -1, 'Clear Chain') + hsizer.Add(self.b_clear, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_clear.Bind(wx.EVT_BUTTON, self.OnClear) + + self.b_add_localization = wx.Button(self, -1, 'Add Localization Task') + hsizer.Add(self.b_add_localization, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_add_localization.Bind(wx.EVT_BUTTON, self.OnAddLocalization) + + self.b_add_recipe = wx.Button(self, -1, 'Add Recipe Task') + hsizer.Add(self.b_add_recipe, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + self.b_add_recipe.Bind(wx.EVT_BUTTON, self.OnAddRecipe) + + vsizer.Add(hsizer, 0, wx.EXPAND, 0) + v_sizer.Add(vsizer, 0, wx.EXPAND, 0) + + if self._localization_panel is None and not localization_settings is None: + self._lpan = wx.Panel(self, -1) + self._localization_panel = LocalizationSettingsPanel(self._lpan,localization_settings, localization_settings.onMetadataChanged) + self._localization_panel.chained_analysis_page = self + sbsizer = wx.StaticBoxSizer(wx.StaticBox(self._lpan, -1, 'Localisation settings'), wx.HORIZONTAL) + sbsizer.Add(self._localization_panel, 1, wx.EXPAND, 0) + self._lpan.SetSizerAndFit(sbsizer) + v_sizer.Add(self._lpan, 1, wx.EXPAND|wx.ALL, 5) + self._lpan.Hide() + self._editor_panels.append(self._lpan) + + self._rpan = wx.Panel(self, -1) + self._recipe_view = RuleRecipeView(self._rpan, self._recipe_manager) + sbsizer = wx.StaticBoxSizer(wx.StaticBox(self._rpan, -1, 'Recipe Editor'), wx.HORIZONTAL) + sbsizer.Add(self._recipe_view, 1, wx.EXPAND, 0) + self._rpan.SetSizerAndFit(sbsizer) + v_sizer.Add(self._rpan, 1, wx.EXPAND|wx.ALL, 5) + self._rpan.Hide() + self._editor_panels.append(self._rpan) + + self.update() + self.SetSizerAndFit(v_sizer) + self._loaded_rules._updated.connect(self.update) + + def OnClear(self, wx_event=None): + self._loaded_rules[self._selected_rule] = RuleChain() + self._loaded_rules._updated.send(self) + + def OnAddRuleChain(self, wx_event=None): + # display a text enty dialog to get the name of the new rule chain + # then add it to the list of rule chains + from PYME import pyme_warnings as warnings + + dlg = wx.TextEntryDialog(self, 'Enter name for new rule chain', 'New Rule Chain', '') + if dlg.ShowModal() == wx.ID_OK: + name = dlg.GetValue() + + if name in self._loaded_rules.keys() or name == '': + dlg.Destroy() + warnings.warn('Name already in use or empty') + return + else: + self._loaded_rules[name] = RuleChain() + self._loaded_rules._updated.send(self) + + self.select_rule_chain(name) + + def OnLoadRuleChain(self, wx_event=None): + # display a dialog to select a rule file to load + # then load it into the list of rule chains + from PYME import pyme_warnings as warnings + + # TODO - specify the directory to start in + # TODO - will this be YAML? + dlg = wx.FileDialog(self, 'Choose a rule chain file', '', '', 'YAML files (*.yaml)|*.yaml', wx.FD_OPEN) + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + name = (os.path.split(path)[-1]) + if name in self._loaded_rules.keys(): + warnings.warn('Name already in use') + return + + with open(path, 'r') as f: + self._loaded_rules[name] = RuleChain.from_yaml(f.read()) + + self._loaded_rules._updated.send(self) + self.select_rule_chain(name) + + def OnSaveRuleChain(self, wx_event=None): + # display a dialog to select a file to save the rule chain to + # then save it + from PYME import pyme_warnings as warnings + + # TODO - specify the directory to start in + # TODO - will this be YAML? + dlg = wx.FileDialog(self, 'Choose a rule chain file', '', '', 'YAML files (*.yaml)|*.yaml', wx.FD_SAVE) + if dlg.ShowModal() == wx.ID_OK: + path = dlg.GetPath() + + with open(path, 'w') as f: + f.write(self._loaded_rules[self._selected_rule].to_yaml()) + + + + def OnRuleChoice(self, wx_event=None): + self.select_rule_chain(self.c_rule_selection.GetStringSelection()) + + def OnSelectRule(self, wx_event=None): + selected = self.l_analysis_rules.GetFirstSelected() + if selected != -1: + self.select_rule_chain(self.l_analysis_rules.GetItemText(selected)) + + wx_event.Skip() + + def select_rule_chain(self, rule='default'): + if rule not in self._loaded_rules.keys(): + self._loaded_rules[rule] = RuleChain() + self._loaded_rules._updated.send(self) + + self._selected_rule = rule + + for e in self._editor_panels: + e.Hide() + + # force a redraw, even though we might just have done so if we added + self.update() + + + def update(self, *args, **kwargs): + rule_keys = list(self._loaded_rules.keys()) + #self.c_rule_selection.SetItems(list(self._loaded_rules.keys())) + #self.c_rule_selection.SetStringSelection(self._selected_rule) + + if not tuple(rule_keys) == self._displayed_rule_keys: + self.l_analysis_rules.DeleteAllItems() + for ind, rule in enumerate(rule_keys): + self.l_analysis_rules.InsertItem(ind, rule) + #self.l_analysis_rules.SetItem(ind, 1, str(self._loaded_rules[rule].rule_factories)) + self._displayed_rule_keys = tuple(rule_keys) + + for ind, rule in enumerate(rule_keys): + #self.l_analysis_rules.InsertItem(ind, rule) + self.l_analysis_rules.SetItem(ind, 1, str(self._loaded_rules[rule].rule_factories)) + if (rule == self._selected_rule) and not self.l_analysis_rules.IsSelected(ind): + self.l_analysis_rules.Select(ind) + elif (rule != self._selected_rule) and self.l_analysis_rules.IsSelected(ind): + self.l_analysis_rules.Select(ind, False) + + + self.l_analysis_rules.SetColumnWidth(0, wx.LIST_AUTOSIZE) + self.l_analysis_rules.SetColumnWidth(1, wx.LIST_AUTOSIZE) + + #self.l_analysis_rules.Select(rule_keys.index(self._selected_rule)) + + self.rule_plot.draw() + + + def edit_localisation_rule(self, rule): + for e in self._editor_panels: + e.Hide() + + self._localization_panel.localization_settings.analysisMDH = rule._rule_kwargs['analysisMetadata'] + self._localization_panel.localization_settings.onMetadataChanged.send_robust(self._localization_panel.localization_settings) + self._lpan.Show() + self.Layout() + + def edit_recipe_rule(self, rule): + for e in self._editor_panels: + e.Hide() + + self._recipe_view.edit_recipe_rule(rule) + self._rpan.Show() + self.Layout() + + def OnPick(self, event): + k = event.artist._data + #logger.info('picked %s' % k) + + if k.rule_type == 'localization': + self.edit_localisation_rule(k) + elif k.rule_type == 'recipe': + self.edit_recipe_rule(k) + + @property + def rule_chain(self): + return self._loaded_rules[self._selected_rule] + + def add_tile(self, rule_tile): + self.rule_chain.rule_factories.append(rule_tile) + self._loaded_rules._updated.send(self) + + # for e in self._editor_panels: + # e.Hide() + + self.Layout() + + + + def OnAddLocalization(self, wx_event=None): + from PYME.IO.MetaDataHandler import DictMDHandler + from PYME.cluster.rules import SpoolLocalLocalizationRuleFactory + # copy the metadata handler so we don't accidentally over-ride the original + mdh = DictMDHandler(self._localization_panel.localization_settings.analysisMDH) + loc_rule = get_rule_tile(SpoolLocalLocalizationRuleFactory)(analysisMetadata=mdh) + self.add_tile(loc_rule) + + self.edit_localisation_rule(loc_rule) + + def OnAddRecipe(self, wx_event=None): + rec = get_rule_tile(RecipeRuleFactory)(recipe='') + self.add_tile(rec) + + self.edit_recipe_rule(rec) + + +from PYME.recipes.recipeGui import RecipeView, RecipeManager, RecipePlotPanel +class RuleRecipeView(RecipeView): + _rule = None + + def edit_recipe_rule(self, rule): + self._rule = rule + self.recipes.activeRecipe.update_from_yaml(rule._rule_kwargs['recipe']) + + def update(self, *args, **kwargs): + if self._rule is not None: + self._rule._rule_kwargs['recipe'] = self.recipes.activeRecipe.toYAML() + + return super().update(*args, **kwargs) + + +from PYME.Acquire.ui.AnalysisSettingsUI import AnalysisSettingsPanel, AnalysisDetailsPanel, manualFoldPanel +class LocalizationSettingsPanel(wx.Panel): + def __init__(self, wx_parent, localization_settings, + chained_analysis_page=None): + #from PYME.ui.autoFoldPanel import collapsingPane + #manualFoldPanel.foldingPane.__init__(self, wx_parent, caption='Localization Analysis') + wx.Panel.__init__(self, wx_parent) + + vsizer = wx.BoxSizer(wx.VERTICAL) + + self.localization_settings = localization_settings + self.chained_analysis_page = chained_analysis_page + + #clp = collapsingPane(self, caption='settings ...') + + asp = AnalysisSettingsPanel(self,self.localization_settings,self.localization_settings.onMetadataChanged, show_save_mdh=False) + vsizer.Add(asp, 0, wx.EXPAND|wx.ALL, 5) + adp = AnalysisDetailsPanel(self, self.localization_settings,self.localization_settings.onMetadataChanged) + vsizer.Add(adp, 1, wx.EXPAND|wx.ALL, 5) + + self.SetSizerAndFit(vsizer) + + +def plug(main_frame, scope, default_pairings=None): + """ + Adds a ChainedAnalysisPane to a microscope gui during start-up + + Parameters + ---------- + main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame + microscope gui application + scope : PYME.Acquire.microscope.Microscope + the microscope itself + default_pairings : dict + [optional] protocol keys with RuleChains as values to + prepopulate panel on start up. By default, None + """ + from PYME.Acquire.ui.AnalysisSettingsUI import AnalysisSettings + + scope.analysis_rules = RuleDict() + scope.analysis_rules.load_from_config() + _recipe_manager = RecipeManager() + _localization_settings = AnalysisSettings() #TODO - we should not need this to be global. + + main_frame.chained_analysis_page = ChainedAnalysisPage(main_frame, + scope.analysis_rules, + _recipe_manager, + None, + default_pairings, localization_settings=_localization_settings) + + + main_frame.AddPage(page=main_frame.chained_analysis_page, select=False, + caption='Chained Analysis') + diff --git a/PYME/Acquire/tweeter.py b/PYME/Acquire/htsms/tweeter.py similarity index 99% rename from PYME/Acquire/tweeter.py rename to PYME/Acquire/htsms/tweeter.py index 4f9ca6a03..e1efe12ac 100644 --- a/PYME/Acquire/tweeter.py +++ b/PYME/Acquire/htsms/tweeter.py @@ -93,7 +93,7 @@ def add_tweet_condition(self, condition): 1: trigger when count is above trigger_counts, 0: trigger when count is equal to trigger_counts, -1: trigger when count is below trigger_counts action_filter: str - Type of task to count, e.g. 'spoolController.StartSpooling'. A null string, '', will count all + Type of task to count, e.g. 'spoolController.start_spooling'. A null string, '', will count all tasks. message: str Message to tweet once triggered diff --git a/PYME/Acquire/logging.json b/PYME/Acquire/logging.json deleted file mode 100644 index e71df9ef3..000000000 --- a/PYME/Acquire/logging.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "version": 1, - "disable_existing_loggers": false, - "formatters": { - "simple": { - "format": "%(levelname)s:%(name)s:%(message)s\n" - }, - "time": { - "format": "%(asctime)s : %(levelname)s : %(name)s : %(message)s\n" - } - }, - - "handlers": { - "console": { - "class": "logging.StreamHandler", - "level": "DEBUG", - "formatter": "simple", - "stream": "ext://sys.stdout" - } - - - }, - - "root": { - "level": "DEBUG", - "handlers": ["console" ] - } -} \ No newline at end of file diff --git a/PYME/Acquire/logging.yaml b/PYME/Acquire/logging.yaml new file mode 100644 index 000000000..9f29f9e72 --- /dev/null +++ b/PYME/Acquire/logging.yaml @@ -0,0 +1,28 @@ + version: 1 + disable_existing_loggers: false + formatters: + simple: + format: "%(levelname)s:%(name)s:%(message)s\n" + + time: + format: "%(asctime)s : %(levelname)s : %(name)s : %(message)s\n" + + handlers: + console: + class: logging.StreamHandler + level: INFO + formatter: simple + stream: ext://sys.stdout + + file: + class: logging.handlers.RotatingFileHandler + level: DEBUG + formatter: simple + filename: PYMEAcquire.log #Alter to make sure directory is OK - this will dump log in current directory + maxBytes: 1000000 + backupCount: 3 + + + root: + level: DEBUG + handlers: [console, file ] diff --git a/PYME/Acquire/meson.build b/PYME/Acquire/meson.build new file mode 100644 index 000000000..92aaf4d4b --- /dev/null +++ b/PYME/Acquire/meson.build @@ -0,0 +1,201 @@ + +# Boilerplate to make sure things go in the right place - TODO can we do some of this in the top-level meson.build? +#py = import('python').find_installation(pure: false) +#np_include_dir = run_command(py, ['-c', '"import numpy; print(numpy.get_include())"'], check: true).stdout().strip() +install_dir = py.get_install_dir() / 'PYME/Acquire' + +py_sources = files( + 'frameWrangler.py', + 'stackSettings.py', + 'acquire_server.py', + 'microscope.py', + 'zScanner.py', + 'sampleInformation.py', + 'protocol.py', + 'PYMEAcquire.py', + 'acquire_client.py', + 'eventLog.py', + 'actions.py', + '__init__.py', + 'SpoolController.py', + 'acquirebase.py', + 'protocol_acquisition.py', + 'event_loop.py', + 'xyztc.py', + 'setup.py', + 'ActionManager.py', + 'acquisition_base.py', + 'stage_leveling.py', + 'ExecTools.py', + 'positionTracker.py', + 'autofocus.py', + 'acquiremainframe.py', + 'acquirewx.py', + 'sampleInformationDjangoDirect.py', + 'acquire_app.py' +) +py.install_sources(py_sources, subdir:'PYME/Acquire') + +subdir('Hardware') + +py_sources = files( + 'Protocols/tile.py', + 'Protocols/dual671_488.py', + 'Protocols/standard671.py', + 'Protocols/htsms-flow.py', + 'Protocols/sequence642_561.py', + 'Protocols/prebleach642.py', + 'Protocols/recover671.py', + 'Protocols/simulStep.py', + 'Protocols/prebleach561.py', + 'Protocols/shiftfield.py', + 'Protocols/simul488.py', + 'Protocols/darkCalibrate.py', + 'Protocols/strip_tile.py', + 'Protocols/simul642HTSMS.py', + 'Protocols/prebleach671ND1.py', + 'Protocols/simul642.py', + 'Protocols/htsms-two-color.py', + 'Protocols/prebleach561NeomEos2ND1.py', + 'Protocols/tile_triggered.py', + 'Protocols/prebleach488.py', + 'Protocols/prebleach561NeomEos2.py', + 'Protocols/sequence642_488.py', + 'Protocols/htsms-tile.py', + 'Protocols/tile671.py', + 'Protocols/htsms-widefield.py', + 'Protocols/__init__.py', + 'Protocols/spdscan.py', + 'Protocols/dual671_470.py', + 'Protocols/simulSIM.py', + 'Protocols/htsms-cal-registration.py', + 'Protocols/DMDMFM.py', + 'Protocols/htsms-cal-psf.py', + 'Protocols/standard488.py', + 'Protocols/photoconversion2.py', + 'Protocols/prebleach671ND2.py', + 'Protocols/photoconversion.py', + 'Protocols/prebleach490.py', + 'Protocols/prebleach671.py', + 'Protocols/htsms-staggered.py', + 'Protocols/prebleach671Neo.py', + 'Protocols/gainCal.py', + 'Protocols/standardArclamp.py', + 'Protocols/tile3D.py', + 'Protocols/simulPA.py', + 'Protocols/paint671.py', + 'Protocols/standard671_no532.py', + 'Protocols/standard470.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Protocols') + +py_sources = files( + 'ui/focus_lock_gui.py', + 'ui/intensity_trace.py', + 'ui/AnalysisSettingsUI.py', + 'ui/splashScreen.py', + 'ui/selectCameraPanel.py', + 'ui/HDFSpoolFrame.py', + 'ui/__init__.py', + 'ui/tile_panel.py', + 'ui/intsliders.py', + 'ui/lasersliders.py', + 'ui/multiview_select.py', + 'ui/actionUI.py', + 'ui/mpd_picosecond_delay_panel.py', + 'ui/seqdialog.py', + 'ui/spool_panel.py', + 'ui/voxelSizeDialog.py', + 'ui/positionUI.py', + 'ui/tilesettingsui.py', + 'ui/preflight.py', + 'ui/scanner_panel.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/ui') + +py_sources = files( + 'Utils/pointScanner.py', + 'Utils/__init__.py', + 'Utils/tiler.py', + 'Utils/MultiPointScanner.py', + 'Utils/failsafe.py', + 'Utils/strip_tiler.py', + 'Utils/vibrationAnalysis.py', + 'Utils/fastTiler.py', + 'Utils/sarcSpacing.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/Utils') + +py_sources = files( + 'webui/__init__.py', + 'webui/ipy.py', + + +) +py.install_sources(py_sources, subdir:'PYME/Acquire/webui') +webui_static_files = files( + 'webui/static/css/pyme-bootstrap.css', + 'webui/static/css/pymeacquire.css', + 'webui/static/js/pymeacquire.js', + 'webui/static/js/pzf.js', +) +py.install_sources(webui_static_files, subdir:'PYME/Acquire', preserve_path: true) + +webui_template_files = files( + 'webui/templates/PYMEAcquire.html', + 'webui/templates/login.html', + 'webui/templates/xtermpage.html', +) +py.install_sources(webui_template_files, subdir:'PYME/Acquire', preserve_path: true) + +py_sources = files( + 'htsms/tweeter.py', + 'htsms/__init__.py', + 'htsms/rule_ui_v2.py', + 'htsms/rule_ui.py', +) +py.install_sources(py_sources, subdir:'PYME/Acquire/htsms') + +scripts_files = files( + 'Scripts/init.py', + 'Scripts/init_N1_Ti_Exeter_ZIx.py', + 'Scripts/init_N2_Ti_Zyla_lasers.py', + 'Scripts/init_Neo.py', + 'Scripts/init_NeoSim.py', + 'Scripts/init_TIRF.py', + 'Scripts/init_TIRF_Neo.py', + 'Scripts/init_TIRF_NeoO.py', + 'Scripts/init_TIRF_onecam.py', + 'Scripts/init_Ti.py', + 'Scripts/init_UOA_n.py', + 'Scripts/init_UOA_n2.py', + 'Scripts/init_Y1.py', + 'Scripts/init_Zyla.py', + 'Scripts/init_drift_tracking.py', + 'Scripts/init_emccd_basic.py', + 'Scripts/init_htsms.py', + 'Scripts/init_htsms_focus_lock.py', + 'Scripts/init_micrpi.py', + 'Scripts/init_orca.py', + 'Scripts/init_pco.py', + 'Scripts/init_rev.py', + 'Scripts/init_sim100.py', + 'Scripts/init_sim103.py', + 'Scripts/init_sim2.py', + 'Scripts/init_sim50.py', + 'Scripts/init_sim75.py', + 'Scripts/init_sim_drift_tracking.py', + 'Scripts/init_sim_htsms.py', + 'Scripts/init_sim_htsms_n.py', + 'Scripts/init_sim_main.py', + 'Scripts/init_sim_min.py', + 'Scripts/init_sim_rem.py', + 'Scripts/init_smi1.py', + 'Scripts/init_spectro.py', + 'Scripts/init_spim.py', + 'Scripts/init_thorcam.py', + 'Scripts/init_twophoton.py', + 'Scripts/init_uc480.py', + 'Scripts/init_ueye.py', +) +py.install_sources(scripts_files, subdir:'PYME/Acquire/Scripts') diff --git a/PYME/Acquire/microscope.py b/PYME/Acquire/microscope.py index 6371e5704..1fcfa1278 100755 --- a/PYME/Acquire/microscope.py +++ b/PYME/Acquire/microscope.py @@ -142,6 +142,15 @@ def __setitem__(self, key, value): # raise KeyError('No handler registered for %s' % key) # #return handler.setState(value) + + def get(self, key, default=None): + try: + return self[key] + except KeyError: + if default is not None: + return default + else: + raise def setItem(self, key, value, stopCamera=False, force=False): """ Set the value of one of our hardware components @@ -268,7 +277,7 @@ def registerHandler(self, key, getFcn = None, setFcn=None, needCamRestart = Fals key : string The hardware key - e.g. "Positioning.x", or "Lasers.405.Power". This - will also be how the hardware state is recorder in the metadata. + will also be how the hardware state is recorded in the metadata. getFcn : function The function to call to get the value of the parameter. Should take one parameter which is the value to get @@ -312,7 +321,7 @@ def registerChangeListener(self, key, callback): -class microscope(object): +class Microscope(object): """ Attributes @@ -398,6 +407,18 @@ def centre_roi_on(self, x, y): dx, dy = self.get_roi_offset() self.SetPos(x=(x-dx), y=(y-dy)) + + def get_roi_centre(self): + """Convenience function to get the centre of the ROI + + exists to allow both automatically and manually specified actions to use the same code + """ + dx, dy = self.get_roi_offset() + pos = self.GetPos() + + return pos['x'] + dx, pos['y'] + dy + + def GetPosRange(self): #Todo - fix to use positioning @@ -439,7 +460,7 @@ def _OpenSettingsDB(self): conn.execute("CREATE TABLE StartupTimes (component TEXT, time REAL)") conn.execute("INSERT INTO StartupTimes VALUES ('total', 5)") - def GetPixelSize(self): + def GetPixelSize(self,cam=None): """Get the (sample space) pixel size for the current camera Returns @@ -448,18 +469,21 @@ def GetPixelSize(self): pixelsize : tuple the pixel size in the x and y axes, in um """ + + if cam is None: + cam = self.cam with self.settingsDB as conn: - currVoxelSizeID = conn.execute("SELECT sizeID FROM VoxelSizeHistory2 WHERE camSerial=? ORDER BY time DESC", (self.cam.GetSerialNumber(),)).fetchone() + currVoxelSizeID = conn.execute("SELECT sizeID FROM VoxelSizeHistory2 WHERE camSerial=? ORDER BY time DESC", (cam.GetSerialNumber(),)).fetchone() if not currVoxelSizeID is None: voxx, voxy = conn.execute("SELECT x,y FROM VoxelSizes WHERE ID=?", currVoxelSizeID).fetchone() - return voxx*self.cam.GetHorizontalBin(), voxy*self.cam.GetVerticalBin() - elif self.cam.__class__.__name__ == 'FakeCamera': + return voxx*cam.GetHorizontalBin(), voxy*cam.GetVerticalBin() + elif hasattr(cam, 'XVals'): # change to looking for attribute so that wrapped (e.g. multiview) cameras still work # read voxel size from directly from our simulated camera logger.info('Reading voxel size directly from simulated camera') - vx_um = (self.cam.XVals[1] - self.cam.XVals[0]) / 1.0e3 - vy_um = (self.cam.YVals[1] - self.cam.YVals[0]) / 1.0e3 - return vx_um * self.cam.GetHorizontalBin(), vy_um * self.cam.GetVerticalBin() + vx_um = float(cam.XVals[1] - cam.XVals[0]) / 1.0e3 + vy_um = float(cam.YVals[1] - cam.YVals[0]) / 1.0e3 + return vx_um * cam.GetHorizontalBin(), vy_um * cam.GetVerticalBin() def GenStartMetadata(self, mdh): @@ -1002,11 +1026,12 @@ def get_roi_offset(self): offsets in um """ + from PYME.Acquire.Hardware.multiview import MultiviewWrapper from PYME.Acquire.Hardware.Camera import MultiviewCameraMixin x0, y0, _, _ = self.state['Camera.ROI'] - if isinstance(self.cam, MultiviewCameraMixin): + if isinstance(self.cam, (MultiviewWrapper, MultiviewCameraMixin)): # fix multiview crazyness if self.cam.multiview_enabled: #always use the 0th ROI for determining relative position, regardless of which ROIs are active diff --git a/tests/__init__.py b/PYME/Acquire/misc/__init__.py similarity index 100% rename from tests/__init__.py rename to PYME/Acquire/misc/__init__.py diff --git a/PYME/misc/tempLogger.py b/PYME/Acquire/misc/tempLogger.py similarity index 100% rename from PYME/misc/tempLogger.py rename to PYME/Acquire/misc/tempLogger.py diff --git a/PYME/misc/tempLoggerDB.py b/PYME/Acquire/misc/tempLoggerDB.py similarity index 98% rename from PYME/misc/tempLoggerDB.py rename to PYME/Acquire/misc/tempLoggerDB.py index dd79122f8..00cc5d16d 100755 --- a/PYME/misc/tempLoggerDB.py +++ b/PYME/Acquire/misc/tempLoggerDB.py @@ -23,7 +23,7 @@ #!/usr/bin/python from PYME.Acquire.Hardware.DigiData.DigiDataClient import getDDClient import time -import tempDB +from PYME.misc import tempDB import subprocess if __name__ == '__main__': diff --git a/PYME/misc/tempMonitor.py b/PYME/Acquire/misc/tempMonitor.py similarity index 100% rename from PYME/misc/tempMonitor.py rename to PYME/Acquire/misc/tempMonitor.py diff --git a/PYME/Acquire/positionTracker.py b/PYME/Acquire/positionTracker.py index 171f0b1f8..738f8070d 100644 --- a/PYME/Acquire/positionTracker.py +++ b/PYME/Acquire/positionTracker.py @@ -107,7 +107,7 @@ def __init__(self, scope, time1, viewsize=25., nPixels=500): self.im = np.zeros([nPixels, nPixels]) self.ps = viewsize/nPixels - time1.WantNotification.append(self.Tick) + time1.register_callback(self.Tick) def Tick(self): positions = np.zeros(len(self.scope.piezos) + 2) diff --git a/PYME/Acquire/protocol.py b/PYME/Acquire/protocol.py index 056d6fc77..04e847001 100644 --- a/PYME/Acquire/protocol.py +++ b/PYME/Acquire/protocol.py @@ -29,6 +29,10 @@ from PYME import config from sys import maxsize as maxint +# more semantically explicit synonym for maxint +# implying that tasks with a `when` property of maxint are excecuted upon finishing the protocol +# e.g. after the stop button is pressed or an explicit stop task was executed +on_finish = maxint #minimal protocol which does nothing class Protocol: @@ -136,12 +140,20 @@ def OnFrame(self, frameNum): self.listPos += 1 def OnFinish(self): + skipped = 0 + term_tasks = 0 while not self.listPos >= len(self.taskList): t = self.taskList[self.listPos] self.listPos += 1 + if t.when < maxint-100: # we assume that all proper termination tasks have a when-time very close to or equal to maxint + skipped += 1 + continue # discard all non-termination tasks t.what(*t.params) + term_tasks += 1 eventLog.logEvent('ProtocolTask', '%s, ' % ( t.what.__name__,) + ', '.join([str(p) for p in t.params])) - + + if skipped > 0: + logger.warning("Protocol terminating: skipped %d tasks, completed %d termination tasks" % (skipped,term_tasks)) @@ -176,11 +188,11 @@ def __init__(self, taskList, startFrame, dwellTime, metadataEntries=[], prefligh """ # add a check to ensure that dwell times are sensible - preflightList.append(C('(self.dwellTime*scope.cam.GetIntegTime() > .1) or not scope.cam.contMode', + pf = list(preflightList) + pf.append(C('(self.dwellTime*scope.cam.GetIntegTime() > .1) or not scope.cam.contMode', 'Z step dwell time too short - increase either dwell time or integration time, or set camera mode to single shot / software triggered')) - TaskListProtocol.__init__(self, taskList, metadataEntries, preflightList, - filename) + TaskListProtocol.__init__(self, taskList, metadataEntries, preflightList=pf,filename=filename) self.startFrame = startFrame self.dwellTime = dwellTime @@ -194,9 +206,11 @@ def __init__(self, taskList, startFrame, dwellTime, metadataEntries=[], prefligh self.require_camera_restart = require_camera_restart def Init(self, spooler): - self.zPoss = np.arange(scope.stackSettings.GetStartPos(), - scope.stackSettings.GetEndPos() + .95 * scope.stackSettings.GetStepSize(), - scope.stackSettings.GetStepSize() * scope.stackSettings.GetDirection()) + stack_settings = getattr(spooler, 'stack_settings', scope.stackSettings) + + self.zPoss = np.arange(stack_settings.GetStartPos(), + stack_settings.GetEndPos() + .95 * stack_settings.GetStepSize(), + stack_settings.GetStepSize() * stack_settings.GetDirection()) if self.slice_order != 'saw': if self.slice_order == 'random': @@ -204,13 +218,13 @@ def Init(self, spooler): elif self.slice_order == 'triangle': if len(self.zPoss) % 2: # odd - self.zPoss = np.concatenate([self.zPoss[1::2], self.zPoss[::-2]]) + self.zPoss = np.concatenate([self.zPoss[::2], self.zPoss[-2::-2]]) else: # even self.zPoss = np.concatenate([self.zPoss[::2], self.zPoss[-1::-2]]) - self.piezoName = 'Positioning.%s' % scope.stackSettings.GetScanChannel() + self.piezoName = 'Positioning.%s' % stack_settings.GetScanChannel() self.startPos = scope.state[self.piezoName + '_target'] #FIXME - _target positions shouldn't be part of scope state self.pos = 0 diff --git a/PYME/Acquire/protocol_acquisition.py b/PYME/Acquire/protocol_acquisition.py new file mode 100644 index 000000000..051bbf738 --- /dev/null +++ b/PYME/Acquire/protocol_acquisition.py @@ -0,0 +1,404 @@ +# protocol_acquisition.py + +# replaces Spooler.py and the various backend-specific spoolers, factoring out everything which is specific to +# asynchronous protocol based acquisition. +# +# Protocol based asynchronous acquisition should be used where it is important that the camera runs as fast as possible. +# To allow this, we do not wait for various hardware events to complete, but rather just record timestamps ("Events") and work things +# out in post-processing. + + +import datetime +import time +import os +import uuid +import sys + +from PYME.contrib import dispatch + +from PYME import config +from PYME.IO import MetaDataHandler +from PYME.IO import acquisition_backends +from PYME.IO.events import HDFEventLogger, MemoryEventLogger + +from PYME.Acquire import eventLog +from PYME.Acquire import protocol as p +from PYME.Acquire.acquisition_base import AcquisitionBase + + +try: + from PYME.Acquire import sampleInformation +except: + sampleInformation= None + + +import logging +logger = logging.getLogger(__name__) + + + +def getReducedFilename(filename): + #rname = filename[len(nameUtils.datadir):] + + sname = '/'.join(filename.split(os.path.sep)) + if sname.startswith('/'): + sname = sname[1:] + + return sname + + +class ProtocolAcquisition(AcquisitionBase): + """Spooler base class""" + def __init__(self, filename, frameSource, frame_wrangler, protocol = p.NullProtocol, + maxFrames = p.maxint, backend='hdf', backend_kwargs={}, **kwargs): + """Create a new spooler. + + Parameters + ---------- + scope : PYME.Acquire.microscope.Microscope object + The microscope providing the data + filename : string + The file into which to spool + frameSource : dispatch.Signal object + A source of frames we can subscribe to. It should implement a "connect" + method allowing us to register a callback and then call the callback with + the frame data in a "frameData" kwarg. + frame_wrangler : PYME.IO.FrameWrangler object + The frame wrangler object to use for spooling - used to stop and start while we update settings + protocol : PYME.Acquire.protocol.TaskListProtocol object + The acquisition protocol + guiUpdateCallback : function + a function to call when the spooling GUI needs updating + + """ + AcquisitionBase.__init__(self) + + self.filename=filename + self.frameSource = frameSource + self._frame_wrangler = frame_wrangler + self.seriesName = getReducedFilename(filename) + + self.protocol = protocol + + self.maxFrames = maxFrames + + stack_settings = kwargs.get('stack_settings', None) + if stack_settings: + # only record stack settings if provided (letting protocol fall through to global stack settings, + # if not provided / None) + self.stack_settings = stack_settings + + + self.spoolOn = False + self.frame_num = 0 + + self.spool_complete = False + + self._spooler_uuid = uuid.uuid4() + + self._last_gui_update = 0 + + self._create_backend(backend_type=backend, **backend_kwargs) + + @classmethod + def from_spool_settings(cls, scope, settings, backend, backend_kwargs={}, series_name=None, spool_controller=None): + '''Create an XYZTCAcquisition object from a spool_controller settings object''' + from PYME.IO import acquisition_backends + + if isinstance(backend, acquisition_backends.MemoryBackend): + # TODO - make this softer and allow memory backend for fixed length protocol acquisitions??? + raise RuntimeError('Memory spooling not supported for protocol-based acquisitions') + + protocol = spool_controller.protocol_settings.get_protocol_for_acquisition(settings=settings) + + preflight_mode = settings.get('preflight_mode', 'interactive') + if (preflight_mode != 'skip'): + from PYME.Acquire.ui import preflight + if not preflight.ShowPreflightResults(protocol.PreflightCheck(), preflight_mode): + logger.debug('Bailing from preflight check') + return None #bail if we failed the pre flight check, and the user didn't choose to continue + + #fix timing when using fake camera + #TODO - move logic into backend? + if scope.cam.__class__.__name__ == 'FakeCamera': + backend_kwargs['spoof_timestamps'] = True + backend_kwargs['cycle_time'] = scope.cam.GetIntegTime() + + #logger.info('Creating spooler for %s' % series_name) + return cls(filename=series_name, + frameSource=scope.frameWrangler.onFrame, + frame_wrangler=scope.frameWrangler, + protocol=protocol, + maxFrames=settings.get('max_frames', sys.maxsize), + stack_settings=settings.get('stack_settings', None), + backend=backend, backend_kwargs=backend_kwargs,) + + @classmethod + def get_frozen_settings(cls, scope, spool_controller=None): + settings = {'z_stepped' : spool_controller.protocol_settings.z_stepped, + 'z_dwell' : spool_controller.protocol_settings.z_dwell,} + + if not spool_controller.protocol_settings.protocol in [p.NullProtocol, p.NullZProtocol]: + settings['protocol_name'] = spool_controller.protocol_settings.protocol.filename + + return settings + + + def _create_backend(self, backend_type=acquisition_backends.HDFBackend, **kwargs): + logger.debug('Creating backend of type %s' % backend_type) + kwargs = kwargs.copy() + + if backend_type in ['cluster', 'Cluster', acquisition_backends.ClusterBackend]: + self._aggregate_h5 = kwargs.pop('cluster_h5', False) + + self.clusterFilter = kwargs.pop('serverfilter', config.get('dataserver-filter', '')) + + chunk_size = config.get('httpspooler-chunksize', 50) + + def dist_fcn(n_servers, i=None): + if i is None: + # distribute at random + import random + return random.randrange(n_servers) + + return int(i/chunk_size) % n_servers + + + self._backend = acquisition_backends.ClusterBackend(self.seriesName, + distribution_fcn=dist_fcn, + compression_settings=kwargs.pop('compression_settings', {}), + cluster_h5=self._aggregate_h5, + serverfilter=self.clusterFilter, + shape=[-1,-1,1,-1,1], #spooled aquisitions are time series (for now) + **kwargs) + + + elif backend_type in ['file', 'File', 'hdf', acquisition_backends.HDFBackend]: # assume hdf + self._backend = acquisition_backends.HDFBackend(self.filename, complevel=kwargs.pop('complevel', 6), complib=kwargs.pop('complib','zlib'), + shape=[-1,-1,1,-1,1], # spooled series are time-series (for now) + **kwargs) + else: + self._backend = backend_type(self.filename, **kwargs) + + self._stopping = False + + @property + def md(self): + return self._backend.mdh + + def StartSpool(self): + from PYME import pyme_warnings as warnings + warnings.warn('StartSpool is deprecated. Use start instead', DeprecationWarning) + self.start() + + def StopSpool(self): + from PYME import pyme_warnings as warnings + warnings.warn('StopSpool is deprecated. Use stop instead', DeprecationWarning) + self.stop() + + def start(self): + """ Perform protocol 'frame -1' tasks, log start metadata, then connect + to the frame source. + """ + with self._frame_wrangler.spooling_stopped(): + # stop the frameWrangler before we start spooling + # this serves to ensure that a) we don't accidentally spool frames which were in the camera buffer when we hit start + # and b) we get a nice clean timestamp for when the actual frames start (after any protocol init tasks) + # it might also slightly improve performance. + self.watchingFrames = True + eventLog.register_event_handler(self._backend.event_logger) + + self.frame_num = 0 + + # set tStart here for simulator so that events in init phase get time stamps. Real start time is set below + # **after** protocol.Init() call + self.tStart = time.time() + + self.protocol.Init(self) + + # record start time when we start receiving frames. + self.tStart = time.time() + self._collect_start_metadata() + self.frameSource.connect(self.on_frame, dispatch_uid=self._spooler_uuid) + + self.spoolOn = True + + logger.debug('Starting spooling: %s' %self.seriesName) + + self._backend.initialise() + + def stop(self): + #try: + logger.debug('Disconnecting from frame source') + self.frameSource.disconnect(self.on_frame, dispatch_uid=self._spooler_uuid) + logger.debug('Frame source should be disconnected') + + #there is a race condition on disconnect - ignore any additional frames + self.watchingFrames = False + + #except: + # pass + + try: + self.protocol.OnFinish()#this may still cause events + self.FlushBuffer() + self._collect_stop_metadata() + except: + import traceback + traceback.print_exc() + + try: + eventLog.remove_event_handler(self._backend.event_logger) + except ValueError: + pass + + self.spoolOn = False + + self.on_progress.send(self) + + self._stopping=True + self.finalise() + self.on_stop.send(self) + self.spool_complete = True + + def finalise(self): + """ + Over-ride in derived classes to do any spooler specific tidy up - e.g. sending events to server + + """ + self._backend.finalise() + + + def abort(self): + """ + Tidy up if something goes horribly wrong. Disconnects frame source and event logger and then calls cleanup() + + """ + #there is a race condition on disconnect - ignore any additional frames + self.watchingFrames = False + + try: + logger.debug('Disconnecting from frame source') + self.frameSource.disconnect(self.OnFrame, dispatch_uid=self._spooler_uuid) + logger.debug('Frame source should be disconnected') + except: + logger.exception('Error disconnecting frame source') + + + try: + eventLog.remove_event_handler(self._backend.event_logger) + except ValueError: + pass + + self.spoolOn = False + self.on_stop.send(self) + + + def on_frame(self, sender, frameData, **kwargs): + """Callback which should be called on every frame""" + if not self.watchingFrames: + #we have allready disconnected - ignore any new frames + return + + self._backend.store_frame(self.frame_num, frameData) + + t = time.time() + + self.frame_num += 1 + + if (t > (self._last_gui_update +.1)): + self._last_gui_update = t + self.on_progress.send(self) + + try: + import wx #FIXME - shouldn't do this here + wx.CallAfter(self.protocol.OnFrame, self.frame_num) + #FIXME - The GUI logic shouldn't be here (really needs to change at the level of the protocol and/or general structure of PYMEAcquire + except (ImportError, AssertionError): # handle if spooler doesn't have a GUI + self.protocol.OnFrame(self.frame_num) #FIXME - This will most likely fail for anything but a NullProtocol + + if self.frame_num == 2 and sampleInformation and sampleInformation.currentSlide[0]: #have first frame and should thus have an imageID + sampleInformation.createImage(self.md, sampleInformation.currentSlide[0]) + + if self.frame_num >= self.maxFrames: + self.stop() + + + def _collect_start_metadata(self): + """Record pertinant information to metadata at start of acquisition. + + Loops through all registered sources of start metadata and adds their entries. + + See Also + -------- + PYME.IO.MetaDataHandler + """ + dt = datetime.datetime.now() + + self.dtStart = dt + + #self.tStart = time.time() + + # create an in-memory metadata handler and populate this prior to copying data over to the spooler + # metadata handler. This significantly improves performance if the spooler metadata handler has high latency + # (as is the case for both the HDFMetaDataHandler and, especially, the QueueMetaDataHandler). + mdt = MetaDataHandler.NestedClassMDHandler() + mdt.setEntry('StartTime', self.tStart) + + #loop over all providers of metadata + for mdgen in MetaDataHandler.provideStartMetadata: + mdgen(mdt) + + self.md.copyEntriesFrom(mdt) + + + def _collect_stop_metadata(self): + """Record information to metadata at end of acquisition""" + self.md.setEntry('EndTime', time.time()) + + #loop over all providers of metadata + for mdgen in MetaDataHandler.provideStopMetadata: + mdgen(self.md) + + # def _fake_time(self): + # """Generate a fake timestamp for use with the simulator where the camera + # cycle time does not match the actual time elapsed to generate the frame""" + # #return self.tStart + self.frame_num*self.scope.cam.GetIntegTime() + # return self.tStart + self.frame_num*self._fakeCamCycleTime + + # @property + # def _time_fcn(self): + # if self._fakeCamCycleTime: + # return self._fake_time + # else: + # return time.time + + def FlushBuffer(self): + pass + + def status(self): + return {'spooling' : self.spoolOn, + 'frames_spooled' : self.frame_num, + 'spool_complete' : self.spool_complete,} + + def cleanup(self): + """ over-ride to do any cleanup""" + del self._backend + + def finished(self): + """ over-ride in derived classes to indicate when buffers flushed""" + + # FIXME - this probably needs a bit more work. + # FIXME - delegate to backend? + return self._stopping + + def get_n_frames(self): + return self.frame_num + + def __del__(self): + if self.spoolOn: + self.StopSpool() + + + + diff --git a/PYME/Acquire/sampleInformation.py b/PYME/Acquire/sampleInformation.py index 3c8e4fbe3..48657f4d4 100644 --- a/PYME/Acquire/sampleInformation.py +++ b/PYME/Acquire/sampleInformation.py @@ -274,7 +274,7 @@ def __init__(self, parent, init_filter=('', '', ''), acquiring=True): if acquiring and self.lSlides.GetItemCount() == 1: self.lSlides.Select(0) - print('foo;') + #print('foo;') def OnAddSlide(self, event): import webbrowser @@ -288,13 +288,13 @@ def OnAddSlide(self, event): def OnSelectSlide(self, event): i = event.GetIndex() - print(i) + #print(i) #self.slide = self.lSlides.qs[i] #r = requests.get(('http://%s/api/get_slide_info?creator=%s&reference=%s&structure=%s&index=%d'%(dbhost, self.creator, self.reference, self.structure, i)).encode()) #resp = r.json() resp = self.lSlides._getSlideInfo(i) self.slide = resp['info'] - print((self.slide)) + #print((self.slide)) self.bOK.Enable() def OnFilterChange(self, event): @@ -469,10 +469,10 @@ def prefillSampleData(parent): if dlg.ShowModal() == wx.ID_OK: dlg.PopulateMetadata(slideMD, False) - print('bar') - print((dlg.slide)) + #print('bar') + #print((dlg.slide)) currentSlide[0] = dlg.slide - print(currentSlide) + #print(currentSlide) else: currentSlide[0] = None @@ -482,7 +482,7 @@ def prefillSampleData(parent): def getSampleData(parent, mdh): #global currentSlide - print(('currSlide:', currentSlide)) + #print(('currSlide:', currentSlide)) cs = currentSlide[0] if cs: @@ -493,7 +493,7 @@ def getSampleData(parent, mdh): dlg = SampleInfoDialog(parent) if dlg.ShowModal() == wx.ID_OK: - print('populating metadata') + #print('populating metadata') dlg.PopulateMetadata(mdh) currentSlide[0] = dlg.slide diff --git a/PYME/Acquire/sampleInformationDjangoDirect.py b/PYME/Acquire/sampleInformationDjangoDirect.py index f577fa4a9..69a19917b 100644 --- a/PYME/Acquire/sampleInformationDjangoDirect.py +++ b/PYME/Acquire/sampleInformationDjangoDirect.py @@ -29,8 +29,8 @@ from PYME.IO.FileUtils import nameUtils from PYME.contrib import TextCtrlAutoComplete -from PYME.SampleDB2 import populate #just to setup the Django environment -from PYME.SampleDB2.samples import models +from SampleDB2 import populate #just to setup the Django environment +from SampleDB2.samples import models lastCreator = nameUtils.getUsername() lastSlideRef = '' diff --git a/PYME/Acquire/setup.py b/PYME/Acquire/setup.py index 6c46d80e5..d2a46fa5c 100755 --- a/PYME/Acquire/setup.py +++ b/PYME/Acquire/setup.py @@ -32,6 +32,8 @@ def configuration(parent_package = '', top_path = None): config.add_data_dir('Scripts') config.add_subpackage('ui') config.add_subpackage('Utils') + config.add_subpackage('webui') + config.add_subpackage('htsms') #config.add_data_files('logo.png') return config diff --git a/PYME/Acquire/stackSettings.py b/PYME/Acquire/stackSettings.py index 113044aad..c95e127ad 100644 --- a/PYME/Acquire/stackSettings.py +++ b/PYME/Acquire/stackSettings.py @@ -25,10 +25,13 @@ import time from PYME.IO import MetaDataHandler +from PYME.util import webframework import logging logger = logging.getLogger(__name__) +import threading + class StackSettings(object): """A class to keep settings for acquiring z-stacks""" # 'Constants' @@ -38,24 +41,102 @@ class StackSettings(object): START_AND_END = 1 FORWARDS = 1 BACKWARDS = -1 + + SCAN_MODES = ['Middle and Number', 'Start and End'] + + DEFAULTS = { + 'StartPos': 0, + 'EndPos': 0, + 'StepSize': 0.2, + 'NumSlices': 100, + 'ScanMode': SCAN_MODES[0], + 'ScanPiezo': 'z', + 'DwellFrames': -1, + } - def __init__(self,scope): + def __init__(self,scope, **kwargs): + """ + Create a stack settings object. + + NB - extra args map 1:1 to stack metadata entries. Start and end pos are ignored if ScanMode = 'Middle and Number' + + Parameters + ---------- + scope + ScanMode + StartPos + EndPos + StepSize + NumSlices + ScanPiezo + """ #PreviewAquisator.__init__(self, chans, cam, shutters, None) self.scope= scope #self.log = _log self.mdh = MetaDataHandler.NestedClassMDHandler() #register as a provider of metadata MetaDataHandler.provideStartMetadata.append(self.ProvideStackMetadata) + + self._settings_changed = threading.Condition() - self.ScanChan = 'z' - self.StartMode = self.CENTRE_AND_LENGTH - self.SeqLength = 100 - self.StepSize = 0.2 - self.startPos = 0 - self.endPos = 0 + d1 = dict(self.DEFAULTS) + d1.update(kwargs) + + self.update(**d1) self.direction = self.FORWARDS + try: + from PYME.Acquire import webui + # add webui endpoints (if running under webui) + webui.add_endpoints(self, '/stack_settings') + except IndexError: + logger.exception('Error loading webui - use a development install if you need webui') + + + def update(self, ScanMode=None, StartPos=None, EndPos=None, StepSize=None, NumSlices=None, ScanPiezo=None, DwellFrames=None): + if ScanPiezo is not None: + self.ScanChan = ScanPiezo + if ScanMode is not None: + self.StartMode = self.SCAN_MODES.index(ScanMode) + if NumSlices is not None: + self.SeqLength = int(NumSlices) + if StepSize is not None: + self.StepSize = float(StepSize) + if StartPos is not None: + self.startPos = float(StartPos) + if EndPos is not None: + self.endPos = float(EndPos) + if DwellFrames is not None: + self._dwell_frames = float(DwellFrames) + + with self._settings_changed: + self._settings_changed.notify_all() + + @webframework.register_endpoint('/update', output_is_json=False) + def update_json(self, body): + import json + self.update(**json.loads(body)) + + @webframework.register_endpoint('/settings', output_is_json=False) + def settings(self): + return { + 'StartPos' : self.GetStartPos(), + 'EndPos' : self.GetEndPos(), + 'StepSize': self.GetStepSize(), + 'NumSlices' : self.GetSeqLength(), + 'ScanMode': self.SCAN_MODES[self.GetStartMode()], + 'ScanPiezo' : self.GetScanChannel(), + 'DwellFrames' : self._dwell_frames, + } + + @webframework.register_endpoint('/settings_longpoll', output_is_json=False) + def settings_longpoll(self): + with self._settings_changed: + self._settings_changed.wait() + + return self.settings() + def GetScanChannel(self): return self.ScanChan @@ -66,9 +147,13 @@ def piezoGoHome(self): def SetScanChannel(self,iMode): self.ScanChan = iMode + with self._settings_changed: + self._settings_changed.notify_all() def SetSeqLength(self,iLength): - self.SeqLength = iLength + self.SeqLength = int(iLength) + with self._settings_changed: + self._settings_changed.notify_all() def GetSeqLength(self): if (self.StartMode == 0): @@ -78,15 +163,24 @@ def GetSeqLength(self): def SetStartMode(self, iMode): self.StartMode = iMode + with self._settings_changed: + self._settings_changed.notify_all() def GetStartMode(self): return self.StartMode + def SetStepSize(self, fSize): - self.StepSize = fSize + self.StepSize = float(fSize) + with self._settings_changed: + self._settings_changed.notify_all() + def GetStepSize(self): return self.StepSize + def SetStartPos(self, sPos): - self.startPos = sPos + self.startPos = float(sPos) + with self._settings_changed: + self._settings_changed.notify_all() def GetStartPos(self): if (self.GetStartMode() == 0): @@ -97,7 +191,10 @@ def GetStartPos(self): return self.startPos def SetEndPos(self, ePos): - self.endPos = ePos + self.endPos = float(ePos) + with self._settings_changed: + self._settings_changed.notify_all() + def GetEndPos(self): if (self.GetStartMode() == 0): return self._CurPos() + (self.GetStepSize()*(self.GetSeqLength() - 1)*self.GetDirection()/2) @@ -105,15 +202,20 @@ def GetEndPos(self): if not ("endPos" in dir(self)): raise RuntimeError("Please call SetEndPos first !!") return self.endPos + def SetPrevPos(self, sPos): self.prevPos = sPos + def GetPrevPos(self): if not ("prevPos" in dir(self)): raise RuntimeError("Please call SetPrevPos first !!") return self.prevPos + def SetDirection(self, dir): " Fowards = 1, backwards = -1 " self.direction = dir + with self._settings_changed: + self._settings_changed.notify_all() def GetDirection(self): if (self.GetStartMode() == 0): @@ -162,7 +264,7 @@ def ProvideStackMetadata(self, mdh): mdh.setEntry('StackSettings.EndPos', self.GetEndPos()) mdh.setEntry('StackSettings.StepSize', self.GetStepSize()) mdh.setEntry('StackSettings.NumSlices', self.GetSeqLength()) - mdh.setEntry('StackSettings.ScanMode', ['Middle and Number', 'Start and End'][self.GetStartMode()]) + mdh.setEntry('StackSettings.ScanMode', self.SCAN_MODES[self.GetStartMode()]) mdh.setEntry('StackSettings.ScanPiezo', self.GetScanChannel()) mdh.setEntry('voxelsize.z', self.GetStepSize()) diff --git a/PYME/Acquire/stage_leveling.py b/PYME/Acquire/stage_leveling.py index 490db2bb0..732f6f042 100644 --- a/PYME/Acquire/stage_leveling.py +++ b/PYME/Acquire/stage_leveling.py @@ -20,7 +20,7 @@ def __init__(self, scope, offset_piezo, pause_on_relocate=0.25, Parameters ---------- - scope: PYME.Acquire.microscope.microscope + scope: PYME.Acquire.microscope.Microscope offset_piezo: PYME.Acquire.Hardware.offsetPiezoREST.OffsetPiezo pause_on_relocate: float [optional] time to pause during measure loop after moving to a new location and before measuring offset. @@ -28,7 +28,7 @@ def __init__(self, scope, offset_piezo, pause_on_relocate=0.25, Notes ----- - Units are derived from PYME.Acquire.microscope.microscope.GetPos and SetPos and should be in micrometers. + Units are derived from PYME.Acquire.microscope.Microscope.GetPos and SetPos and should be in micrometers. Attributes ---------- @@ -64,7 +64,7 @@ def add_position(self): """ self._positions.append(self._scope.GetPos()) - def add_grid(self, x_length, y_length, x_spacing, y_spacing, center=True): + def add_grid(self, x_length, y_length, x_spacing, y_spacing, center): """ Add a grid of set spacings to the list of positions to scan when measuring offsets. @@ -113,8 +113,27 @@ def add_96wp_positions(self, short='x'): self.add_grid(9000 * 12, 9000 * 8, 9000, 9000, center=False) else: logger.error('short axes must be "x" or "y"') + + def add_ibidi8wellslide_positions(self, short='x'): + """Shortcut for queueing center positions on a ibidi 8 well slide from + minimum x, y well. x (2 well) should be short axis, y (4 well) long + + Parameters + ---------- + short: str + stage dimension of the short axis (2 wells) of the plate. Defaults + to x. + """ + if short=='x': + self.add_grid(11.2 * 1e3 * 2, 12.5 * 1e3 * 4, + 11.2 * 1e3, 12.5 * 1e3, center=False) + elif short=='y': + self.add_grid(12.5 * 1e3 * 4, 11.2 * 1e3 * 2, + 12.5 * 1e3, 11.2 * 1e3, center=False) + else: + logger.error('short axes must be "x" or "y"') - def measure_offsets(self, optimize_path=True): + def measure_offsets(self, optimize_path=True, use_previous_scan=True): """ Visit each position and log the offset @@ -144,8 +163,15 @@ def measure_offsets(self, optimize_path=True): time.sleep(self._pause_on_relocate) if hasattr(self, '_focus_lock') and not self._focus_lock.LockOK(): logger.debug('focus lock not OK, scanning offset') - # self.scan_offset_until_ok() - self._focus_lock.ReacquireLock() + if use_previous_scan: + try: + start_at = self.lookup_offset(positions[ind, 0], + positions[ind, 1]) + except: + start_at = -25 + else: + start_at = -25 + self._focus_lock.ReacquireLock(start_at=start_at) time.sleep(1.) if self._focus_lock.LockOK(): @@ -212,3 +238,48 @@ def plot(self, index=-1, interpolation_factor=50): if len(self._scans) < 1: raise UserWarning('no scans available, call StageLeveler.measure_offsets() first') StageLeveler.plot_scan(self._scans[index], interpolation_factor=interpolation_factor) + + def store_scan(self, index=-1): + self._current_scan = self._scans[index] + + @property + def current_scan(self): + try: + return self._current_scan + except AttributeError: + if len(self._scans) > 0: + return self._scans[-1] + + def lookup_offset(self, x, y, default=0): + """use a stored scan to estimate what the z offset should be at a given + xy position + + Parameters + ---------- + x : float + x position in micrometers + y : float + y position in micrometers + + Returns + ------- + float + offset at xy from interpolated scan + """ + from scipy.interpolate import interp2d + try: + scan = self.current_scan + except (IndexError, ValueError): + logger.error('no scan, returning %f for offset lookup' % default) + return default + f = interp2d(scan['x'], scan['y'], scan['offset']) + return f(x, y)[0] + + def acquire_focus_lock(self): + self._focus_lock.EnableLock() + if self._focus_lock.LockOK(): + return + time.sleep(1) + if not self._focus_lock.LockOK(): + p = self._scope.GetPos() + self._scope.focus_lock.ReacquireLock(self.lookup_offset(p['x'], p['y'])) diff --git a/PYME/Acquire/ui/AnalysisSettingsUI.py b/PYME/Acquire/ui/AnalysisSettingsUI.py index b01240ea9..f8b7c4fd5 100644 --- a/PYME/Acquire/ui/AnalysisSettingsUI.py +++ b/PYME/Acquire/ui/AnalysisSettingsUI.py @@ -8,17 +8,18 @@ from PYME.localization import MetaDataEdit as mde import PYME.localization.FitFactories - +from PYME.ui import manualFoldPanel from PYME.IO import MetaDataHandler - +import logging from PYME.contrib import dispatch +logger = logging.getLogger(__name__) + class AnalysisSettingsPanel(wx.Panel): - def __init__(self, parent, analysisSettings, mdhChangedSignal=None): + def __init__(self, parent, analysisSettings, mdhChangedSignal=None, show_save_mdh=True): wx.Panel.__init__(self, parent, -1) self.analysisSettings = analysisSettings - self.analysisMDH = analysisSettings.analysisMDH self.mdhChangedSignal = mdhChangedSignal self._inChange = False @@ -37,16 +38,21 @@ def __init__(self, parent, analysisSettings, mdhChangedSignal=None): hsizer.Add(self.cFitType, 1, wx.ALIGN_CENTER_VERTICAL, 0) vsizer.Add(hsizer, 0, wx.EXPAND|wx.ALL, 4) - hsizer = wx.BoxSizer(wx.HORIZONTAL) - self.cbLogSettings = wx.CheckBox(self, -1, 'Save analysis settings to metadata') - self.cbLogSettings.SetValue(False) - self.cbLogSettings.Bind(wx.EVT_CHECKBOX, lambda e : self.analysisSettings.SetPropagate(e.IsChecked())) - hsizer.Add(self.cbLogSettings, 1, wx.ALIGN_CENTER_VERTICAL, 0) - vsizer.Add(hsizer, 0, wx.EXPAND|wx.ALL, 4) + if show_save_mdh: + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.cbLogSettings = wx.CheckBox(self, -1, 'Save analysis settings to metadata') + self.cbLogSettings.SetValue(False) + self.cbLogSettings.Bind(wx.EVT_CHECKBOX, lambda e : self.analysisSettings.SetPropagate(e.IsChecked())) + hsizer.Add(self.cbLogSettings, 1, wx.ALIGN_CENTER_VERTICAL, 0) + vsizer.Add(hsizer, 0, wx.EXPAND|wx.ALL, 4) self.SetSizerAndFit(vsizer) self.update() + + @property + def analysisMDH(self): + return self.analysisSettings.analysisMDH def OnFitModuleChanged(self, event): self._inChange = True @@ -86,7 +92,7 @@ def __init__(self, parent, analysisSettings, mdhChangedSignal=None): wx.Panel.__init__(self, parent, -1) self.analysisSettings = analysisSettings - self.analysisMDH = analysisSettings.analysisMDH + #self.analysisMDH = analysisSettings.analysisMDH self.mdhChangedSignal = mdhChangedSignal mdhChangedSignal.connect(self.OnMDChanged) @@ -106,6 +112,10 @@ def __init__(self, parent, analysisSettings, mdhChangedSignal=None): self.SetSizerAndFit(vsizer) + @property + def analysisMDH(self): + return self.analysisSettings.analysisMDH + def _populateStdOptionsPanel(self, pan, vsizer): for param in self.DEFAULT_PARAMS: pg = param.createGUI(pan, self.analysisMDH, syncMdh=True, @@ -118,7 +128,7 @@ def _populateCustomAnalysisPanel(self, pan, vsizer): try: #fitMod = self.fitFactories[self.cFitType.GetSelection()] self._analysisModule = self.analysisMDH['Analysis.FitModule'] - fm = __import__('PYME.localization.FitFactories.' + self._analysisModule, fromlist=['PYME', 'localization', 'FitFactories']) + fm = PYME.localization.FitFactories.import_fit_factory(self._analysisModule) #vsizer = wx.BoxSizer(wx.VERTICAL) for param in fm.PARAMETERS: @@ -129,7 +139,7 @@ def _populateCustomAnalysisPanel(self, pan, vsizer): self.Layout() self.SetMinSize([200, self.GetBestSize()[1]]) self.GetParent().Layout() - print('custom analysis settings populated') + logger.debug('custom analysis settings populated') except (KeyError, AttributeError): pass @@ -143,6 +153,9 @@ def OnMDChanged(self, event=None, sender=None, signal=None, mdh=None): self.GetParent().fold1(self) + + + class AnalysisSettings(object): def __init__(self): self.analysisMDH = MetaDataHandler.NestedClassMDHandler() diff --git a/PYME/Acquire/ui/HDFSpoolFrame.py b/PYME/Acquire/ui/HDFSpoolFrame.py index 4b9ead437..32337ca79 100755 --- a/PYME/Acquire/ui/HDFSpoolFrame.py +++ b/PYME/Acquire/ui/HDFSpoolFrame.py @@ -20,7 +20,7 @@ # along with this program. If not, see . # ################## -"""The GUI controls for streaming acquisiton. +"""The GUI controls for streaming acquisition. """ @@ -43,7 +43,7 @@ wxID_FRSPOOLPANEL1, wxID_FRSPOOLSTATICBOX1, wxID_FRSPOOLSTATICBOX2, wxID_FRSPOOLSTATICTEXT1, wxID_FRSPOOLSTNIMAGES, wxID_FRSPOOLSTSPOOLDIRNAME, wxID_FRSPOOLSTSPOOLINGTO, wxID_FRSPOOLTCSPOOLFILE, -] = [wx.NewId() for _init_ctrls in range(14)] +] = [wx.NewIdRef() for _init_ctrls in range(14)] import PYME.ui.manualFoldPanel as afp @@ -129,7 +129,7 @@ def _spool_to_pan(self): elif (self.spoolController.spoolType == 'Cluster'): self.rbSpoolCluster.SetValue(True) else: - print(self.spoolController.spoolType) + #print(self.spoolController.spoolType) self.rbSpoolFile.SetValue(True) spoolDirSizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) @@ -185,7 +185,7 @@ def _comp_pan(self, clp): self.tQuantizeScale = wx.TextCtrl(pan, -1, '0.5') self.tQuantizeScale.SetToolTip(wx.ToolTip( 'Quantization scale in units of sigma\n. The default of 0.5 will give a quantization interval that is half the std dev. of the expected Poisson noise in a pixel.')) - hsizer.Add(self.tQuantizeScale, 1.0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + hsizer.Add(self.tQuantizeScale, 1, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) @@ -226,7 +226,7 @@ def _spool_pan(self): self.bStartSpool = wx.Button(pan, -1, 'Start', style=wx.BU_EXACTFIT) self.bStartSpool.Bind(wx.EVT_BUTTON, self.OnBStartSpoolButton) - self.bStartSpool.SetDefault() + #self.bStartSpool.SetDefault() hsizer.Add(self.bStartSpool, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) # self.bStartStack = wx.Button(pan,-1,'Z-Series',style=wx.BU_EXACTFIT) @@ -248,7 +248,7 @@ def _spool_pan(self): self.stSpoolingTo = wx.StaticText(self.spoolProgPan, -1, 'Spooling to .....') spoolProgSizer.Add(self.stSpoolingTo, 0, wx.ALL, 0) - self.stNImages = wx.StaticText(self.spoolProgPan, -1, 'NNNNN images spooled in MM minutes') + self.stNImages = wx.StaticText(self.spoolProgPan, -1, 'NNN images spooled in MM mins') self.stSpoolingTo.SetForegroundColour(wx.TheColourDatabase.Find('GREY')) self.stNImages.SetForegroundColour(wx.TheColourDatabase.Find('GREY')) @@ -343,7 +343,7 @@ def __init__(self, parent, scope, **kwargs): self.spoolController.onSpoolProgress.connect(self._tick) self.spoolController.onSpoolStart.connect(self.OnSpoolingStarted) - self.spoolController.onSpoolStop.connect(self.OnSpoolingStopped) + self.spoolController.on_stop.connect(self.OnSpoolingStopped) self.stSpoolDirName.SetLabel(self.spoolController.display_dirname) self.tcSpoolFile.SetValue(self.spoolController.seriesName) @@ -438,7 +438,7 @@ def get_compression_settings(self, ui_message_on_error=True): try: q_scale = float(self.tQuantizeScale.GetValue()) / self.scope.cam.noise_properties['ElectronsPerCount'] except (AttributeError, NotImplementedError): - print("WARNING: Camera doesn't provide electrons per count, using qscale in units of ADUs instead") + logger.warning("Camera doesn't provide electrons per count, using qscale in units of ADUs instead") q_scale = float(self.tQuantizeScale.GetValue()) compSettings = { @@ -472,13 +472,10 @@ def OnBStartSpoolButton(self, event=None, stack=False): try: - self.spoolController.StartSpooling(fn, #stack=stack, #compLevel = compLevel, - #pzf_compression_settings=self.get_compression_settings(), - #cluster_h5=self.cbClusterh5.GetValue() - ) + self.spoolController.start_spooling(fn) except IOError as e: logger.exception('IO error whilst spooling') - ans = wx.MessageBox(str(e.message), 'Error', wx.OK) + ans = wx.MessageBox(str(e.strerror), 'Error', wx.OK) self.tcSpoolFile.SetValue(self.spoolController.seriesName) def update_ui(self): diff --git a/PYME/Acquire/ui/actionUI.py b/PYME/Acquire/ui/actionUI.py index 22a91d9bb..ad594dcbe 100644 --- a/PYME/Acquire/ui/actionUI.py +++ b/PYME/Acquire/ui/actionUI.py @@ -7,6 +7,13 @@ import wx import numpy as np import logging +import time + +from wx.core import LIST_FORMAT_LEFT + +from PYME.Acquire import actions +from PYME.ui import cascading_layout +from PYME.ui import progress logger = logging.getLogger(__name__) @@ -18,32 +25,83 @@ def __init__(self, parent, actionManager, pos=wx.DefaultPosition, self.actionManager = actionManager self.actionManager.onQueueChange.connect(self.update) - self.InsertColumn(0, "Priority") - self.InsertColumn(1, "Function") - self.InsertColumn(2, "Args") + self.InsertColumn(0, "When") + self.InsertColumn(1, "Priority") + self.InsertColumn(2, "Action") + #self.InsertColumn(2, "Args") self.InsertColumn(3, "Expiry") self.InsertColumn(4, 'Max Duration') - self.SetColumnWidth(0, 50) - self.SetColumnWidth(1, 150) - self.SetColumnWidth(2, 450) - self.SetColumnWidth(3, 50) + self.SetColumnWidth(0, 60) + self.SetColumnWidth(1, 50) + self.SetColumnWidth(2, 600) + #self.SetColumnWidth(2, 450) + self.SetColumnWidth(3, 60) self.SetColumnWidth(4, 200) + self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown) + + def OnGetItemText(self, item, col): - vals = self._queueItems[item] + if item < len(self._queueItems): + vals = self._queueItems[item] + if (col==0): + return 'pending' + else: + item -= len(self._queueItems) + when, vals = self._scheduledItems[item] + if (col==0): + return time.strftime('%H:%M:%S', time.localtime(when)) - val = vals[col] - return repr(val) + val = vals[col-1] + + if col == 3: # expiry is a timestamp + return time.strftime('%H:%M:%S', time.localtime(val)) + else: + return repr(val) + + + def OnKeyDown(self, event): + #logger.info('Key down %d' % event.GetKeyCode()) + if event.GetKeyCode() in (wx.WXK_DELETE, wx.WXK_BACK): + self.delete_selected() + else: + event.Skip() + + def delete_selected(self): + logger.info('Deleting selected actions') + to_remove = [] + to_deselect = [] + + idx = self.GetFirstSelected() + + while idx != -1: + if idx < len(self._queueItems): + a =self._queueItems[idx] + else: + idx -= len(self._queueItems) + a = self._scheduledItems[idx] + + to_remove.append(a) + to_deselect.append(idx) + idx = self.GetNextSelected(idx) + + for idx in to_deselect: + self.Select(idx, on=0) + + if to_remove: + self.actionManager.remove_actions(to_remove) def update(self, **kwargs): self._queueItems = list(self.actionManager.actionQueue.queue) - self._queueItems.sort() - self.SetItemCount(len(self._queueItems)) + self._queueItems.sort(key=lambda a : a[0]) + self._scheduledItems = list(self.actionManager.scheduledQueue.queue) + self._scheduledItems.sort(key=lambda a : a[0]) + self.SetItemCount(len(self._queueItems) + len(self._scheduledItems)) self.Refresh() -ACTION_DEFAULTS = ['spoolController.StartSpooling', +ACTION_DEFAULTS = ['spoolController.start_spooling', 'state.update', ] @@ -51,6 +109,8 @@ def update(self, **kwargs): 'None': lambda positions, scope_position: positions, } +ACTION_TYPES = ['MoveTo', 'UpdateState', 'CenterROIOn', 'SpoolSeries', 'RemoteSpoolSeries', 'SimultaneousSpoolSeries', 'FunctionAction'] + try: from PYME.Analysis.points.traveling_salesperson import sort as tspsort #avoid clobbering sort() builtin from PYME.Analysis.points.traveling_salesperson import queue_opt @@ -58,92 +118,653 @@ def update(self, **kwargs): except ImportError: pass -class ActionPanel(wx.Panel): + +class SingleActionPanel(wx.Panel, cascading_layout.CascadingLayoutMixin): + description='An action that does something' + supports_then =True + num_actions = 1 + def __init__(self, parent, actionManager, scope): wx.Panel.__init__(self, parent) self.actionManager = actionManager self.scope = scope - vsizer = wx.BoxSizer(wx.VERTICAL) - self.actionList = ActionList(self, self.actionManager) - vsizer.Add(self.actionList, 1, wx.EXPAND, 0) + self._pan_then = None - hsizer = wx.BoxSizer(wx.HORIZONTAL) - - hsizer.Add(wx.StaticText(self, -1, 'Nice:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.tNice = wx.TextCtrl(self, -1, '10', size=(30, -1)) - hsizer.Add(self.tNice, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + sizer = wx.BoxSizer(wx.VERTICAL) + + self._init_controls(sizer) - hsizer.Add(wx.StaticText(self, -1, 'Function:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.tFunction = wx.ComboBox(self, -1, '', choices=ACTION_DEFAULTS,size=(150, -1)) - hsizer.Add(self.tFunction, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + if self.supports_then: + hsizer = wx.BoxSizer(wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'Args:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.tArgs = wx.TextCtrl(self, -1, '', size=(150, -1)) - hsizer.Add(self.tArgs, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + hsizer.Add(wx.StaticText(self, -1, 'Then:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.cThen = wx.Choice(self, -1, choices=['None', ] + ACTION_TYPES) + self.cThen.Bind(wx.EVT_CHOICE, self.OnThenChanged) + hsizer.Add(self.cThen, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + sizer.Add(hsizer, 0, wx.EXPAND|wx.TOP, 5) + + self._then_sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self._then_sizer, 0, wx.EXPAND|wx.LEFT, 20) + + self.SetSizerAndFit(sizer) + + def _init_controls(self, sizer): + pass + + def OnThenChanged(self, event): + if self._pan_then is not None: + self._pan_then.Destroy() + self._then_sizer.Clear() - hsizer.Add(wx.StaticText(self, -1, 'Timeout [s]:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.tTimeout = wx.TextCtrl(self, -1, '1000000', size=(50, -1)) - hsizer.Add(self.tTimeout, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + then = self.cThen.GetStringSelection() + if then != 'None': + #print('Changing then to %s' % then) + self._pan_then = globals()[then + 'Panel'](self, self.actionManager, self.scope) + self._then_sizer.Add(self._pan_then, 0, wx.EXPAND, 0) + else: + self._pan_then = None - hsizer.Add(wx.StaticText(self, -1, 'Max Duration [s]:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.t_duration = wx.TextCtrl(self, -1, '%.1f' % np.finfo(float).max, size=(50, -1)) - hsizer.Add(self.t_duration, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + #print('re-layouting') + self.cascading_layout() + + def _set_then(self, then): + self.cThen.SetSelection(ACTION_TYPES.index(then)+1) + self.OnThenChanged(None) + + def get_action(self, idx=0): + action = self._get_action(idx) - self.bAdd = wx.Button(self, -1, 'Add', style=wx.BU_EXACTFIT) - self.bAdd.Bind(wx.EVT_BUTTON, self.OnAddAction) - hsizer.Add(self.bAdd, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + if self._pan_then: + return action.then(self._pan_then.get_action(idx)) + else: + return action - vsizer.Add(hsizer, 0, wx.EXPAND, 0) - vsizer.Add(wx.StaticLine(self), 0, wx.EXPAND|wx.ALL, 4) - + def get_actions(self): + return [self.get_action(i) for i in range(self.num_actions)] + + def _get_action(self, idx=0): + """Return an Action object that represents the current state of the panel + + This should be implemented in an action-specific subclass. + """ + raise NotImplementedError('This should be implemented in a subclass') + +class MoveToPanel(SingleActionPanel): + def _init_controls(self, sizer): hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, 'X:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tX = wx.TextCtrl(self, -1, '0', size=(50, -1)) + hsizer.Add(self.tX, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + hsizer.Add(wx.StaticText(self, -1, 'Y:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tY = wx.TextCtrl(self, -1, '0', size=(50, -1)) + hsizer.Add(self.tY, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.bMoveToHere = wx.Button(self, -1, 'Add move to current location') - self.bMoveToHere.Bind(wx.EVT_BUTTON, self.OnAddMove) - hsizer.Add(self.bMoveToHere, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.bSetCurrent = wx.Button(self, -1, 'Use current') + self.bSetCurrent.Bind(wx.EVT_BUTTON, self.OnSetCurrent) + hsizer.Add(self.bSetCurrent, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + sizer.Add(hsizer, 0, wx.EXPAND, 0) - hsizer.AddStretchSpacer() + def OnSetCurrent(self, event): + pos = self.scope.GetPos() + self.tX.SetValue('%.2f' % pos['x']) + self.tY.SetValue('%.2f' % pos['y']) - vsizer.Add(hsizer, 0, wx.EXPAND, 0) - hsizer = wx.BoxSizer(wx.HORIZONTAL) + def _get_action(self, idx=0): + x = float(self.tX.GetValue()) + y = float(self.tY.GetValue()) + return actions.MoveTo(x, y) - self.rbNoSteps = wx.RadioButton(self, -1, '2D', style=wx.RB_GROUP) - hsizer.Add(self.rbNoSteps, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) - self.rbZStepped = wx.RadioButton(self, -1, 'Z stepped') - hsizer.Add(self.rbZStepped, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) +#TODO - move somewhere more sensible +from wx.lib.mixins import listctrl as listctrlMixins + +class VSChoice(wx.Choice): + def SetValue(self, val): + if isinstance(val, str): + self.SetStringSelection(val) + else: + self.SetSelection(val) + + def GetValue(self): + return self.GetStringSelection() + +class DictEditorListCtrl(wx.ListCtrl, listctrlMixins.TextEditMixin, listctrlMixins.ListCtrlAutoWidthMixin): + def __init__(self, *args, **kwargs): + self._keys= list(kwargs.pop('keys', [])) + self._choice_cols = kwargs.pop('choice_columns', []) + wx.ListCtrl.__init__(self, *args, **kwargs) + listctrlMixins.TextEditMixin.__init__(self) + listctrlMixins.ListCtrlAutoWidthMixin.__init__(self) + + def make_choice_editor(self, col_style=wx.LIST_FORMAT_LEFT): + style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB|wx.TE_RICH2 + style |= {wx.LIST_FORMAT_LEFT: wx.TE_LEFT, + wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT, + wx.LIST_FORMAT_CENTRE : wx.TE_CENTRE + }[col_style] + + editor = VSChoice(self, -1, style=style, choices = ['...', ] + self._keys) + editor.SetBackgroundColour(self.editorBgColour) + editor.SetForegroundColour(self.editorFgColour) + font = self.GetFont() + editor.SetFont(font) + + self.curRow = 0 + self.curCol = 0 + + editor.Hide() + if hasattr(self, 'editor'): + self.editor.Destroy() + self.editor = editor + + self.col_style = col_style + self.editor.Bind(wx.EVT_CHAR, self.OnChar) + self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor) + + # def make_editor(self, col_style=wx.LIST_FORMAT_LEFT): + # return self.make_choice_editor(col_style) + + def OpenEditor(self, col, row): + ''' Opens an editor at the current position. ''' + + # give the derived class a chance to Allow/Veto this edit. + evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_BEGIN_LABEL_EDIT, self.GetId()) + evt.Index = row + evt.Column = col + item = self.GetItem(row, col) + evt.Item.SetId(item.GetId()) + evt.Item.SetColumn(item.GetColumn()) + evt.Item.SetData(item.GetData()) + evt.Item.SetText(item.GetText()) + ret = self.GetEventHandler().ProcessEvent(evt) + if ret and not evt.IsAllowed(): + return # user code doesn't allow the edit. - self.rbNoSteps.SetValue(True) - hsizer.AddStretchSpacer() + if col in self._choice_cols: + self.make_choice_editor(self.GetColumn(col).Align) + choice_editor = True + else: + self.make_editor(self.GetColumn(col).Align) + choice_editor = False + + # if self.GetColumn(col).Align != self.col_style: + # self.make_editor(self.GetColumn(col).Align) + + x0 = self.col_locs[col] + x1 = self.col_locs[col+1] - x0 + + scrolloffset = self.GetScrollPos(wx.HORIZONTAL) + + # scroll forward + if x0+x1-scrolloffset > self.GetSize()[0]: + if wx.Platform == "__WXMSW__": + # don't start scrolling unless we really need to + offset = x0+x1-self.GetSize()[0]-scrolloffset + # scroll a bit more than what is minimum required + # so we don't have to scroll everytime the user presses TAB + # which is very tireing to the eye + addoffset = self.GetSize()[0]/4 + # but be careful at the end of the list + if addoffset + scrolloffset < self.GetSize()[0]: + offset += addoffset + + self.ScrollList(offset, 0) + scrolloffset = self.GetScrollPos(wx.HORIZONTAL) + else: + # Since we can not programmatically scroll the ListCtrl + # close the editor so the user can scroll and open the editor + # again + self.editor.SetValue(self.GetItem(row, col).GetText()) + self.curRow = row + self.curCol = col + self.CloseEditor() + return + + y0 = self.GetItemRect(row)[1] + + def _activate_editor(editor): + editor.SetSize(x0-scrolloffset,y0, x1,-1, wx.SIZE_USE_EXISTING) + editor.SetValue(self.GetItem(row, col).GetText()) + editor.Show() + editor.Raise() + if not choice_editor: + editor.SetSelection(-1,-1) + editor.SetFocus() + + wx.CallAfter(_activate_editor, self.editor) + + self.curRow = row + self.curCol = col + +class UpdateStatePanel(SingleActionPanel): + def _init_controls(self, sizer): + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, 'State:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + #self.tState = wx.TextCtrl(self, -1, '', size=(150, -1)) + #hsizer.Add(self.tState, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.lState = DictEditorListCtrl(self, -1, style=wx.LC_REPORT|wx.LC_EDIT_LABELS,size=(-1, 100), + keys=self.scope.state.keys(), choice_columns=[0,]) + + self.lState.InsertColumn(0, 'Key') + self.lState.SetColumnWidth(0, 300) + self.lState.InsertColumn(1, 'Value') + #self.lState.makeColumnEditable(0) + #self.lState.makeColumnEditable(1) - hsizer.Add(wx.StaticText(self, -1, 'Num frames: '), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + # start with a blank entry + self.lState.InsertItem(0, '...') + self.lState.SetItem(0, 1, '') - self.tNumFrames = wx.TextCtrl(self, -1, '10000', size=(50, -1)) - hsizer.Add(self.tNumFrames, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.lState.Bind(wx.EVT_LIST_END_LABEL_EDIT, self.OnEndLabelEdit) - hsizer.AddStretchSpacer() + hsizer.Add(self.lState, 1, wx.EXPAND|wx.ALL, 2) + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + def OnEndLabelEdit(self, event): + if event.GetText() not in ('', '...') and event.GetColumn() == 0: + row = event.GetIndex() + key = event.GetText() + if self.lState.GetItemText(row, 1) == '': + self.lState.SetItem(row, 1, str(self.scope.state.get(key, ''))) + + if event.GetIndex() == (self.lState.GetItemCount() -1) and not event.GetText() in ('', '...'): + # add a new empty row + self.lState.InsertItem(self.lState.GetItemCount(), '...') + self.lState.SetItem(self.lState.GetItemCount()-1, 1, '') + + def _get_action(self, idx=0): + # TODO - use a cleaner dictionary editor + + state = {} + for i in range(self.lState.GetItemCount()): + key = self.lState.GetItemText(i, 0) + val = self.lState.GetItemText(i, 1) + if key not in ('', '...'): + state[key] = eval(val) + #state = eval('dict(%s)' % self.tState.GetValue()) + return actions.UpdateState(state) + +class CenterROIOnPanel(SingleActionPanel): + def _init_controls(self, sizer): + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.rbModeSingle = wx.RadioButton(self, -1, 'Single ROI', style=wx.RB_GROUP) + self.rbModeSingle.SetValue(True) + self.rbModeSingle.Bind(wx.EVT_RADIOBUTTON, self.OnSetMode) + hsizer.Add(self.rbModeSingle, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.bAddAquisition = wx.Button(self, -1, 'Add acquisition') - self.bAddAquisition.Bind(wx.EVT_BUTTON, self.OnAddSequence) - hsizer.Add(self.bAddAquisition, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.rbModeROIList = wx.RadioButton(self, -1, 'ROI List') + self.rbModeROIList.Bind(wx.EVT_RADIOBUTTON, self.OnSetMode) + hsizer.Add(self.rbModeROIList, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - vsizer.Add(hsizer, 0, wx.EXPAND, 0) + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + # single mode hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stXLabel = wx.StaticText(self, -1, 'X:') + hsizer.Add(self.stXLabel, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tX = wx.TextCtrl(self, -1, '0', size=(50, -1)) + hsizer.Add(self.tX, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + self.stYLabel = wx.StaticText(self, -1, 'Y:') + hsizer.Add(self.stYLabel, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tY = wx.TextCtrl(self, -1, '0', size=(50, -1)) + hsizer.Add(self.tY, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + self.bSetCurrent = wx.Button(self, -1, 'Use current centre') + self.bSetCurrent.Bind(wx.EVT_BUTTON, self.OnSetCurrent) + hsizer.Add(self.bSetCurrent, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + #ROI list mode + self.stROIList = wx.StaticText(self, -1, 'No rois specified') + sizer.Add(self.stROIList, 0, wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) #wx.StaticBoxSizer(wx.StaticBox(self, label='Queue acquisitions for each ROI'), wx.HORIZONTAL) - hsizer.Add(wx.StaticText(self, -1, 'Sort Function:'), 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) + self.stSortFcnLabel = wx.StaticText(self, -1, 'Sort Function:') + hsizer.Add(self.stSortFcnLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) self.SortSelect = wx.ComboBox(self, -1, 'None', choices=list(SORT_FUNCTIONS.keys()), size=(150, -1)) hsizer.Add(self.SortSelect, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) - self.bQueueROIsFromFile = wx.Button(self, -1, 'Queue ROIs from file') + self.bQueueROIsFromFile = wx.Button(self, -1, 'from file') self.bQueueROIsFromFile.Bind(wx.EVT_BUTTON, self.OnROIsFromFile) hsizer.Add(self.bQueueROIsFromFile, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) - self.bQueueROIsFromTileviewer = wx.Button(self, -1, 'Queue ROIs from Tile Viewer') + self.bQueueROIsFromTileviewer = wx.Button(self, -1, 'from Tile Viewer') self.bQueueROIsFromTileviewer.Bind(wx.EVT_BUTTON, self.OnROIsFromTileviewer) hsizer.Add(self.bQueueROIsFromTileviewer, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) - vsizer.Add(hsizer, 0, wx.EXPAND, 0) + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + self.OnSetMode(None) + + wx.CallAfter(self._set_then, 'SpoolSeries') + + def OnSetCurrent(self, event): + x, y = self.scope.get_roi_centre() + self.tX.SetValue('%.2f' % x) + self.tY.SetValue('%.2f' % y) + + def _get_action(self, idx=0): + if self.rbModeSingle.GetValue(): + x = float(self.tX.GetValue()) + y = float(self.tY.GetValue()) + return actions.CentreROIOn(x, y) + else: + try: + x, y = self._rois[idx, :] + except AttributeError: + raise ValueError('No ROIs have been loaded') + return actions.CentreROIOn(x, y) + + @property + def num_actions(self): + if self.rbModeSingle.GetValue(): + return 1 + else: + try: + return len(self._rois) + except AttributeError: + raise ValueError('No ROIs have been loaded') + + def OnSetMode(self, event): + if self.rbModeSingle.GetValue(): + self.stXLabel.Show() + self.tX.Show() + self.stYLabel.Show() + self.tY.Show() + self.bSetCurrent.Show() + + self.stROIList.Hide() + self.stSortFcnLabel.Hide() + self.SortSelect.Hide() + self.bQueueROIsFromFile.Hide() + self.bQueueROIsFromTileviewer.Hide() + else: + self.stXLabel.Hide() + self.tX.Hide() + self.stYLabel.Hide() + self.tY.Hide() + self.bSetCurrent.Hide() + + self.stROIList.Show() + self.stSortFcnLabel.Show() + self.SortSelect.Show() + self.bQueueROIsFromFile.Show() + self.bQueueROIsFromTileviewer.Show() + + wx.CallAfter(self.cascading_layout) + + def _add_ROIs(self, rois): + positions = np.reshape(rois, (len(rois), 2)).astype(float) + + # apply sorting function + scope_pos = self.scope.GetPos() + positions = SORT_FUNCTIONS[self.SortSelect.GetValue()](positions, (scope_pos['x'], scope_pos['y'])) + + self._rois = positions + + self.stROIList.SetLabel('Loaded %d ROIs' % len(self._rois)) + + + def OnROIsFromFile(self, event): + # TODO - support .csv as well + import wx + from PYME.IO import tabular + + filename = wx.FileSelector("Load ROI Positions:", wildcard="*.hdf", flags=wx.FD_OPEN) + if not filename == '': + rois = tabular.HDFSource(filename, tablename='roi_locations') + + rois = [(x, y) for x, y in zip(rois['x_um'], rois['y_um'])] + + self._add_ROIs(rois) + + def OnROIsFromTileviewer(self, event): + import requests + resp = requests.get('http://localhost:8979/get_roi_locations') + if resp.status_code != 200: + raise requests.HTTPError('Could not get ROI locations') + + rois = np.array(resp.json()) + self._add_ROIs(rois) + + + + + +class SpoolSeriesPanel(SingleActionPanel): + supports_then = False + + def __init__(self, parent, actionManager, scope): + super().__init__(parent, actionManager, scope) + + self._disp_aqType = None + self._aq_settings_string = None + + #scope.spoolController.onSettingsChange.connect(self._update) + + def _init_controls(self, sizer): + self.stAqType = wx.StaticText(self, -1, 'Add an acquisition using the currently selected type and settings') + sizer.Add(self.stAqType, 0, wx.ALL, 2) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stNumFramesLabel = wx.StaticText(self, -1, 'Max frames:') + hsizer.Add(self.stNumFramesLabel, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tNumFrames = wx.TextCtrl(self, -1, '10000', size=(50, -1)) + hsizer.Add(self.tNumFrames, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + self._mf = self.GetTopLevelParent() + self._mf.time1.register_callback(self._update) + + def _get_action(self, idx=0): + settings = self.scope.spoolController.get_settings() + settings['max_frames'] = int(self.tNumFrames.GetValue()) + return actions.SpoolSeries(settings=settings, preflight_mode='warn', ) + + def _update(self, **kwargs): + if not self: + self._mf.time1.unregister_callback(self._update) + del self._mf # free the reference to the main frame + return + + aqType = self.scope.spoolController.acquisition_type + aq_string = f'{aqType} with the following settings:\n{self.scope.spoolController.get_settings()}' + + if (self._disp_aqType != aqType) or (self._aq_settings_string != aq_string): + # only update if the settings have changed + if aqType == 'ProtocolAcquisition': + self.stNumFramesLabel.Show() + self.tNumFrames.Show() + else: + self.stNumFramesLabel.Hide() + self.tNumFrames.Hide() + + self.stAqType.SetLabel(aq_string) + self.stAqType.Wrap(self.GetSize()[0]-5) + + self.cascading_layout() + + self._disp_aqType = aqType + self._aq_settings_string = aq_string + + + +class RemoteSpoolSeriesPanel(SpoolSeriesPanel): + supports_then = False + + def __init__(self, parent, actionManager, scope): + super().__init__(parent, actionManager, scope) + + #scope.spoolController.onSettingsChange.connect(self._update) + + def _init_controls(self, sizer): + self.stAqType = wx.StaticText(self, -1, 'Add a remote acquisition using the remotely selected type and settings') + sizer.Add(self.stAqType, 0, wx.ALL, 2) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stRemoteInstanceName = wx.StaticText(self, -1, 'Remote instance name:') + hsizer.Add(self.stRemoteInstanceName, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tRemoteInstanceName = wx.TextCtrl(self, -1, 'remote_acquire_instance', size=(150, -1)) + hsizer.Add(self.tRemoteInstanceName, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.stNumFramesLabel = wx.StaticText(self, -1, 'Max frames:') + hsizer.Add(self.stNumFramesLabel, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tNumFrames = wx.TextCtrl(self, -1, '10000', size=(50, -1)) + hsizer.Add(self.tNumFrames, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + def _get_action(self, idx=0): + # use remote settings for acquisition type and type-specific settings + remote_instance = self.tRemoteInstanceName.GetValue() + settings = getattr(self.scope, remote_instance).spooling_info()['settings'] + + # use local settings for spool method and compression + settings.update(self.scope.spoolController.get_settings(method_only=True)) + settings['max_frames'] = int(self.tNumFrames.GetValue()) + + return actions.RemoteSpoolSeries(remote_instance=remote_instance,settings=settings, preflight_mode='warn', ) + +class SimultaneousSpoolSeriesPanel(SpoolSeriesPanel): + supports_then = False + + def __init__(self, parent, actionManager, scope): + super().__init__(parent, actionManager, scope) + + #scope.spoolController.onSettingsChange.connect(self._update) + + def _init_controls(self, sizer): + self.stAqType = wx.StaticText(self, -1, 'Add simultaneously executing remote and local acquisition') + sizer.Add(self.stAqType, 0, wx.ALL, 2) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stRemoteInstanceName = wx.StaticText(self, -1, 'Remote instance name:') + hsizer.Add(self.stRemoteInstanceName, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tRemoteInstanceName = wx.TextCtrl(self, -1, 'remote_acquire_instance', size=(150, -1)) + hsizer.Add(self.tRemoteInstanceName, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.stNumFramesLabel = wx.StaticText(self, -1, 'Max frames:') + hsizer.Add(self.stNumFramesLabel, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tNumFrames = wx.TextCtrl(self, -1, '10000', size=(50, -1)) + hsizer.Add(self.tNumFrames, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + sizer.Add(hsizer, 0, wx.EXPAND, 0) + + def _get_action(self, idx=0): + # use remote settings for acquisition type and type-specific settings + remote_instance = self.tRemoteInstanceName.GetValue() + remote_settings = getattr(self.scope, remote_instance).spooling_info()['settings'] + + # use local settings for spool method and compression + remote_settings.update(self.scope.spoolController.get_settings(method_only=True)) + remote_settings['max_frames'] = int(self.tNumFrames.GetValue()) + + local_settings = self.scope.spoolController.get_settings() + local_settings['max_frames'] = int(self.tNumFrames.GetValue()) + + return actions.SimultaeneousSpoolSeries(remote_instance=remote_instance,local_settings=local_settings, remote_settings=remote_settings, preflight_mode='warn', ) + + + # def _update(self, **kwargs): + # aqType = self.scope.spoolController.acquisition_type + # if aqType == 'ProtocolAcquisition': + # self.stNumFramesLabel.Show() + # self.tNumFrames.Show() + # else: + # self.stNumFramesLabel.Hide() + # self.tNumFrames.Hide() + + # self.stAqType.SetLabel(f'An {aqType} acquisition will be added with the following settings: {self.scope.spoolController.get_settings()}') + + # self.cascading_layout() + + +class FunctionActionPanel(SingleActionPanel): + supports_then = False + + def _init_controls(self, sizer): + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, 'Function:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tFunction = wx.ComboBox(self, -1, '', choices=ACTION_DEFAULTS,size=(150, -1)) + hsizer.Add(self.tFunction, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + hsizer.Add(wx.StaticText(self, -1, 'Args:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tArgs = wx.TextCtrl(self, -1, '', size=(150, -1)) + hsizer.Add(self.tArgs, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + sizer.Add(hsizer) + + def _get_action(self, idx=0): + function_name = self.tFunction.GetValue() + args = eval('dict(%s)' % self.tArgs.GetValue()) + return actions.FunctionAction(function_name, args) + + +class ActionPanel(wx.Panel, cascading_layout.CascadingLayoutMixin): + def __init__(self, parent, actionManager, scope): + wx.Panel.__init__(self, parent) + self.actionManager = actionManager + self.scope = scope + + vsizer = wx.BoxSizer(wx.VERTICAL) + self.actionList = ActionList(self, self.actionManager) + vsizer.Add(self.actionList, 1, wx.EXPAND, 0) + + self.add_single_sizer = wx.StaticBoxSizer(wx.StaticBox(self, label='Add Action(s)'), wx.VERTICAL) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, 'Action type:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.cActionType = wx.Choice(self, -1, choices=ACTION_TYPES) + self.cActionType.SetSelection(ACTION_TYPES.index('CenterROIOn')) + self.cActionType.Bind(wx.EVT_CHOICE, self.OnActionChanged) + hsizer.Add(self.cActionType, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.add_single_sizer.Add(hsizer, 0, wx.EXPAND, 0) + + self._pan_action = CenterROIOnPanel(self, self.actionManager, self.scope) + self._pan_action_sizer = wx.BoxSizer(wx.VERTICAL) + self._pan_action_sizer.Add(self._pan_action, 0, wx.EXPAND, 0) + self.add_single_sizer.Add(self._pan_action_sizer, 0, wx.EXPAND|wx.LEFT, 20) + + vsizer.Add(wx.StaticLine(self, -1, style=wx.LI_HORIZONTAL), 0, wx.EXPAND|wx.ALL, 5) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + hsizer.Add(wx.StaticText(self, -1, 'Delay[s]:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tDelay = wx.TextCtrl(self, -1, '0', size=(40, -1)) + hsizer.Add(self.tDelay, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + + hsizer.Add(wx.StaticText(self, -1, 'Repetitions:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tRepeats = wx.TextCtrl(self, -1, '1', size=(30, -1)) + hsizer.Add(self.tRepeats, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + hsizer.Add(wx.StaticText(self, -1, 'Period [s]:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tPeriod = wx.TextCtrl(self, -1, '0', size=(30, -1)) + hsizer.Add(self.tPeriod, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + hsizer.Add(wx.StaticText(self, -1, 'Nice:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tNice = wx.TextCtrl(self, -1, '10', size=(30, -1)) + hsizer.Add(self.tNice, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + self.add_single_sizer.Add(hsizer, 0, wx.EXPAND|wx.TOP, 15) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + hsizer.Add(wx.StaticText(self, -1, 'Timeout [s]:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.tTimeout = wx.TextCtrl(self, -1, '1000000', size=(50, -1)) + hsizer.Add(self.tTimeout, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + hsizer.Add(wx.StaticText(self, -1, 'Max duration [s]:'), 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + self.t_duration = wx.TextCtrl(self, -1, '3600', size=(50, -1)) + hsizer.Add(self.t_duration, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + hsizer.AddStretchSpacer() + + self.bAdd = wx.Button(self, -1, 'Add', style=wx.BU_EXACTFIT) + self.bAdd.Bind(wx.EVT_BUTTON, progress.managed(self.OnAddActions, self, 'Adding action')) + hsizer.Add(self.bAdd, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 2) + + self.add_single_sizer.Add(hsizer, 0, wx.EXPAND|wx.TOP, 2) + vsizer.Add(self.add_single_sizer, 0, wx.EXPAND, 0) hsizer = wx.BoxSizer(wx.HORIZONTAL) @@ -159,6 +780,23 @@ def __init__(self, parent, actionManager, scope): #hsizer.Add(vsizer, 0, 0, 0) self.SetSizerAndFit(vsizer) + + def OnActionChanged(self, event): + if self._pan_action is not None: + self._pan_action.Destroy() + self._pan_action_sizer.Clear() + + action = self.cActionType.GetStringSelection() + if action != 'None': + #print('Changing action to %s' % action) + self._pan_action = globals()[action + 'Panel'](self, self.actionManager, self.scope) + self._pan_action_sizer.Add(self._pan_action, 0, wx.EXPAND, 0) + else: + self._pan_action = None + logger.warning('No action selected (we shouldn\'t get here)') + + #print('re-layouting') + self.cascading_layout() def OnPauseActions(self, event): if self.actionManager.paused: @@ -168,100 +806,87 @@ def OnPauseActions(self, event): self.actionManager.paused = True self.bPause.SetLabel('Resume') - def OnAddAction(self, event): - nice = float(self.tNice.GetValue()) - functionName = self.tFunction.GetValue() - args = eval('dict(%s)' % self.tArgs.GetValue()) - timeout = float(self.tTimeout.GetValue()) - max_duration = float(self.t_duration.GetValue()) - self.actionManager.QueueAction(functionName, args, nice, timeout, - max_duration) - def OnAddMove(self, event): + def OnAddActions(self, event): + delay = float(self.tDelay.GetValue()) nice = float(self.tNice.GetValue()) #functionName = self.tFunction.GetValue() #args = eval('dict(%s)' % self.tArgs.GetValue()) - - functionName = 'state.update' - args = {'state' : {'Positioning.x': self.scope.state['Positioning.x'], - 'Positioning.y': self.scope.state['Positioning.y'], - 'Positioning.z': self.scope.state['Positioning.z']}} - timeout = float(self.tTimeout.GetValue()) max_duration = float(self.t_duration.GetValue()) - self.actionManager.QueueAction(functionName, args, nice, timeout, - max_duration) - - def OnAddSequence(self, event): - nice = float(self.tNice.GetValue()) - functionName = 'spoolController.StartSpooling' - args = {'maxFrames' : int(self.tNumFrames.GetValue()), 'stack': bool(self.rbZStepped.GetValue())} - timeout = float(self.tTimeout.GetValue()) - max_duration = float(self.t_duration.GetValue()) - self.actionManager.QueueAction(functionName, args, nice, timeout, - max_duration) - - - def _add_ROIs(self, rois): - """ - Add ROI positioning and spooling actions to queue. - - Parameters - ---------- - rois: list-like - list of ROI (x, y) positions, or array of shape (n_roi, 2). Units in micrometers. - - Notes - ----- - Currently ignores the `Max Duration` GUI control, ensuring the timeout - is long enough for all queued ROI tasks, and with 10 s max duration on - movements and ~2x acquisition time max durations for spooling series. - """ - - # coordinates are for the centre of ROI, and are referenced to the 0,0 pixel of the camera, - # correct this for a custom ROI. - roi_offset_x, roi_offset_y = self.scope.get_roi_offset() + repetitions = int(self.tRepeats.GetValue()) + period = float(self.tPeriod.GetValue()) - # subtract offset and reshape to N x 2 array - positions = np.reshape(rois, (len(rois), 2)).astype(float) - np.array([roi_offset_x, roi_offset_y])[None, :] + if delay > 0: + execute_after = time.time() + delay + else: + execute_after = time.time() - # apply sorting function - scope_pos = self.scope.GetPos() - positions = SORT_FUNCTIONS[self.SortSelect.GetValue()](positions, (scope_pos['x'], scope_pos['y'])) + #self.actionManager.QueueAction(functionName, args, nice, timeout, + # max_duration, execute_after=execute_after)' + actions = self._pan_action.get_actions() - # get queue parameters - n_frames = int(self.tNumFrames.GetValue()) - nice = float(self.tNice.GetValue()) - time_est = 1.25 * n_frames / self.scope.cam.GetFPS() # per series - logger.debug('Expecting series to complete in %.1f s each' % time_est) - # allow enough time for what we queue - timeout = max(float(self.tTimeout.GetValue()), - positions.shape[0] * time_est) - for ri in range(positions.shape[0]): - args = {'state': {'Positioning.x': positions[ri, 0], 'Positioning.y': positions[ri, 1]}} - self.actionManager.QueueAction('state.update', args, nice, timeout, 10) - args = {'maxFrames': n_frames, 'stack': bool(self.rbZStepped.GetValue())} - self.actionManager.QueueAction('spoolController.StartSpooling', args, nice, timeout, 2 * time_est) - - def OnROIsFromFile(self, event): - import wx - from PYME.IO import tabular + # FIXME - this is a very empirical - maybe revisit + t_est = actions[0].estimated_duration(self.scope) + logger.debug('Expect actions to complete in %.1f s' % t_est) + max_duration = max(2*t_est, max_duration) + timeout = max(max_duration*len(actions), timeout) - filename = wx.FileSelector("Load ROI Positions:", wildcard="*.hdf", flags=wx.FD_OPEN) - if not filename == '': - rois = tabular.HDFSource(filename, tablename='roi_locations') + for i in range(repetitions): + self.actionManager.queue_actions(actions, nice, timeout, max_duration, execute_after=execute_after) + execute_after += period + + + # def _add_ROIs(self, rois): + # """ + # Add ROI positioning and spooling actions to queue. + + # Parameters + # ---------- + # rois: list-like + # list of ROI (x, y) positions, or array of shape (n_roi, 2). Units in micrometers. + + # Notes + # ----- + # Currently ignores the `Max Duration` GUI control, ensuring the timeout + # is long enough for all queued ROI tasks, and with 10 s max duration on + # movements and ~2x acquisition time max durations for spooling series. + # """ + + # # coordinates are for the centre of ROI, and are referenced to the 0,0 pixel of the camera, + # # correct this for a custom ROI. + # roi_offset_x, roi_offset_y = self.scope.get_roi_offset() + + # # subtract offset and reshape to N x 2 array + # positions = np.reshape(rois, (len(rois), 2)).astype(float) - np.array([roi_offset_x, roi_offset_y])[None, :] + + # # apply sorting function + # scope_pos = self.scope.GetPos() + # positions = SORT_FUNCTIONS[self.SortSelect.GetValue()](positions, (scope_pos['x'], scope_pos['y'])) + + # # get queue parameters + # n_frames = int(self.tNumFrames.GetValue()) + # nice = float(self.tNice.GetValue()) + # try: + # time_est = 1.25 * n_frames / self.scope.cam.GetFPS() # per series + # except NotImplementedError: + # # specifically the simulated camera here, which has a non-predictable frame rate + # # use a conservative default of 10 s/frame (should not matter as simulation will generally not be doing 10s of thousands of series) + # time_est = 10*n_frames + + # logger.debug('Expecting series to complete in %.1f s each' % time_est) + # # allow enough time for what we queue + # timeout = max(float(self.tTimeout.GetValue()), + # positions.shape[0] * time_est) + + # acts = [] + # for ri in range(positions.shape[0]): + # state = {'Positioning.x': positions[ri, 0], 'Positioning.y': positions[ri, 1]} + # settings = {'max_frames': n_frames, 'z_stepped': bool(self.rbZStepped.GetValue())} - rois = [(x, y) for x, y in zip(rois['x_um'], rois['y_um'])] + # acts.append(actions.UpdateState(state).then(actions.SpoolSeries(settings=settings, preflight_mode='warn'))) - self._add_ROIs(rois) + # self.actionManager.queue_actions(acts, nice, timeout, 2 * time_est) + - def OnROIsFromTileviewer(self, event): - import requests - resp = requests.get('http://localhost:8979/get_roi_locations') - if resp.status_code != 200: - raise requests.HTTPError('Could not get ROI locations') - - rois = np.array(resp.json()) - print(rois.shape) - self._add_ROIs(rois) diff --git a/PYME/Acquire/ui/focus_lock_gui.py b/PYME/Acquire/ui/focus_lock_gui.py index 86efbd622..4640416f3 100644 --- a/PYME/Acquire/ui/focus_lock_gui.py +++ b/PYME/Acquire/ui/focus_lock_gui.py @@ -24,11 +24,22 @@ def __init__(self, parent, focus_PID, winid=-1, offset_piezo=None): hsizer.Add(self.set_position_button, 0, wx.ALL, 2) self.set_position_button.Bind(wx.EVT_BUTTON, self.OnUpdateSetpoint) + if self.offset_piezo is not None: + self.set_home_button = wx.Button(self, -1, 'Set Home') + hsizer.Add(self.set_home_button, 0, wx.ALL, 2) + self.set_home_button.Bind(wx.EVT_BUTTON, self.OnUpdateHome) + + sizer_1.Add(hsizer, 0, wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) self.set_subtraction_button = wx.Button(self, -1, 'Set Dark') hsizer.Add(self.set_subtraction_button, 0, wx.ALL, 2) self.set_subtraction_button.Bind(wx.EVT_BUTTON, self.OnSetSubtractionProfile) + if hasattr(self.servo, 'subtraction_profile') and self.servo.subtraction_profile is not None: + self.set_subtraction_button.SetBackgroundColour("green") sizer_1.Add(hsizer, 0, wx.EXPAND, 0) + if self.offset_piezo is not None: offset, offset_range = self._get_offset_and_range() @@ -61,9 +72,14 @@ def OnToggleLock(self, event): def OnUpdateSetpoint(self, event): self.servo.ChangeSetpoint() + + def OnUpdateHome(self, wx_event): + if self.servo.lock_enabled: + self.servo._piezo_home = self.offset_piezo.GetTargetPos() def OnSetSubtractionProfile(self, event): self.servo.SetSubtractionProfile() + self.set_subtraction_button.SetBackgroundColour("green") def refresh(self): self.lock_checkbox.SetValue(bool(self.servo.lock_enabled)) diff --git a/PYME/Acquire/ui/idle_control.py b/PYME/Acquire/ui/idle_control.py new file mode 100644 index 000000000..6ff7c1f46 --- /dev/null +++ b/PYME/Acquire/ui/idle_control.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# Idle mode control panel for PYMEAcquire + +import wx +import logging + +logger = logging.getLogger(__name__) + +class IdleModeControl(wx.Panel): + """ + Two-button (Play/Stop) control for setting camera idle/active state. + + - â–ļ Play: exit Idle (set active). + - ⏚ Stop: enter Idle. + + """ + def __init__(self, parent, scope): + wx.Panel.__init__(self, parent) + self.scope = scope + self.parent = parent + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + # Label + hsizer.Add(wx.StaticText(self, -1, "Camera:"), 0, + wx.ALIGN_CENTER_VERTICAL | wx.ALL, 2) + + green = wx.Colour(0, 170, 0) + red = wx.Colour(200, 0, 0) + + # Play (exit idle) + self.btnPlay = wx.Button(self, -1, "â–ļ", style=wx.BU_EXACTFIT) + self.btnPlay.SetToolTip(wx.ToolTip("Set Active (exit Idle)")) + self.btnPlay.Bind(wx.EVT_BUTTON, self.on_play) + self.btnPlay.SetForegroundColour(green) + hsizer.Add(self.btnPlay, 0, wx.ALL, 2) + + # Stop (enter idle) + self.btnStop = wx.Button(self, -1, "⏚", style=wx.BU_EXACTFIT) + self.btnStop.SetToolTip(wx.ToolTip("Set Idle (pause camera)")) + self.btnStop.Bind(wx.EVT_BUTTON, self.on_stop) + self.btnStop.SetForegroundColour(red) + hsizer.Add(self.btnStop, 0, wx.ALL, 2) + + self.SetSizerAndFit(hsizer) + + # Listen for state changes from the camera + self.scope.cam.on_idle_change.connect(self.on_idle_changed) + + # Initial state + self.update() + + def on_play(self, event=None): + """Exit idle mode (set Active).""" + try: + if not self.scope.cam.GetIdle(): + return # already active -> no-op + logger.info('User setting camera to ACTIVE') + self.scope.cam.SetIdle(False) + except Exception: + logger.exception('Error setting Active state from IdleModeControl') + finally: + wx.CallAfter(self.update) + + def on_stop(self, event=None): + """Enter idle mode (pause camera).""" + try: + if self.scope.cam.GetIdle(): + return # already idle -> no-op + logger.info('User setting camera to IDLE') + self.scope.cam.SetIdle(True) + except Exception: + logger.exception('Error setting Idle state from IdleModeControl') + finally: + wx.CallAfter(self.update) + + def on_idle_changed(self, sender, idle, **kwargs): + """Called when the idle state changes (from signal)""" + wx.CallAfter(self.update) + + def update(self): + """enable/disable buttons based on idle/active.""" + idle = self.scope.cam.GetIdle() + + if idle: # Idle state + self.btnPlay.SetToolTip(wx.ToolTip("Set Active (exit Idle)")) + self.btnStop.SetToolTip(wx.ToolTip("Camera is idle")) + self.btnPlay.Enable() + self.btnStop.Disable() + else: # Active state + self.btnPlay.SetToolTip(wx.ToolTip("Camera is active")) + self.btnStop.SetToolTip(wx.ToolTip("Set Idle (pause camera)")) + self.btnPlay.Disable() + self.btnStop.Enable() + + self.Layout() diff --git a/PYME/Acquire/ui/intensity_trace.py b/PYME/Acquire/ui/intensity_trace.py new file mode 100644 index 000000000..191b110fc --- /dev/null +++ b/PYME/Acquire/ui/intensity_trace.py @@ -0,0 +1,143 @@ + +import wx +import numpy as np +from PYME.ui.fastGraph import FastGraphPanel +import weakref +from PYME.ui.selection import SELECTION_RECTANGLE +import logging + +logger = logging.getLogger(__name__) + + +class IntensityTracePanel(FastGraphPanel): + def __init__(self, parent, frame_wrangler, winid=-1, n_frames=1000): + """If the square region select tool is active, will plot the average value within it + over time. Note that the update rate is set by the GUI timer, not the camera frame rate. + + Parameters + ---------- + parent : wx.Window + PYMEAcquire MainFrame / AUIFrame + frame_wrangler : PYME.Acquire.FrameWrangler + frame_wrangler object, which is grabbing new frames from the camera + winid : int, optional + direct pass-through to FastGraphPanel, by default -1 + n_frames : int, optional + Number of frame-updates to store and display, by default 1000 + """ + self.frame_vals = np.arange(n_frames) + + # over-allocate buffer so that there is room at the end for us to record multiple values before displaying. + # Overallocation is designed to give a factor of 2 margin at a maximum frame rate of around 1000 hz assuming + # display and buffer shifting happens at 2Hz with the standard GUI timer + self.intensity_avg = np.zeros(n_frames + 1000, dtype=float) + FastGraphPanel.__init__(self, parent, winid, self.frame_vals, + self.intensity_avg[:n_frames]) + self.wrangler = frame_wrangler + + self._mf = weakref.ref(parent) # weak ref to MainFrame + self._relative_val = 1 + + self._buf_idx=0 + self._n_frames = n_frames + + + @property + def do(self): + """ + get the display options. This is done lazily + because it might not be created when this panel is instantiated. + """ + return self._mf().vp.do # PYME.UI.displayOps.DisplayOptions + + def clear(self): + """ + Clear the trace + """ + self._relative_val = 1. + x0, x1, y0, y1, _, _ = self.do.sorted_selection + self.intensity_avg = np.ones_like(self.intensity_avg) * np.nan_to_num(np.mean(self.wrangler.currentFrame[x0:x1, y0:y1])) + + def update_trace(self, sender=None, **kwargs): + """ + Update trace intensity data with the current frame's average value within the selected region (can be called separately from display). + This is likely light-weight enough to be hooked to FrameWrangler.onFrame in most circumstances, although could be hooked to a lower-rate timer (e.g. onFrameGroup + or time1) if desired. + """ + if self.do.selection.mode != SELECTION_RECTANGLE: + return + + # try to get data from the signal argument (if we bound to the onFrame signal), otherwise use + # the current frame (if bound to onFrameGroup, time1) + data = kwargs.get('frameData',self.wrangler.currentFrame).squeeze() + + #print(data.shape, self.wrangler.currentFrame.shape) + + x0, x1, y0, y1, _, _ = self.do.sorted_selection + + # check, do we swap xy / rc here? + self.intensity_avg[self._buf_idx] = np.nan_to_num(np.mean(data[x0:x1, y0:y1])) / self._relative_val + + if self._buf_idx < (len(self.intensity_avg)-1): + # prevent an out-of-bounds error if gui bogs down and we don't re-shift buffer in time. + self._buf_idx += 1 + + def update_display(self, sender=None, **kwargs): + """Updates the GUI display and does buffer shifting. + + Note that this must be wired-up to the GUI timer, not the frameWrangler's onFrame + + Parameters + ---------- + sender : optional + dispatch caller, included only to match the required function signature + """ + + + + if self._buf_idx >= (self._n_frames - 1): + #move data backwards in the buffer so that the last data point is at the right hand side of the displayed interval. + offset = self._buf_idx - (self._n_frames -2) + #print(self._n_frames, self._buf_idx, offset) + self.intensity_avg[:self._n_frames] = self.intensity_avg[offset:(offset+ self._n_frames)] + self._buf_idx = self._n_frames - 1 + + print(self.intensity_avg[self._n_frames-1]) + self.SetData(self.frame_vals, self.intensity_avg[:self._n_frames]) + +class TraceROISelectPanel(wx.Panel): + def __init__(self, parent, trace_page, winid=-1): + """ + Simple panel to facilitate clearing the Itensity trace through the GUI + """ + wx.Panel.__init__(self, parent, winid) + self.trace_page = trace_page + + vsizer = wx.BoxSizer(wx.VERTICAL) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.clear_button = wx.Button(self, -1, 'Clear') + hsizer.Add(self.clear_button, 0, wx.ALL, 2) + self.clear_button.Bind(wx.EVT_BUTTON, self.OnClear) + vsizer.Add(hsizer, 0, wx.EXPAND, 0) + + self.SetSizerAndFit(vsizer) + + def OnClear(self, wx_event=None): + self.trace_page.clear() + + +# example init script plug: +# @init_gui('intensity trace') +# def intensity_trace(MainFrame, scope): +# from PYME.Acquire.ui.intensity_trace import IntensityTracePanel, TraceROISelectPanel + +# intensity_trace = IntensityTracePanel(MainFrame, scope.frameWrangler) +# MainFrame.AddPage(page=intensity_trace, select=False, caption='Trace') +# scope.frameWrangler.onFrame.connect(intensity_trace.update_trace) # to update data on every frame +# #scope.frameWrangler.onFrameGroup.connect(intensity_trace.update_trace) # to update data at ~5Hz, or every frame, whichever is slower + +# MainFrame.time1.register_callback(intensity_trace.update_display) # update the display at 2Hz + +# panel = TraceROISelectPanel(MainFrame, intensity_trace) +# MainFrame.camPanels.append((panel, 'Trace ROI Select')) diff --git a/PYME/Acquire/ui/intsliders.py b/PYME/Acquire/ui/intsliders.py index 357c7f3ac..30b7e20e2 100755 --- a/PYME/Acquire/ui/intsliders.py +++ b/PYME/Acquire/ui/intsliders.py @@ -53,7 +53,7 @@ def __init__(self, chaninfo, parent, scope, winid=-1): for c in range(nsliders): #if not sys.platform == 'darwin': - sl = wx.Slider(self, -1, self.chaninfo.itimes[c], 1, min(5*self.chaninfo.itimes[c], 10000), size=wx.Size(100,-1),style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS)#|wx.SL_LABELS) + sl = wx.Slider(self, -1, int(self.chaninfo.itimes[c]), 1, int(min(5*self.chaninfo.itimes[c], 10000)), size=wx.Size(100,-1),style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS)#|wx.SL_LABELS) #else:#workaround for broken mouse event handling (and hence sliders) on MacOS # sl = wx.Slider(self, -1, self.chaninfo.itimes[c], 1, min(5*self.chaninfo.itimes[c], 10000), size=wx.Size(300,-1),style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_LABELS) @@ -106,7 +106,7 @@ def onSlide(self, event): def onCombobox(self, event): cb = event.GetEventObject() ind = self.cboxes.index(cb) - print((cb.GetValue())) + #print((cb.GetValue())) self.chaninfo.itimes[ind] = float(cb.GetValue()) self.sliders[ind].SetValue(self.chaninfo.itimes[ind]) self.sliders[ind].SetRange(1, min(5*self.chaninfo.itimes[ind], 10000)) @@ -139,10 +139,11 @@ def __init__(self, parent, scope, winid=-1): #for c in range(nsliders): c = 1 - itime = 1e3*self.scope.state['Camera.IntegrationTime'] - + # slider works in integer ms, ensure at least 1 ms to avoid 0 range error + # TODO - do we need to revisit the UI for cameras where you might routinely want to set sub ms exposure times? + itime = max([1e3*self.scope.state['Camera.IntegrationTime'], 1]) - sl = wx.Slider(self, -1, itime, 1, min(5*itime, 10000), size=wx.Size(100,-1),style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS)#|wx.SL_LABELS) + sl = wx.Slider(self, -1, int(itime), 1, int(min(5*itime, 10000)), size=wx.Size(100,-1),style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS)#|wx.SL_LABELS) sl_val = wx.ComboBox(self, -1, choices = timeChoices, value = '%d' % itime, size=(65, -1), style=wx.CB_DROPDOWN|wx.TE_PROCESS_ENTER) @@ -205,8 +206,8 @@ def onCombobox(self, event): self.scope.state['Camera.IntegrationTime'] = itime/1e3 new_itime= self.scope.state['Camera.IntegrationTime'] wx.CallAfter(self.cboxes[ind].SetValue, '%1.2f' % (1e3*new_itime)) - self.sliders[ind].SetValue(new_itime*1e3) - self.sliders[ind].SetRange(1, min(5*new_itime*1e3, 10000)) + self.sliders[ind].SetValue(int(new_itime*1e3)) + self.sliders[ind].SetRange(1, min(int(5*new_itime*1e3), 10000)) #self.scope.frameWrangler.stop() #self.scope.frameWrangler.start() @@ -214,7 +215,7 @@ def onCombobox(self, event): def update(self, value, **kwargs): ind = 0 #print 'update: ', value - value = value*1e3 + value = int(value*1e3) self.sliders[ind].SetValue(value) self.sliders[ind].SetRange(1, min(5*value, 10000)) diff --git a/PYME/Acquire/ui/lasersliders.py b/PYME/Acquire/ui/lasersliders.py index a97c552b5..26ea06992 100644 --- a/PYME/Acquire/ui/lasersliders.py +++ b/PYME/Acquire/ui/lasersliders.py @@ -312,7 +312,7 @@ def onSlide(self, event): ind = self.sliders.index(sl) self.sl = sl self.ind = ind - print((self.lasers[ind].power, self.lasers[ind].MAX_POWER*2**(sl.GetValue())/1024.)) + #print((self.lasers[ind].power, self.lasers[ind].MAX_POWER*2**(sl.GetValue())/1024.)) self.lasers[ind].SetPower(self.lasers[ind].MAX_POWER*2**(sl.GetValue())/1024.) finally: self.sliding = False diff --git a/PYME/Acquire/ui/mpd_picosecond_delay_panel.py b/PYME/Acquire/ui/mpd_picosecond_delay_panel.py new file mode 100644 index 000000000..8dd8966c3 --- /dev/null +++ b/PYME/Acquire/ui/mpd_picosecond_delay_panel.py @@ -0,0 +1,69 @@ +import wx + +class DelayPanel(wx.Panel): + """ + Simple slider panel showing/controling the delay of the MPD picosecond + delay module. See PYME.Acquire.Hardware.mpd_picosecond_delayer. + + Example setup in init script: + @init_gui('pulse phase control') + def pulse_phase_controls(MainFrame, scope): + from PYME.Acquire.ui import mpd_picosecond_delay_panel + delay_panel = mpd_picosecond_delay_panel.DelayPanel(MainFrame, scope.mpd, scope) + MainFrame.camPanels.append((delay_panel, 'Phase Delay', False, False)) + MainFrame.time1.register_callback(delay_panel.update) + """ + def __init__(self, parent, delayer, scope): + self.delayer = delayer + self.scope = scope + self.sliding = False + + wx.Panel.__init__(self, parent) + vsizer=wx.BoxSizer(wx.VERTICAL) + + delay = wx.StaticBoxSizer(wx.StaticBox(self, -1, u'Delay (ps)'), wx.VERTICAL) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.l = wx.StaticText(self, -1, '%d' % self.delayer.delay) + hsizer.Add(self.l, 0, wx.ALL, 2) + self.sl = wx.Slider(self, -1, self.delayer.delay, 0, self.delayer.GetMaxDelay(), size=wx.Size(150,-1),style=wx.SL_HORIZONTAL | wx.SL_HORIZONTAL | wx.SL_AUTOTICKS ) + self.sl.SetTickFreq(25000) + self.Bind(wx.EVT_SCROLL,self.on_slide) + hsizer.Add(self.sl, 1, wx.ALL|wx.EXPAND, 2) + delay.Add(hsizer, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL, 0) + + + + vsizer.Add(delay, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.cb_highspeed = wx.CheckBox(self, wx.ID_ANY, 'High-Speed mode (no GUI update)') + self.cb_highspeed.SetValue(self.delayer.high_speed_mode) + self.cb_highspeed.Bind(wx.EVT_CHECKBOX, self.on_cb_highspeed) + + vsizer.Add(hsizer, 0, wx.EXPAND|wx.ALIGN_CENTER_HORIZONTAL, 0) + + self.SetSizer(vsizer) + + + + def on_slide(self, event): + self.sliding = True + try: + sl = event.GetEventObject() + self.delayer.delay = sl.GetValue() + self.l.SetLabel('%d' % self.delayer.delay) + finally: + self.sliding = False + + def on_cb_highspeed(self, wx_event): + self.delayer.high_speed_mode = self.cb_highspeed.GetValue() + + def update(self): + # only update if we aren't sliding and if we aren't running + # the delayer in high speed mode (where it doesn't even update + # the front panel for the sake of speed) + if (not self.sliding) and (not self.delayer.high_speed_mode): + delay = self.delayer.GetDelay() + self.sl.SetValue(delay) + self.l.SetLabel('%d' % delay) diff --git a/PYME/Acquire/ui/multiview_select.py b/PYME/Acquire/ui/multiview_select.py index 9db1a954a..28e674dde 100644 --- a/PYME/Acquire/ui/multiview_select.py +++ b/PYME/Acquire/ui/multiview_select.py @@ -10,7 +10,7 @@ def __init__(self, parent, scope, winid=-1, ): sizer_1= wx.BoxSizer(wx.VERTICAL) l = wx.StaticText(self, -1, 'Select channels:') - sizer_1.Add(l, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + sizer_1.Add(l, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 2) sizer_2 = wx.BoxSizer(wx.HORIZONTAL) self.buttons = [] for view in self.views: diff --git a/PYME/Acquire/ui/positionUI.py b/PYME/Acquire/ui/positionUI.py index 79b10f185..b811f5d66 100644 --- a/PYME/Acquire/ui/positionUI.py +++ b/PYME/Acquire/ui/positionUI.py @@ -54,7 +54,7 @@ def __init__(self, scope, parent, joystick = None, id=-1): rmin, rmax = self.ranges[pName] #print rmin, rmax pos = poss[pName] - sl = wx.Slider(self, -1, 100*pos, 100*rmin, 100*rmax, size=wx.Size(100,-1), style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS|wx.SL_LABELS) + sl = wx.Slider(self, -1, int(100*pos), int(100*rmin), int(100*rmax), size=wx.Size(100,-1), style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS|wx.SL_LABELS) #else: # sl = wx.Slider(self.panel_1, -1, 100*p[0].GetPos(p[1]), 100*p[0].GetMin(p[1]), 100*p[0].GetMax(p[1]), size=wx.Size(300,-1), style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_LABELS) #sl.SetSize((800,20)) @@ -225,7 +225,7 @@ def __init__(self, scope, parent, joystick=None, id=-1): rmin, rmax = self.ranges[pName] #print rmin, rmax pos = poss[pName] - sl = wx.Slider(self, -1, 100 * pos, 100 * rmin, 100 * rmax, size=wx.Size(100, -1), + sl = wx.Slider(self, -1, int(100 * pos), int(100 * rmin), int(100 * rmax), size=wx.Size(100, -1), style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS|wx.SL_LABELS) #else: # sl = wx.Slider(self.panel_1, -1, 100*p[0].GetPos(p[1]), 100*p[0].GetMin(p[1]), 100*p[0].GetMax(p[1]), size=wx.Size(300,-1), style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_LABELS) @@ -315,8 +315,8 @@ def update(self): self.sliders[ind].SetValue(int(100 * pos)) self.sliderLabels[ind].SetLabel(u'%s - %2.3f %s' % (pName, pos, unit)) - self.sliders[ind].SetMin(100 * self.ranges[pName][0]) - self.sliders[ind].SetMax(100 * self.ranges[pName][1]) + self.sliders[ind].SetMin(int(100 * self.ranges[pName][0])) + self.sliders[ind].SetMax(int(100 * self.ranges[pName][1])) if len(self.stageNames) > 0: if not self.tX.HasFocus(): @@ -329,5 +329,191 @@ def update(self): self.updating = False +class GOTODialog(wx.Dialog): + def __init__(self, scope, parent, *args, **kw): + super().__init__(parent, *args, title='GOTO position', **kw) + poss = scope.GetPos() + sizer = wx.BoxSizer(wx.VERTICAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + + self._ctrls = {} + + for k, v in poss.items(): + if not '_target' in k: + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, -1, '%s:' % k), 0, wx.ALIGN_CENTER_VERTICAL) + tc = wx.TextCtrl(self, -1, str(v)) + self._ctrls[k] = tc + hsizer.Add(tc, 1, wx.ALIGN_CENTER_VERTICAL) + hsizer.Add(wx.StaticText(self, -1, '\u03BCm'), 0, wx.ALIGN_CENTER_VERTICAL) + vsizer.Add(hsizer, 0, wx.EXPAND) + + sizer.Add(vsizer, 1, wx.EXPAND|wx.ALL, 5) + + hsizer = self.CreateButtonSizer(wx.OK | wx.CANCEL) + + sizer.Add(hsizer, 0, wx.EXPAND) + + self.SetSizerAndFit(sizer) + + def GetValues(self): + return {k: float(v.GetValue()) for k, v in self._ctrls.items()} + + +class PositionPanelV2(PositionPanel): + def __init__(self, scope, parent, joystick=None, id=-1): + # begin wxGlade: MyFrame1.__init__ + #kwds["style"] = wx.DEFAULT_FRAME_STYLE + wx.Panel.__init__(self, parent, id) + + self.updating = False + + self.scope = scope + self.joystick = joystick + #self.panel_1 = wx.Panel(self, -1) + self.sliders = [] + self.sliderLabels = [] + self.piezoNames = list(self.scope.positioning.keys()) + self.stageNames = [] + + if 'x' in self.piezoNames and 'y' in self.piezoNames: + #Special case for x and y + + self.piezoNames.remove('x') + self.piezoNames.remove('y') + + self.stageNames = ['x', 'y'] + + + + self.ranges = self.scope.GetPosRange() + poss = self.scope.GetPos() + + sizer_ = wx.BoxSizer(wx.VERTICAL) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + sizer_2 = wx.BoxSizer(wx.VERTICAL) + + if len(self.stageNames) > 0: + #hsizer = wx.BoxSizer(wx.HORIZONTAL) + + gsizer = wx.GridBagSizer(3,2)#, vgap=0, hgap=0) + gsizer.AddGrowableCol(0) + + self.stX = wx.StaticText(self, -1, u"x - 0 \u03BCm") + gsizer.Add(self.stX, (0,0), flag=wx.ALIGN_CENTRE_VERTICAL|wx.ALL|wx.EXPAND, border=0) + + self.stY = wx.StaticText(self, -1, u"y - 0 \u03BCm") + gsizer.Add(self.stY, (1, 0), flag=wx.ALIGN_CENTRE_VERTICAL | wx.ALL | wx.EXPAND, border=0) + + #gsizer = wx.GridBagSizer(2, 2) + self.bLeft = wx.Button(self, -1, '<', style=wx.BU_EXACTFIT) + gsizer.Add(self.bLeft, (0, 1)) + self.bRight = wx.Button(self, -1, '>', style=wx.BU_EXACTFIT) + gsizer.Add(self.bRight, (0, 2)) + self.bUp = wx.Button(self, -1, '>', style=wx.BU_EXACTFIT) + gsizer.Add(self.bUp, (1, 2)) + self.bDown = wx.Button(self, -1, '<', style=wx.BU_EXACTFIT) + gsizer.Add(self.bDown, (1, 1)) + + self.bLeft.Bind(wx.EVT_BUTTON, lambda e : self.nudge('x', -1)) + self.bRight.Bind(wx.EVT_BUTTON, lambda e: self.nudge('x', 1)) + self.bUp.Bind(wx.EVT_BUTTON, lambda e: self.nudge('y', 1)) + self.bDown.Bind(wx.EVT_BUTTON, lambda e: self.nudge('y', -1)) + + + sizer_2.Add(gsizer, 0, wx.EXPAND|wx.ALL, 2) + + sizer_2.AddSpacer(10) + + for pName in self.piezoNames: + #if sys.platform == 'darwin': #sliders are subtly broken on MacOS, requiring workaround + rmin, rmax = self.ranges[pName] + #print rmin, rmax + pos = poss[pName] + sl = wx.Slider(self, -1, int(100 * pos), int(100 * rmin), int(100 * rmax), size=wx.Size(100, -1), + style=wx.SL_HORIZONTAL)#|wx.SL_AUTOTICKS|wx.SL_LABELS) + #else: + # sl = wx.Slider(self.panel_1, -1, 100*p[0].GetPos(p[1]), 100*p[0].GetMin(p[1]), 100*p[0].GetMax(p[1]), size=wx.Size(300,-1), style=wx.SL_HORIZONTAL|wx.SL_AUTOTICKS|wx.SL_LABELS) + #sl.SetSize((800,20)) + #if 'units' in dir(p[0]): + # unit = p[0].units + #else: + unit = u'\u03BCm' + + sLab = wx.StaticBox(self, -1, u'%s - %2.3f %s' % (pName, pos, unit)) + + # if 'minorTick' in dir(p): + # sl.SetTickFreq(100, p.minorTick) + # else: + # sl.SetTickFreq(100, 1) + sz = wx.StaticBoxSizer(sLab, wx.HORIZONTAL) + sz.Add(sl, 1, wx.ALL | wx.EXPAND, 0) + #sz.Add(sLab, 0, wx.ALL|wx.EXPAND, 2) + sizer_2.Add(sz, 1, wx.EXPAND, 0) + + self.sliders.append(sl) + self.sliderLabels.append(sLab) + + + hsizer.Add(sizer_2, 1, wx.EXPAND, 0) + + self.bGo = wx.Button(self, -1, "G\nO\nT\nO", size=(30, -1)) + #self.bGo.SetSize((25, 200)) + self.bGo.Bind(wx.EVT_BUTTON, self.on_moveto) + #vsizer = wx.BoxSizer(wx.VERTICAL) + #vsizer.Add(self.bGo, 1, wx.EXPAND, 0) + hsizer.Add(self.bGo, 0, wx.EXPAND,0) + + sizer_.Add(hsizer, 1, wx.EXPAND, 0) + + if not joystick is None: + self.cbJoystick = wx.CheckBox(self, -1, 'Enable Joystick') + sizer_.Add(self.cbJoystick, 0, wx.TOP | wx.BOTTOM, 2) + self.cbJoystick.Bind(wx.EVT_CHECKBOX, self.OnJoystickEnable) + + #sizer_2.AddSpacer(1) + + self.Bind(wx.EVT_SCROLL, self.onSlide) + + #self.SetAutoLayout(1) + self.SetSizer(sizer_) + sizer_.Fit(self) + + + def update(self): + poss = self.scope.GetPos() + self.ranges = self.scope.GetPosRange() + + self.updating = True + + for ind in range(len(self.piezoNames)): + pName = self.piezoNames[ind] + + unit = u'\u03BCm' + + pos = poss[pName] + self.sliders[ind].SetValue(int(100 * pos)) + self.sliderLabels[ind].SetLabel(u'%s - %2.3f %s' % (pName, pos, unit)) + + self.sliders[ind].SetMin(int(100 * self.ranges[pName][0])) + self.sliders[ind].SetMax(int(100 * self.ranges[pName][1])) + + if len(self.stageNames) > 0: + #if not self.tX.HasFocus(): + self.stX.SetLabel('x - %2.3f \u03BCm' % poss['x']) + + #if not self.tY.HasFocus(): + self.stY.SetLabel('y - %2.3f \u03BCm' % poss['y']) + + if not self.joystick is None: + self.cbJoystick.SetValue(self.joystick.IsEnabled()) + + self.updating = False + + def on_moveto(self, event): + d = GOTODialog(self.scope, self, pos=self.bGo.GetScreenPosition(), style=wx.CAPTION) + if d.ShowModal() == wx.ID_OK: + self.scope.SetPos(**d.GetValues()) diff --git a/PYME/Acquire/ui/preflight.py b/PYME/Acquire/ui/preflight.py index 5dc6e58f9..7602b088c 100644 --- a/PYME/Acquire/ui/preflight.py +++ b/PYME/Acquire/ui/preflight.py @@ -21,21 +21,38 @@ # ################ -def ShowPreflightResults(parent, failedChecks): - import wx +import logging +logger = logging.getLogger(__name__) + +def ShowPreflightResults(failedChecks, preflight_mode='interactive', parent=None): if len(failedChecks) == 0: return True else: - #print failedChecks - errormsgs = '\n'.join(['- ' + c.message for c in failedChecks]) - msg = 'Preflight check found the following potential problems:\n\n' + errormsgs + '\n\nDo you wish to continue?' - dlg = wx.MessageDialog(parent, msg, 'Preflight Check:', wx.YES_NO|wx.NO_DEFAULT|wx.ICON_ERROR) - - ret = dlg.ShowModal() - dlg.Destroy() - - return ret == wx.ID_YES + if preflight_mode == 'interactive': + import wx + #print failedChecks + errormsgs = '\n'.join(['- ' + c.message for c in failedChecks]) + msg = 'Preflight check found the following potential problems:\n\n' + errormsgs + '\n\nDo you wish to continue?' + dlg = wx.MessageDialog(parent, msg, 'Preflight Check:', wx.YES_NO|wx.NO_DEFAULT|wx.ICON_ERROR) + + ret = dlg.ShowModal() + dlg.Destroy() + + return ret == wx.ID_YES + elif preflight_mode == 'warn': + import warnings + for c in failedChecks: + warnings.warn('PREFLIGHT FAILURE: ' + c.message) + logger.warning('PREFLIGHT FAILURE: ' + c.message) + + return True + else: # 'abort' + for c in failedChecks: + logger.error('PREFLIGHT FAILURE: ' + c.message) + + logger.error('Aborting series due to preflight failures') + diff --git a/PYME/Acquire/ui/rules.py b/PYME/Acquire/ui/rules.py deleted file mode 100644 index 2d03eb169..000000000 --- a/PYME/Acquire/ui/rules.py +++ /dev/null @@ -1,546 +0,0 @@ - -import wx -from PYME.ui import manualFoldPanel -from PYME.cluster.rules import LocalisationRuleFactory as LocalizationRuleFactory -from collections import OrderedDict -import queue -import os -import posixpath -import logging -logger = logging.getLogger(__name__) - -class ProtocolRules(OrderedDict): - """ - Container for associating sets of analysis rules with specific acquisition protocols - """ - def __init__(self, posting_thread_queue_size=5): - """[summary] - Parameters - ---------- - posting_thread_queue_size : int, optional - sets the size of a queue to hold rule posting threads to ensure they - have time to execute, by default 5. .. seealso:: modules :py:mod:`PYME.cluster.rules` - """ - import queue - - OrderedDict.__init__(self) - - self.posting_thread_queue = queue.Queue(posting_thread_queue_size) - - self['default'] = [] - - -class RuleFactoryListCtrl(wx.ListCtrl): - def __init__(self, rule_factory_chain, wx_parent): - """ - Parameters - ---------- - rule_factory_chain : list - wx_parent - """ - - wx.ListCtrl.__init__(self, wx_parent, style=wx.LC_REPORT | wx.BORDER_SUNKEN | wx.LC_VIRTUAL | wx.LC_VRULES) - self._rule_factory_chain = rule_factory_chain - - self.InsertColumn(0, 'Type', width=125) - self.InsertColumn(1, 'ID', width=75) - - self.update_list() - - @property - def localization_rule_indices(self): - return [ind for ind, rf in enumerate(self._rule_factory_chain) if isinstance(rf, LocalizationRuleFactory)] - - def add_rule_factory(self, rule_factory, index=None): - """ - Parameters - ---------- - rule_factory : PYME.cluster.rules.RuleFactory - Returns - ------- - """ - if index is None: - self._rule_factory_chain.append(rule_factory) - else: - self._rule_factory_chain.insert(rule_factory, index) - - self.update_list() - - def replace_rule_factory(self, rule_factory, index): - self._rule_factory_chain[index] = rule_factory - - def OnGetItemText(self, item, col): - """ - Note that this is overriding the wxListCtrl method as required for wxLC_VIRTUAL style - - Parameters - ---------- - item : long - wx list item - col : long - column specifier for wxListCtrl - Returns - ------- - str : Returns string of column 'col' for item 'item' - """ - try: - if col == 0: - return self._rule_factory_chain[item]._type - if col == 1: - return str(item) - except: - return '' - else: - return '' - - def update_list(self, sender=None, **kwargs): - n_rules = len(self._rule_factory_chain) - for s_ind, f_ind in enumerate(range(1, n_rules)): - self._rule_factory_chain[s_ind].chain(self._rule_factory_chain[f_ind]) - - self.SetItemCount(n_rules) - self.Update() - self.Refresh() - - def delete_rules(self, indices=None): - selected_indices = self.get_selected_items() if indices is None else indices - - for ind in reversed(sorted(selected_indices)): # delete in reverse order so we can pop without changing indices - self._rule_factory_chain.pop(ind) - self.DeleteItem(ind) - - self.update_list() - - def get_selected_items(self): - selection = [] - current = -1 - next = 0 - while next != -1: - next = self.get_next_selected(current) - if next != -1: - selection.append(next) - current = next - return selection - - def get_next_selected(self, current): - return self.GetNextItem(current, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) - - def clear_localization_rules(self): - self.delete_rules(self.localization_rule_indices) - - -class ProtocolRuleFactoryListCtrl(wx.ListCtrl): - def __init__(self, protocol_rules, wx_parent): - """ - Parameters - ---------- - protocol_rules: dict - acquisition protocols (keys) and their associated rule factory - chains - wx_parent - """ - wx.ListCtrl.__init__(self, wx_parent, style=wx.LC_REPORT | wx.BORDER_SUNKEN | wx.LC_VIRTUAL | wx.LC_VRULES) - - self._protocol_rules = protocol_rules - - self.InsertColumn(0, 'Protocol', width=125) - self.InsertColumn(1, '# Rules', width=75) - - self.update_list() - - def OnGetItemText(self, item, col): - """ - Note that this is overriding the wxListCtrl method as required for wxLC_VIRTUAL style - - Parameters - ---------- - item : long - wx list item - col : long - column specifier for wxListCtrl - Returns - ------- - str : Returns string of column 'col' for item 'item' - """ - try: - if col == 0: - return list(self._protocol_rules.keys())[item] - if col == 1: - print(list(self._protocol_rules.items())) - return str(len(list(self._protocol_rules.values())[item])) - except: - return '' - else: - return '' - - def update_list(self, sender=None, **kwargs): - self.SetItemCount(len(self._protocol_rules.keys())) - self.Update() - self.Refresh() - - def delete_rule_chains(self, indices=None): - selected_indices = self.get_selected_items() if indices is None else indices - - for ind in reversed(sorted(selected_indices)): # delete in reverse order so we can pop without changing indices - if self.GetItemText(ind, col=0) == 'default': - logger.error('Cannot delete the default rule chain') - continue # try to keep people from deleting the default chain - self._protocol_rules.popitem(ind) - self.DeleteItem(ind) - - self.update_list() - - def get_selected_items(self): - selection = [] - current = -1 - next = 0 - while next != -1: - next = self.get_next_selected(current) - if next != -1: - selection.append(next) - current = next - return selection - - def get_next_selected(self, current): - return self.GetNextItem(current, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) - - -class ChainedAnalysisPanel(wx.Panel): - _RULE_LAUNCH_MODES = ['off', 'spool start', 'spool stop'] - def __init__(self, parent, protocol_rules, recipe_manager, spool_controller, - default_pairings=None): - """ - - Parameters - ---------- - parent : wx something - protocol_rules : dict - [description] - recipe_manager : PYME.recipes.recipeGui.RecipeManager - [description] - spool_controller : PYME.Acquire.SpoolController.SpoolController - microscope's spool controller instance, so we can launch on spool - start/stop - default_pairings : dict - protocol keys with lists of RuleFactorys as values to prepopulate - panel on start up - """ - from PYME.contrib import dispatch - - wx.Panel.__init__(self, parent, -1) - - self._protocol_rules_updated = dispatch.Signal() - - self._protocol_rules = protocol_rules - self._rule_chain = protocol_rules[list(protocol_rules.keys())[0]] - self._recipe_manager = recipe_manager - self._spool_controller = spool_controller - - # self._rule_chain_updated = dispatch.Signal() - self._protocol_rules_updated = dispatch.Signal() - - v_sizer = wx.BoxSizer(wx.VERTICAL) - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - - h_sizer.Add(wx.StaticText(self, -1, 'Post automatically: '), 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 2) - self.choice_launch = wx.Choice(self, -1, choices=self._RULE_LAUNCH_MODES) - self.choice_launch.SetSelection(0) - self.choice_launch.Bind(wx.EVT_CHOICE, self.OnToggleAuto) - h_sizer.Add(self.choice_launch, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10) - v_sizer.Add(h_sizer, 0, wx.ALL | wx.EXPAND, 2) - - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self._rule_list = RuleFactoryListCtrl(self._rule_chain, self) - h_sizer.Add(self._rule_list) - v_sizer.Add(h_sizer, 0, wx.EXPAND|wx.TOP, 0) - - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.button_add = wx.Button(self, -1, 'Add from Recipe Panel') - self.button_add.Bind(wx.EVT_BUTTON, self.OnAddFromRecipePanel) - h_sizer.Add(self.button_add, 0, wx.ALL, 2) # todo - (disable until activeRecipe.modules) > 0 - - self.button_del = wx.Button(self, -1, 'Delete') - self.button_del.Bind(wx.EVT_BUTTON, self.OnRemoveRules) - h_sizer.Add(self.button_del, 0, wx.ALL, 2) - v_sizer.Add(h_sizer) - - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.button_pair_protocol = wx.Button(self, -1, 'Pair with protocol') - self.button_pair_protocol.Bind(wx.EVT_BUTTON, self.OnPairWithProtocol) - h_sizer.Add(self.button_pair_protocol, 0, wx.ALL, 2) - v_sizer.Add(h_sizer) - - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self._protocol_rules_list = ProtocolRuleFactoryListCtrl(self._protocol_rules, self) - h_sizer.Add(self._protocol_rules_list) - v_sizer.Add(h_sizer, 0, wx.EXPAND|wx.TOP, 0) - - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.button_del_chain = wx.Button(self, -1, 'Delete pair') - self.button_del_chain.Bind(wx.EVT_BUTTON, self.OnRemoveProtocolRule) - h_sizer.Add(self.button_del_chain, 0, wx.ALL, 2) - v_sizer.Add(h_sizer) - - self.SetSizerAndFit(v_sizer) - - if default_pairings is not None: - self._set_up_defaults(default_pairings) - - def _set_up_defaults(self, pairings): - for protocol_name, rule_factory_list in pairings.items(): - # make sure we've chained the rules - n_rules = len(rule_factory_list) - for s_ind, f_ind in enumerate(range(1, n_rules)): - rule_factory_list[s_ind].chain(rule_factory_list[f_ind]) - - # add them to the protocol rules dict - self._protocol_rules[protocol_name] = rule_factory_list - - # update the GUI - self._protocol_rules_list.update_list() - self._protocol_rules_updated.send(self) - - def OnAddFromRecipePanel(self, wx_event=None): - from PYME.cluster.rules import RecipeRuleFactory - if len(self._recipe_manager.activeRecipe.modules) > 0: - rule_factory = RecipeRuleFactory(recipe=self._recipe_manager.activeRecipe.toYAML()) - self._rule_list.add_rule_factory(rule_factory) - - def OnRemoveRules(self, wx_event=None): - self._rule_list.delete_rules() - - def OnRemoveProtocolRule(self, wx_event=None): - self._protocol_rules_list.delete_rule_chains() - - def OnPairWithProtocol(self, wx_event=None): - from PYME.Acquire import protocol - - dialog = wx.SingleChoiceDialog(self, '', 'Select Protocol', - protocol.get_protocol_list()) - - ret = dialog.ShowModal() - - if ret == wx.ID_OK: - protocol_name = os.path.splitext(dialog.GetStringSelection())[0] - self._protocol_rules[protocol_name] = self._rule_chain - # replace the gui-editable chain with a new one - self._rule_chain = [] - self._rule_list._rule_chain = self._rule_chain - self._rule_list.update_list() - self._protocol_rules['default'] = self._rule_chain - - dialog.Destroy() - self._protocol_rules_list.update_list() - self._protocol_rules_updated.send(self) - - def OnToggleAuto(self, wx_event=None): - mode = self.choice_launch.GetSelection() - - self._spool_controller.onSpoolStart.disconnect(self.post_rules) - self._spool_controller.onSpoolStop.disconnect(self.post_rules) - - if mode == 1: # series start - self._spool_controller.onSpoolStart.connect(self.post_rules) - elif mode == 2: # series stop - self._spool_controller.onSpoolStop.connect(self.post_rules) - - def post_rules(self, **kwargs): - """ - pipe input series name into rule chain and post them all - Parameters - ---------- - kwargs: dict - present here to allow us to call this method through a dispatch.Signal.send - """ - - prot_filename = self._spool_controller.spooler.protocol.filename - prot_filename = '' if prot_filename is None else prot_filename - protocol_name = os.path.splitext(os.path.split(prot_filename)[-1])[0] - logger.info('protocol name : %s' % protocol_name) - - try: - rule_factory_chain = self._protocol_rules[protocol_name] - except KeyError: - rule_factory_chain = self._protocol_rules['default'] - - if len(rule_factory_chain) == 0: - logger.info('no rules in chain') - return - - # set the context based on the input series - series_uri = self._spool_controller.spooler.getURL() - spool_dir, series_stub = posixpath.split(series_uri) - series_stub = posixpath.splitext(series_stub)[0] - context = {'spool_dir': spool_dir, 'series_stub': series_stub, - 'seriesName': series_uri, 'inputs': {'input': [series_uri]}, - 'output_dir': posixpath.join(spool_dir, 'analysis')} - - # rule chain is already linked, add context and push - rule_factory_chain[0].get_rule(context=context).push() - - @staticmethod - def plug(main_frame, scope, default_pairings=None): - """ - Adds a ChainedAnalysisPanel to a microscope gui during start-up - Parameters - ---------- - main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame - microscope gui application - scope : PYME.Acquire.microscope.microscope - the microscope itself - default_pairings : dict - [optional] protocol keys with lists of RuleFactorys as values to - prepopulate panel on start up. By default, None - """ - from PYME.recipes.recipeGui import RecipeView, RecipeManager - - # add a recipe panel - scope._recipe_manager = RecipeManager() - main_frame.recipe_view = RecipeView(main_frame, scope._recipe_manager) - main_frame.AddPage(page=main_frame.recipe_view, select=False, caption='Recipe') - - # give the scope a protocol_rules dict - scope.protocol_rules = ProtocolRules() - - # add this panel - chained_analysis = ChainedAnalysisPanel(main_frame, scope.protocol_rules, - scope._recipe_manager, - scope.spoolController, - default_pairings) - main_frame.anPanels.append((chained_analysis, 'Automatic Analysis', True)) - - -class SMLMChainedAnalysisPanel(manualFoldPanel.foldingPane): - def __init__(self, wx_parent, protocol_rules, recipe_manager, - localization_settings, spool_controller, - default_pairings=None): - """ - Parameters - ---------- - wx_parent - localization_settings: PYME.ui.AnalysisSettingsUI.AnalysisSettings - rule_list_ctrl: RuleChainListCtrl - default_pairings : dict - [optional] protocol keys with lists of RuleFactorys as values to - prepopulate panel on start up. By default, None - """ - from PYME.ui.autoFoldPanel import collapsingPane - from PYME.Acquire.ui import AnalysisSettingsUI - manualFoldPanel.foldingPane.__init__(self, wx_parent, caption='Localization Analysis') - - # add checkbox to propagate rule to rule chain - localization_checkbox_panel = wx.Panel(self, -1) - v_sizer = wx.BoxSizer(wx.VERTICAL) - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.checkbox_propagate = wx.CheckBox(localization_checkbox_panel, -1, 'Localize automatically') - self.checkbox_propagate.SetValue(False) - self.checkbox_propagate.Bind(wx.EVT_CHECKBOX, self.OnTogglePropagate) - h_sizer.Add(self.checkbox_propagate, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 10) - v_sizer.Add(h_sizer, 0, wx.ALL | wx.EXPAND, 2) - - h_sizer = wx.BoxSizer(wx.HORIZONTAL) - self.checkbox_view_live = wx.CheckBox(localization_checkbox_panel, -1, 'Open (live) view') - self.checkbox_view_live.SetValue(False) - if not self.checkbox_propagate.GetValue(): - self.checkbox_view_live.Disable() - self.checkbox_view_live.Bind(wx.EVT_CHECKBOX, self.OnToggleLiveView) - v_sizer.Add(self.checkbox_view_live) - - localization_checkbox_panel.SetSizer(v_sizer) - self.AddNewElement(localization_checkbox_panel) - - clp = collapsingPane(self, caption='settings ...') - clp.AddNewElement(AnalysisSettingsUI.AnalysisSettingsPanel(clp, localization_settings, - localization_settings.onMetadataChanged)) - clp.AddNewElement(AnalysisSettingsUI.AnalysisDetailsPanel(clp, localization_settings, - localization_settings.onMetadataChanged)) - self.AddNewElement(clp) - - self._localization_settings = localization_settings - self._localization_settings.onMetadataChanged.connect(self.update_localization_rule) - - self.rule_panel = ChainedAnalysisPanel(self, protocol_rules, - recipe_manager, spool_controller, - default_pairings) - self.AddNewElement(self.rule_panel) - self._rule_list_ctrl = self.rule_panel._rule_list - self.rule_panel._protocol_rules_updated.connect(self.reset) - - def reset(self, **kwargs): - self.checkbox_propagate.SetValue(False) - self._rule_list_ctrl.clear_localization_rules() - self.checkbox_view_live.Disable() - self.checkbox_view_live.SetValue(False) - self.rule_panel._protocol_rules_list.update_list() - - def OnTogglePropagate(self, wx_event=None): - # for now, assume max of one localization rule per chain, and assume it's controlled by this panel - if self.checkbox_propagate.GetValue(): - loc_rule_indices = self._rule_list_ctrl.localization_rule_indices - if len(loc_rule_indices) < 1: - self._rule_list_ctrl.add_rule_factory( - LocalizationRuleFactory( - analysisMetadata=self._localization_settings.analysisMDH - ) - ) - self.checkbox_view_live.Enable() - else: - self._rule_list_ctrl.clear_localization_rules() - self.checkbox_view_live.Disable() - self.checkbox_view_live.SetValue(False) - - self.rule_panel._protocol_rules_list.update_list() - - def OnToggleLiveView(self, wx_event=None): - if self.checkbox_view_live.GetValue() and 0 in self._rule_list_ctrl.localization_rule_indices: - self._rule_list_ctrl._rule_chain.posted.connect(self._open_live_view) - else: - self._rule_list_ctrl._rule_chain.posted.disconnect(self._open_live_view) - - def update_localization_rule(self): - # NB - panel will only modify first localization rule in the chain - if self.checkbox_propagate.GetValue(): - rule = LocalizationRuleFactory(analysisMetadata=self._localization_settings.analysisMDH) - self._rule_list_ctrl.replace_rule_factory(rule, self._rule_list_ctrl.localization_rule_indices[0]) - - def _open_live_view(self, **kwargs): - """ - Open PYMEVisualize on a freshly spooled series which is being localized - Parameters - ---------- - kwargs: dict - present here to allow us to call this method through a dispatch.Signal.send - """ - import subprocess - # get the URL - uri = self._rule_list_ctrl._rule_chain[self._rule_list_ctrl.localization_rule_indices[0]].outputs[0]['input'] + '/live' - subprocess.Popen('visgui %s' % uri, shell=True) - - @staticmethod - def plug(main_frame, scope, default_pairings=None): - """ - Adds a SMLMChainedAnalysisPanel to a microscope gui during start-up - Parameters - ---------- - main_frame : PYME.Acquire.acquiremainframe.PYMEMainFrame - microscope gui application - scope : PYME.Acquire.microscope.microscope - the microscope itself - default_pairings : dict - [optional] protocol keys with lists of RuleFactorys as values to - prepopulate panel on start up. By default, None - """ - from PYME.recipes.recipeGui import RecipeView, RecipeManager - from PYME.Acquire.ui.AnalysisSettingsUI import AnalysisSettings - - scope._recipe_manager = RecipeManager() - main_frame.recipe_view = RecipeView(main_frame, scope._recipe_manager) - main_frame.AddPage(page=main_frame.recipe_view, select=False, caption='Recipe') - - scope.protocol_rules = ProtocolRules() - scope._localization_settings = AnalysisSettings() - - chained_analysis = SMLMChainedAnalysisPanel(main_frame, scope.protocol_rules, scope._recipe_manager, - scope._localization_settings, scope.spoolController, - default_pairings) - main_frame.anPanels.append((chained_analysis, 'Automatic Analysis', True)) diff --git a/PYME/Acquire/ui/seqdialog.py b/PYME/Acquire/ui/seqdialog.py index eeb512e6d..3fb030671 100755 --- a/PYME/Acquire/ui/seqdialog.py +++ b/PYME/Acquire/ui/seqdialog.py @@ -28,6 +28,7 @@ #from PYME.Acquire import simplesequenceaquisator from PYME.Acquire import stackSettings +from PYME.ui import cascading_layout #from PYME.IO import MetaDataHandler #redefine wxFrame with a version that hides when someone tries to close it @@ -69,11 +70,11 @@ def create(parent): wxID_SEQDIALOGTNUMSLICES, wxID_SEQDIALOGTSTEPSIZE, wxID_SEQDIALOGTSTPOS, -] = [wx.NewId() for i in range(18)] +] = [wx.NewIdRef() for i in range(18)] -class seqPanel(wx.Panel): +class seqPanel(cascading_layout.CascadingLayoutPanel): def _init_ctrls(self, prnt): # generated method, don't edit @@ -173,25 +174,26 @@ def _init_ctrls(self, prnt): #hsizer.Add(sNSlices, 1, 0, 0) vsizer.Add(hsizer, 0, wx.EXPAND|wx.BOTTOM, 5) - if not (self.mode == 'sequence'): + if not (self.mode in ['sequence']): hsizer = wx.BoxSizer(wx.HORIZONTAL) self.stMemory = wx.StaticText(self, -1, '') hsizer.Add(self.stMemory, 1, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) - self.bStart = wx.Button(self, -1, 'Single Stack', style=wx.BU_EXACTFIT) - self.bStart.Bind(wx.EVT_BUTTON, self.OnBSingle) - hsizer.Add(self.bStart, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0) - - self.bLive = wx.Button(self, -1, 'Live', style=wx.BU_EXACTFIT) - self.bLive.Bind(wx.EVT_BUTTON, self.OnBLive) - hsizer.Add(self.bLive, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0) + if not (self.mode in ['compact']): + self.bStart = wx.Button(self, -1, 'Single Stack', style=wx.BU_EXACTFIT) + self.bStart.Bind(wx.EVT_BUTTON, self.OnBSingle) + hsizer.Add(self.bStart, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0) + + self.bLive = wx.Button(self, -1, 'Live', style=wx.BU_EXACTFIT) + self.bLive.Bind(wx.EVT_BUTTON, self.OnBLive) + hsizer.Add(self.bLive, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5, 0) vsizer.Add(hsizer, 0, wx.EXPAND, 0) - else: + elif self.mode == 'sequence': # sequence mode doesn't have dwell functionality yet hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'Dwell [frames]:'), 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 0) - self.t_zdwell = wx.TextCtrl(self, -1, value=str(self.scope.spoolController.z_dwell), size=(75,-1)) + self.t_zdwell = wx.TextCtrl(self, -1, value=str(self.scope.spoolController.protocol_settings.z_dwell), size=(75,-1)) self.t_zdwell.Bind(wx.EVT_KILL_FOCUS, self.OnDwellKillFocus) hsizer.Add(self.t_zdwell, 1, wx.ALIGN_CENTER_VERTICAL, 0) vsizer.Add(hsizer, 0, wx.EXPAND|wx.BOTTOM, 5) @@ -262,43 +264,67 @@ def OnBStartHereButton(self, event): + def _update_disp(self, *args, **kwargs): + wx.CallAfter(self.__update_disp) + + def __update_disp(self): + #print('vrf') + self._view.view.Redraw() + #print('vrf_') + + if self._aq_type_single: + self.dlgAqProg.Tick() + + def OnBSingle(self, event): self.OnBLive(event, True) #monkey patch in a progress panel - self.dlgAqProg = SeqProgressPanel(self.scope.zs) + self.dlgAqProg = SeqProgressPanel(self.scope.zs, self._view) self.pinfo1 = aui.AuiPaneInfo().Name("deconvPanel").Top().Caption('Acquisition Progress').DestroyOnClose(True).CloseButton(False)#.MinimizeButton(True).MinimizeMode(aui.AUI_MINIMIZE_CAPT_SMART|aui.AUI_MINIMIZE_POS_RIGHT)#.CaptionVisible(False) - self.scope.zs.view._mgr.AddPane(self.dlgAqProg, self.pinfo1) - self.scope.zs.view._mgr.Update() + self._view._mgr.AddPane(self.dlgAqProg, self.pinfo1) + self._view._mgr.Update() - #self.scope.zs.WantTickNotification.append(self.dlgAqProg.Tick) - self.scope.zs.onSingleFrame.connect(self.dlgAqProg.Tick) + + def _single_end(self): + self.__update_disp() + + self.bStart.Enable(True) + self.bLive.SetLabel('Live') + + #self.dlgAqProg.gProgress.Destroy() + self._view._mgr.ClosePane(self.pinfo1) + self._view._mgr.Update() + + def OnSingleEnd(self, **kwargs): #wx.MessageBox('Acquisition Finished') #self.scope.zs.WantFrameNotification.remove(self.OnSingleEnd) #self.scope.zs.WantTickNotification.remove(self.dlgAqProg.Tick) + self.scope.zs.onStack.disconnect(self.OnSingleEnd) - self.scope.zs.onSingleFrame.disconnect(self.dlgAqProg.Tick) + self.scope.frameWrangler.onFrameGroup.disconnect(self._update_disp) - self.bStart.Enable(True) - self.bLive.SetLabel('Live') + wx.CallAfter(self._single_end) - self.scope.zs.view._mgr.ClosePane(self.pinfo1) - self.scope.zs.view._mgr.Update() - print('se') + #print('se') def OnBLive(self, event, single=False): from PYME.Acquire import zScanner + from PYME.DSView import ViewIm3D + + self._aq_type_single = single if 'zs' in dir(self.scope) and self.scope.zs.running: #stop self.scope.zs.Stop() self.bLive.SetLabel('Live') self.bStart.Enable(True) + self.scope.frameWrangler.onFrameGroup.disconnect(self._update_disp) else: res = self.stackSettings.Verify() @@ -323,9 +349,13 @@ def OnBLive(self, event, single=False): self.scope.zs.Single() else: self.scope.zs.Start() + self.bLive.SetLabel('Stop') self.bStart.Enable(False) - + + self._view = ViewIm3D(self.scope.zs.img, 'Z Stack') + self.scope.frameWrangler.onFrameGroup.connect(self._update_disp) + else: dialog = wx.MessageDialog(None, res[2] + ' (%2.3f)'% res[3], "Parameter Error", wx.OK) dialog.ShowModal() @@ -388,12 +418,12 @@ def OnTStepSizeKillFocus(self, event): event.Skip() def OnDwellKillFocus(self, wx_event): - self.scope.spoolController.z_dwell = int(self.t_zdwell.GetValue()) + self.scope.spoolController.protocol_settings.z_dwell = int(self.t_zdwell.GetValue()) wx_event.Skip() def UpdateDisp(self): - print('seqd: update display') + #print('seqd: update display') try: self.chPiezo.SetSelection(self.scanDirs.index(self.stackSettings.GetScanChannel())) except ValueError: @@ -428,12 +458,12 @@ def UpdateDisp(self): if not self.mode == 'sequence': self.stMemory.SetLabel('Mem: %2.1f MB' % (self.scope.cam.GetPicWidth()*self.scope.cam.GetPicHeight()*self.stackSettings.GetSeqLength()*2*1/(1024.0*1024.0))) else: - self.t_zdwell.SetValue(str(self.scope.spoolController.z_dwell)) + self.t_zdwell.SetValue(str(self.scope.spoolController.protocol_settings.z_dwell)) class SeqProgressPanel(wx.Panel): - def __init__(self, zscanner): - wx.Panel.__init__(self, zscanner.view) + def __init__(self, zscanner, parent=None): + wx.Panel.__init__(self, parent) self.cancelled = False self.zs = zscanner @@ -443,7 +473,7 @@ def __init__(self, zscanner): self.gProgress = wx.Gauge(self, -1, self.zs.nz) - sizer1.Add(self.gProgress, 5, wx.EXPAND|wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) + sizer1.Add(self.gProgress, 5, wx.EXPAND| wx.ALL, 5) #btSizer = wx.StdDialogButtonSizer() diff --git a/PYME/Acquire/ui/splashScreen.py b/PYME/Acquire/ui/splashScreen.py index 66b423476..069ef0313 100755 --- a/PYME/Acquire/ui/splashScreen.py +++ b/PYME/Acquire/ui/splashScreen.py @@ -28,6 +28,7 @@ logger = logging.getLogger(__name__) from PYME import resources +from PYME.ui import wx_compat class SplashPanel(wx.Panel): def __init__(self, parent, scope, size=(-1,-1)): @@ -67,7 +68,7 @@ def DoPaint(self, dc): ts = dc.GetTextExtent('PYME Acquire') - dc.DrawText('PYME Acquire', self.Size[0]/2 - ts[0]/2, yp) + dc.DrawText('PYME Acquire', self.Size[0]//2 - ts[0]//2, yp) yp += ts[1] @@ -111,7 +112,7 @@ def drawProgBar(self, dc, x, y, width, height, frac): dc.DrawRectangle(x, y, width, height) dc.SetBrush(wx.Brush(wx.Colour(57, 76, 135))) - dc.DrawRectangle(x, y, width*frac, height) + dc.DrawRectangle(x, y, int(width*frac), height) dc.SetPen(pen) dc.SetBrush(brush) @@ -121,7 +122,7 @@ def OnPaint(self,event): #self.PrepareDC(DC) s = self.GetVirtualSize() - MemBitmap = wx.EmptyBitmap(s.GetWidth(), s.GetHeight()) + MemBitmap = wx_compat.EmptyBitmap(s.GetWidth(), s.GetHeight()) #del DC MemDC = wx.MemoryDC() OldBitmap = MemDC.SelectObject(MemBitmap) diff --git a/PYME/Acquire/ui/spool_panel.py b/PYME/Acquire/ui/spool_panel.py new file mode 100644 index 000000000..7adc493fc --- /dev/null +++ b/PYME/Acquire/ui/spool_panel.py @@ -0,0 +1,830 @@ +#!/usr/bin/python + +################## +# HDFSpoolFrame.py +# +# Copyright David Baddeley, 2009 +# d.baddeley@auckland.ac.nz +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +################## +"""The GUI controls for streaming acquisition. + +""" + +import wx +import datetime + +import PYME.Acquire.Protocols +from PYME.Acquire.SpoolController import SpoolController +#import PYME.Acquire.protocol_acquisition + +import os +import glob + +from PYME.IO import PZFFormat +import sys + +from PYME import config + +[wxID_FRSPOOL, wxID_FRSPOOLBSETSPOOLDIR, wxID_FRSPOOLBSTARTSPOOL, + wxID_FRSPOOLBSTOPSPOOLING, wxID_FRSPOOLCBCOMPRESS, wxID_FRSPOOLCBQUEUE, + wxID_FRSPOOLPANEL1, wxID_FRSPOOLSTATICBOX1, wxID_FRSPOOLSTATICBOX2, + wxID_FRSPOOLSTATICTEXT1, wxID_FRSPOOLSTNIMAGES, wxID_FRSPOOLSTSPOOLDIRNAME, + wxID_FRSPOOLSTSPOOLINGTO, wxID_FRSPOOLTCSPOOLFILE, +] = [wx.NewIdRef() for _init_ctrls in range(14)] + + +import PYME.ui.manualFoldPanel as afp +from PYME.ui import cascading_layout +from . import seqdialog +from . import AnalysisSettingsUI + +import logging +logger = logging.getLogger(__name__) + +class ProtocolAcquisitionPane(cascading_layout.CascadingLayoutPanel): + def _protocol_pan(self): + pan = wx.Panel(parent=self, style=wx.TAB_TRAVERSAL) + + vsizer = wx.BoxSizer(wx.VERTICAL) + + ### Aquisition Protocol + sbAP = wx.StaticBox(pan, -1, 'Aquisition Protocol') + APSizer = wx.StaticBoxSizer(sbAP, wx.VERTICAL) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.stAqProtocol = wx.StaticText(pan, -1, '', size=wx.Size(136, -1)) + hsizer.Add(self.stAqProtocol, 5, wx.ALL | wx.EXPAND, 2) + + self.bSetAP = wx.Button(pan, -1, 'Set', style=wx.BU_EXACTFIT) + self.bSetAP.Bind(wx.EVT_BUTTON, self.OnBSetAqProtocolButton) + + hsizer.Add(self.bSetAP, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + + APSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.rbNoSteps = wx.RadioButton(pan, -1, 'Standard', style=wx.RB_GROUP) + self.rbNoSteps.Bind(wx.EVT_RADIOBUTTON, self.OnToggleZStepping) + hsizer.Add(self.rbNoSteps, 1, wx.ALL | wx.EXPAND, 2) + self.rbZStepped = wx.RadioButton(pan, -1, 'Z stepped') + self.rbZStepped.Bind(wx.EVT_RADIOBUTTON, self.OnToggleZStepping) + hsizer.Add(self.rbZStepped, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + + if not hasattr(self.scope, 'stackSettings'): + self.rbZStepped.Disable() + + if self.protocol_settings.z_stepped: + self.rbZStepped.SetValue(True) + else: + self.rbNoSteps.SetValue(True) + + APSizer.Add(hsizer, 0, wx.TOP | wx.EXPAND, 4) + + + vsizer.Add(APSizer, 0, wx.TOP | wx.EXPAND, 4) + + pan.SetSizerAndFit(vsizer) + return pan + + def _init_ctrls(self): + vsizer = wx.BoxSizer(wx.VERTICAL) + vsizer.Add(self._protocol_pan(), 0, wx.ALL | wx.EXPAND, 0) + + if hasattr(self.scope, 'stackSettings'): + clp = afp.collapsingPane(self, caption='Z stepping ...') + self._seq_panel = seqdialog.seqPanel(clp, self.scope, mode='sequence') + clp.AddNewElement(self._seq_panel, priority=1) + vsizer.Add(clp, 0, wx.ALL | wx.EXPAND, 0) + #self.AddNewElement(clp, priority=1) + self.seq_pan = clp + + + self.SetSizerAndFit(vsizer) + #end analysis settings + + + def __init__(self, parent, scope, **kwargs): + """Initialise the spooling panel. + + Parameters + ---------- + parent : wx.Window derived class + The parent window + scope : microscope instance + The currently active microscope class (see microscope.py) + defDir : string pattern + The default directory to save data to. Any keys of the form `%()` + will be substituted using the values defined in `PYME.fileUtils.nameUtils.dateDict` + defSeries : string pattern + This specifies a pattern for file naming. Keys will be substituted as for `defDir` + + """ + #afp.foldingPane.__init__(self, parent, caption='Protocol Acquisition', **kwargs) + cascading_layout.CascadingLayoutPanel.__init__(self, parent, **kwargs) + self.scope = scope + self.spoolController = scope.spoolController + + self._init_ctrls() + + @property + def protocol_settings(self) -> PYME.Acquire.SpoolController.ProtocolAcquisitionSettings: + return self.scope.spoolController.protocol_settings + + def OnToggleZStepping(self, event): + self.protocol_settings.z_stepped = self.rbZStepped.GetValue() + pan = event.GetEventObject().GetParent() + if self.rbZStepped.GetValue(): + if self.seq_pan.folded: + self.seq_pan.OnFold() + + else: + if not self.seq_pan.folded: + self.seq_pan.OnFold() + + def OnBSetAqProtocolButton(self, event): + """Set the current protocol (GUI callback). + + See also: PYME.Acquire.Protocols.""" + from PYME.Acquire import protocol + pDlg = wx.SingleChoiceDialog(self, '', 'Select Protocol', protocol.get_protocol_list()) + + ret = pDlg.ShowModal() + #print 'Protocol choice: ', ret, wx.ID_OK + if ret == wx.ID_OK: + pname = pDlg.GetStringSelection() + self.spoolController.protocol_settings.set_protocol(pname) + # do this after setProtocol so that an error in SetProtocol avoids setting the new name + self.stAqProtocol.SetLabel(pname) + self._seq_panel.UpdateDisp() # update display of e.g. z_dwell + + pDlg.Destroy() + + + + +class SpoolingPane(afp.foldingPane): + """A Panel containing the GUI controls for spooling""" + + def _spool_to_pan(self): + pan = cascading_layout.CascadingLayoutPanel(parent=self, style=wx.TAB_TRAVERSAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + + ###Spool directory + sbSpoolDir = wx.StaticBox(pan, -1, 'Spool to:') + spoolDirSizer = wx.StaticBoxSizer(sbSpoolDir, wx.VERTICAL) + + #queues etcc + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self._spool_method_rbs = [] + for i, sm in enumerate(self.spoolController.available_spool_methods): + rb = wx.RadioButton(pan, -1, sm, style=(wx.RB_GROUP if i == 0 else 0)) + rb.Bind(wx.EVT_RADIOBUTTON, self.OnSpoolMethodChanged) + hsizer.Add(rb, 1, wx.ALL | wx.EXPAND, 2) + self._spool_method_rbs.append(rb) + + self._spool_method_rbs[self.spoolController.available_spool_methods.index(self.spoolController.spoolType)].SetValue(True) + + spoolDirSizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stSpoolDirName = wx.StaticText(pan, -1, 'Save images in: Blah Blah', size=wx.Size(136, -1)) + hsizer.Add(self.stSpoolDirName, 5, wx.ALL | wx.EXPAND, 5) + + self.bSetSpoolDir = wx.Button(pan, -1, 'Set', style=wx.BU_EXACTFIT) + self.bSetSpoolDir.Bind(wx.EVT_BUTTON, self.OnBSetSpoolDirButton) + + hsizer.Add(self.bSetSpoolDir, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + spoolDirSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0) + + ### Series Name & start button + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + #hsizer.Add(wx.StaticText(self, -1, 'Series: '), 0,wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5) + + self.tcSpoolFile = wx.TextCtrl(pan, -1, 'dd_mm_series_a', size=wx.Size(100, -1)) + self.tcSpoolFile.Bind(wx.EVT_TEXT, self.OnTcSpoolFileText) + + hsizer.Add(self.tcSpoolFile, 5, wx.ALL | wx.EXPAND, 5) + + + + # self.bStartStack = wx.Button(pan,-1,'Z-Series',style=wx.BU_EXACTFIT) + # self.bStartStack.Bind(wx.EVT_BUTTON, self.OnBStartStackButton) + # hsizer.Add(self.bStartStack, 0,wx.ALL|wx.ALIGN_CENTER_VERTICAL, 2) + + spoolDirSizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) + + self.stDiskSpace = wx.StaticText(pan, -1, 'Free space:') + spoolDirSizer.Add(self.stDiskSpace, 0, wx.ALL | wx.EXPAND, 2) + + ### Compression etc + clp = afp.collapsingPane(pan, caption='Compression ...', padding=2) + clp.AddNewElement(self._comp_pan(clp)) + spoolDirSizer.Add(clp, 0, wx.ALL | wx.EXPAND, 2) + + self._comp_p = clp + + vsizer.Add(spoolDirSizer, 0, wx.ALL | wx.EXPAND, 0) + + pan.SetSizerAndFit(vsizer) + return pan + + def _comp_pan(self, clp): + pan = cascading_layout.CascadingLayoutPanel(clp, -1) + + vsizer = wx.BoxSizer(wx.VERTICAL) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.cbCompress = wx.CheckBox(pan, -1, 'Compression') + self.cbCompress.SetValue(self.spoolController.hdf_compression_level > 0) + + hsizer.Add(self.cbCompress, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.cbQuantize = wx.CheckBox(pan, -1, 'Quantization') + self.cbQuantize.SetValue(config.get('spooler-quantize_by_default', False)) + + hsizer.Add(self.cbQuantize, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(pan, -1, 'Quantization offset:'), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + + self.tQuantizeOffset = wx.TextCtrl(pan, -1, 'auto') + hsizer.Add(self.tQuantizeOffset, 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + + vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(pan, -1, 'Quantization scale:'), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + + self.tQuantizeScale = wx.TextCtrl(pan, -1, '0.5') + self.tQuantizeScale.SetToolTip(wx.ToolTip( + 'Quantization scale in units of sigma\n. The default of 0.5 will give a quantization interval that is half the std dev. of the expected Poisson noise in a pixel.')) + hsizer.Add(self.tQuantizeScale, 1, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + + vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) + + # spool to h5 on cluster + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.cbClusterh5 = wx.CheckBox(pan, -1, 'Spool to h5 on cluster (cluster of 1)') + self.cbClusterh5.SetValue(self.spoolController.cluster_h5) + self.cbClusterh5.Bind(wx.EVT_CHECKBOX, lambda e: setattr(self.spoolController,'cluster_h5', self.cbClusterh5.GetValue())) + #self.cbClusterh5.SetValue(self._N_data_servers == 1) #set to true if we have a single node cluster + + hsizer.Add(self.cbClusterh5, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + vsizer.Add(hsizer, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 0) + pan.SetSizerAndFit(vsizer) + + #setup callbacks to update compression settings on UI change + self.cbCompress.Bind(wx.EVT_CHECKBOX, lambda e: self.update_spooler_compression_settings()) + self.cbQuantize.Bind(wx.EVT_CHECKBOX, lambda e: self.update_spooler_compression_settings()) + self.tQuantizeOffset.Bind(wx.EVT_KILL_FOCUS, lambda e: self.update_spooler_compression_settings()) + self.tQuantizeScale.Bind(wx.EVT_KILL_FOCUS, lambda e: self.update_spooler_compression_settings()) + + return pan + + + def _analysis_pan(self): + pan = cascading_layout.CascadingLayoutPanel(parent=self, style=wx.TAB_TRAVERSAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + + ###Spool directory + sbAnalysis = wx.StaticBox(pan, -1, 'Analysis:') + analysisSizer = wx.StaticBoxSizer(sbAnalysis, wx.VERTICAL) + + #analysis settings + if hasattr(self.scope, 'analysis_rules'): + # new style analysis rules + # analysis mode + hsizer = wx.BoxSizer(wx.HORIZONTAL) + #hsizer.Add(wx.StaticText(pan, -1, 'Analysis:'), 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + self.rbInteractiveAnalysis = wx.RadioButton(pan, -1, 'Interactive', style=wx.RB_GROUP) + self.rbInteractiveAnalysis.Bind(wx.EVT_RADIOBUTTON, self.OnToggleAnalysis) + self.rbInteractiveAnalysis.SetToolTip('Interactive analysis launches PYMEImage and allows you to test analysis parameters etc ... before running the analysis. It also shows a real-time update of localisations as they come in.') + hsizer.Add(self.rbInteractiveAnalysis, 1, wx.ALL | wx.EXPAND, 2) + self.rbAutomaticAnalysis = wx.RadioButton(pan, -1, 'Automatic') + self.rbAutomaticAnalysis.Bind(wx.EVT_RADIOBUTTON, self.OnToggleAnalysis) + self.rbAutomaticAnalysis.SetToolTip('Automatic analysis will runs analysis rules on the data with no user interaction. This is useful for high-throughput applications, but does not show progress updates.') + hsizer.Add(self.rbAutomaticAnalysis, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + if self.spoolController.analysis_mode == 'interactive': + self.rbInteractiveAnalysis.SetValue(True) + else: + self.rbAutomaticAnalysis.SetValue(True) + analysisSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0) + + + #analysis trigger + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(pan, -1, 'Start:'), 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + self.rbOnTrigger = wx.RadioButton(pan, -1, 'Trigger', style=wx.RB_GROUP) + self.rbOnTrigger.Bind(wx.EVT_RADIOBUTTON, self.OnToggleAnalysisTrigger) + self.rbOnTrigger.SetToolTip('Analysis is triggered by the protocol, or by manually clicking the `Analyse` button. Should be used if analysis should start before the series is complete.') + hsizer.Add(self.rbOnTrigger, 1, wx.ALL | wx.EXPAND, 2) + self.rbOnEnd = wx.RadioButton(pan, -1, 'Series end') + self.rbOnEnd.Bind(wx.EVT_RADIOBUTTON, self.OnToggleAnalysisTrigger) + self.rbOnEnd.SetToolTip('Analysis is triggered when the series is complete. Should be used if analysis should only start once all data is available.') + hsizer.Add(self.rbOnEnd, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + if self.spoolController.analysis_launch_mode == 'triggered': + self.rbOnTrigger.SetValue(True) + else: + self.rbOnEnd.SetValue(True) + analysisSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0) + + #analysis rule name + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stRuleChain = wx.StaticText(pan, -1, 'Rule chain:') + hsizer.Add(self.stRuleChain, 0, wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 5) + choices = [r for r in self.scope.analysis_rules.keys()] + self.cRuleChain = wx.Choice(pan, -1, choices=choices, size=(100, -1)) + self.cRuleChain.SetSelection(choices.index(self.spoolController.analysis_rule_name)) + self.cRuleChain.Bind(wx.EVT_CHOICE, self.OnRuleChainChanged) + hsizer.Add(self.cRuleChain, 1, wx.ALL | wx.EXPAND, 2) + analysisSizer.Add(hsizer, 0, wx.ALL | wx.EXPAND, 0) + + self._update_analysis_controls() + + + else: + clp = afp.collapsingPane(pan, caption='Real time analysis ...') + + self.scope.analysisSettings = AnalysisSettingsUI.AnalysisSettings() #Move me??? + + clp.AddNewElement(AnalysisSettingsUI.AnalysisSettingsPanel(clp, self.scope.analysisSettings, + self.scope.analysisSettings.onMetadataChanged)) + clp.AddNewElement(AnalysisSettingsUI.AnalysisDetailsPanel(clp, self.scope.analysisSettings, + self.scope.analysisSettings.onMetadataChanged)) + #self.AddNewElement(clp) + analysisSizer.Add(clp, 0, wx.ALL | wx.EXPAND, 0) + + vsizer.Add(analysisSizer, 0, wx.ALL | wx.EXPAND, 0) + pan.SetSizerAndFit(vsizer) + return pan + + + def OnToggleAnalysis(self, event): + self.spoolController.analysis_mode = 'interactive' if self.rbInteractiveAnalysis.GetValue() else 'rule-based' + self._update_analysis_controls() + + def OnToggleAnalysisTrigger(self, event): + self.spoolController.analysis_launch_mode = 'triggered' if self.rbOnTrigger.GetValue() else 'series-end' + + def OnRuleChainChanged(self, event): + self.spoolController.analysis_rule_name = self.cRuleChain.GetStringSelection() + + def _update_analysis_controls(self): + self.rbOnTrigger.SetValue(self.spoolController.analysis_launch_mode == 'triggered') + self.rbInteractiveAnalysis.SetValue(self.spoolController.analysis_mode == 'interactive') + + if hasattr(self.scope, 'analysis_rules'): + choices = [r for r in self.scope.analysis_rules.keys()] + self.cRuleChain.SetItems(choices) + self.cRuleChain.SetSelection(choices.index(self.spoolController.analysis_rule_name)) + + if self.spoolController.analysis_mode == 'interactive': + #self.stRuleChain.Hide() + #self.cRuleChain.Hide() + self.cRuleChain.Disable() + else: + #self.stRuleChain.Show() + #self.cRuleChain.Show() + self.cRuleChain.Enable() + + else: + self.cRuleChain.Disable() + #self.stRuleChain.Hide() + #self.cRuleChain.Hide() + + #self.cascading_layout() + + def _spool_pan(self): + pan = wx.Panel(parent=self, style=wx.TAB_TRAVERSAL) + vsizer = wx.BoxSizer(wx.VERTICAL) + + ### Spooling Progress + + self.spoolProgPan = wx.Panel(pan, -1) + vsizer_sp = wx.BoxSizer(wx.VERTICAL) + + self.sbSpoolProgress = wx.StaticBox(self.spoolProgPan, -1, 'Spooling Progress') + self.sbSpoolProgress.Enable(False) + + spoolProgSizer = wx.StaticBoxSizer(self.sbSpoolProgress, wx.VERTICAL) + + self.stSpoolingTo = wx.StaticText(self.spoolProgPan, -1, 'Spooling to .....') + spoolProgSizer.Add(self.stSpoolingTo, 0, wx.ALL, 0) + + self.stNImages = wx.StaticText(self.spoolProgPan, -1, 'NNN images spooled in MM mins') + self.stSpoolingTo.SetForegroundColour(wx.TheColourDatabase.Find('GREY')) + self.stNImages.SetForegroundColour(wx.TheColourDatabase.Find('GREY')) + + spoolProgSizer.Add(self.stNImages, 0, wx.ALL, 0) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.bStartSpool = wx.Button(self.spoolProgPan, -1, 'Start', style=wx.BU_EXACTFIT) + self.bStartSpool.Bind(wx.EVT_BUTTON, self.OnStartStopButton) + #self.bStartSpool.SetDefault() + hsizer.Add(self.bStartSpool, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + + self.bView = wx.Button(self.spoolProgPan, -1, 'View', style=wx.BU_EXACTFIT) + self.bView.Bind(wx.EVT_BUTTON, self.OnViewButton) + hsizer.Add(self.bView, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + self.bView.Enable(False) + + # self.bStopSpooling = wx.Button(self.spoolProgPan, -1, 'Stop', style=wx.BU_EXACTFIT) + # self.bStopSpooling.Enable(False) + # self.bStopSpooling.Bind(wx.EVT_BUTTON, self.OnBStopSpoolingButton) + + # hsizer.Add(self.bStopSpooling, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.bAnalyse = wx.Button(self.spoolProgPan, -1, 'Analyse', style=wx.BU_EXACTFIT) + self.bAnalyse.Enable(False) + self.bAnalyse.Bind(wx.EVT_BUTTON, self.OnBAnalyse) + + hsizer.Add(self.bAnalyse, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + spoolProgSizer.Add(hsizer, 0, wx.ALL | wx.ALIGN_CENTER_HORIZONTAL, 0) + + vsizer_sp.Add(spoolProgSizer, 0, wx.ALL | wx.EXPAND, 0) + + self.spoolProgPan.SetSizerAndFit(vsizer_sp) + + vsizer.Add(self.spoolProgPan, 0, wx.ALL | wx.EXPAND, 0) + + pan.SetSizerAndFit(vsizer) + return pan + + def _aq_settings_pan(self): + pan = cascading_layout.CascadingLayoutPanel(parent=self, style=wx.TAB_TRAVERSAL) + v1 = wx.BoxSizer(wx.VERTICAL) + vsizer = wx.StaticBoxSizer(wx.StaticBox(pan, label='Acquisition Type'), wx.VERTICAL) + + self._aq_type_btns = [] + + _settings_box_name = '' + + for i, aq_type in enumerate(self.spoolController.acquisition_types): + try: + pane, name = self.acquisition_uis[aq_type] + btn = wx.RadioButton(pan, -1, name, style=(wx.RB_GROUP if i == 0 else 0)) + btn.aq_type = aq_type + btn.SetValue(aq_type == self.spoolController.acquisition_type) + if aq_type == self.spoolController.acquisition_type: + _settings_box_name = f'{name} Settings' + btn.Bind(wx.EVT_RADIOBUTTON, self.OnAqTypeChanged) + vsizer.Add(btn, 0, wx.ALL | wx.EXPAND, 2) + self._aq_type_btns.append(btn) + except KeyError: + logger.warn(f'No UI defined for acquisition type {aq_type}') + + + v1.Add(vsizer, 0, wx.ALL | wx.EXPAND, 0) + self.sbAqSettings = wx.StaticBox(pan, -1, _settings_box_name) + vsizer = wx.StaticBoxSizer(self.sbAqSettings, wx.VERTICAL) + + for i, aq_type in enumerate(self.spoolController.acquisition_types): + try: + pane, name = self.acquisition_uis[aq_type] + + pane.Reparent(pan) + vsizer.Add(pane, 1, wx.ALL | wx.EXPAND, 2) + pane.Show(aq_type == self.spoolController.acquisition_type) + except KeyError: + pass + + v1.Add(vsizer, 0, wx.ALL | wx.EXPAND, 0) + pan.SetSizerAndFit(v1) + + self.aq_settings_pan = pan + return pan + + def OnAqTypeChanged(self, event): + #btn = event.GetEventObject() + #if btn.GetValue(): + self.spoolController.acquisition_type = event.GetEventObject().aq_type + self.on_aq_type_changed() + + def on_aq_type_changed(self): + for btn in self._aq_type_btns: + btn.SetValue(btn.aq_type == self.spoolController.acquisition_type) + + for aq_type in self.spoolController.acquisition_types: + self.acquisition_uis[aq_type][0].Show(aq_type == self.spoolController.acquisition_type) + if (aq_type == self.spoolController.acquisition_type): + self.sbAqSettings.SetLabel(f'{self.acquisition_uis[aq_type][1]} Settings') + + self.aq_settings_pan.cascading_layout() + #self.cascading_layout() + + def _init_ctrls(self): + + self.AddNewElement(self._aq_settings_pan(), priority=1, foldable=False) + self._pan_spool_to = self._spool_to_pan() + self.AddNewElement(self._pan_spool_to) + self.AddNewElement(self._analysis_pan()) + + self.AddNewElement(self._spool_pan()) + + + + + def __init__(self, parent, scope, acquisition_uis = {}, **kwargs): + """Initialise the spooling panel. + + Parameters + ---------- + parent : wx.Window derived class + The parent window + scope : microscope instance + The currently active microscope class (see microscope.py) + defDir : string pattern + The default directory to save data to. Any keys of the form `%()` + will be substituted using the values defined in `PYME.fileUtils.nameUtils.dateDict` + defSeries : string pattern + This specifies a pattern for file naming. Keys will be substituted as for `defDir` + + """ + afp.foldingPane.__init__(self, parent, caption='Spooling', **kwargs) + self.scope = scope + self.spoolController = scope.spoolController + self.acquisition_uis = acquisition_uis + + #check to see if we have a cluster + #self._N_data_servers = len(hybrid_ns.getNS('_pyme-http').get_advertised_services()) + + self._init_ctrls() + + #self.spoolController = SpoolController(scope, defDir, **kwargs) + + self.spoolController.onSpoolProgress.connect(self._tick) + self.spoolController.onSpoolStart.connect(self.OnSpoolingStarted) + self.spoolController.on_stop.connect(self.OnSpoolingStopped) + + self.stSpoolDirName.SetLabel(self.spoolController.display_dirname) + self.tcSpoolFile.SetValue(self.spoolController.seriesName) + self.UpdateFreeSpace() + + #update the spool method (specifically so that the default in the GUI and spool controller match) + #self.OnSpoolMethodChanged(None) + + #make default compression settings in spooler match the display. + self.update_spooler_compression_settings(False) + + + def update_spooler_compression_settings(self, ui_on_error=True): + try: + self.spoolController.pzf_compression_settings = self.get_compression_settings(ui_on_error) + self.spoolController.hdf_compression_level = 2 if self.cbCompress.GetValue() else 0 + except: + logger.warn('Compression settings invalid, disabling quantization') + if ui_on_error: + ans = wx.MessageBox( + "Compression settings invalid, disabling quantization", + 'Error', wx.OK) + self.cbQuantize.SetValue(False) + self.spoolController.pzf_compression_settings = self.get_compression_settings() + + def _get_spool_method(self): + for i, rb in enumerate(self._spool_method_rbs): + if rb.GetValue(): + return self.spoolController.available_spool_methods[i] + + return None + + def UpdateFreeSpace(self, event=None): + """Updates the free space display. + + Designed to be used as a callback with one of the system timers, but + can be called separately + """ + freeGB = self.spoolController.get_free_space() + self.stDiskSpace.SetLabel('Free Space: %3.2f GB' % freeGB) + if freeGB < 5: + self.stDiskSpace.SetForegroundColour(wx.Colour(200, 0,0)) + else: + self.stDiskSpace.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + + + + def get_compression_settings(self, ui_message_on_error=True): + if not self.cbQuantize.GetValue(): + compSettings = { + 'compression': PZFFormat.DATA_COMP_HUFFCODE if self.cbCompress.GetValue() else PZFFormat.DATA_COMP_RAW, + 'quantization': PZFFormat.DATA_QUANT_NONE, + 'quantizationOffset': 0.0, + 'quantizationScale': 1.0 + } + + return compSettings + + else: + #try and set our quantization offset automatically as the AD offset of the camera + q_offset = self.tQuantizeOffset.GetValue() + if q_offset == 'auto': + #FIXME - add getter to camera??? + try: + q_offset = self.scope.cam.noise_properties['ADOffset'] + except AttributeError: + if ui_message_on_error: + ans = wx.MessageBox( + "Camera doesn't define noise properties, manually set the desired quantization offset", + 'Error', wx.OK) + raise + else: + q_offset = float(q_offset) + + #quantization scale in GUI is in units of sigma, convert to ADU + try: + q_scale = float(self.tQuantizeScale.GetValue()) / self.scope.cam.noise_properties['ElectronsPerCount'] + except (AttributeError, NotImplementedError): + logger.warning("Camera doesn't provide electrons per count, using qscale in units of ADUs instead") + q_scale = float(self.tQuantizeScale.GetValue()) + + compSettings = { + 'compression': PZFFormat.DATA_COMP_HUFFCODE if self.cbCompress.GetValue() else PZFFormat.DATA_COMP_RAW, + 'quantization': PZFFormat.DATA_QUANT_SQRT, + 'quantizationOffset': q_offset, + 'quantizationScale': q_scale + } + + return compSettings + + + def OnBStartSpoolButton(self, event=None, stack=False): + """GUI callback to start spooling. + + NB: this is also called programatically by the start stack button.""" + + #if self.rbZStepped.GetValue(): + # stack = True + + fn = self.tcSpoolFile.GetValue() + + if fn == '': #sanity checking + wx.MessageBox('Please enter a series name', 'No series name given', wx.OK) + return #bail + + # if self.cbCompress.GetValue(): + # compLevel = 2 + # else: + # compLevel = 0 + + + try: + self.spoolController.start_spooling(fn) + return True + except IOError as e: + logger.exception('IO error whilst spooling') + ans = wx.MessageBox(str(e.strerror), 'Error', wx.OK) + self.tcSpoolFile.SetValue(self.spoolController.seriesName) + return False + + def OnStartStopButton(self, event=None): + if self.bStartSpool.GetLabel() == 'Start': + self.OnBStartSpoolButton(event) + else: + self.OnBStopSpoolingButton(event) + + def update_ui(self): + self.cbCompress.SetValue(self.spoolController.hdf_compression_level > 0) + + + + def OnSpoolingStarted(self, **kwargs): + if self.spoolController.spoolType in ['Queue', 'Cluster']: + self.bAnalyse.SetLabel('Analyse') + self.bAnalyse.Enable() + self.bView.Enable() # todo - does this make sense for tiling? + else: + self.bView.Enable(False) + + + #self.bStartSpool.Enable(False) + #self.bStartStack.Enable(False) + #self.bStopSpooling.Enable(True) + self.bStartSpool.SetLabel('Stop') + #self.stSpoolingTo.Enable(True) + #self.stNImages.Enable(True) + self.stSpoolingTo.SetForegroundColour(None) + self.stNImages.SetForegroundColour(None) + self.stSpoolingTo.SetLabel('Spooling to ' + self.spoolController.seriesName) + self.stNImages.SetLabel('0 images spooled in 0 minutes') + + + def OnBStopSpoolingButton(self, event): + """GUI callback to stop spooling.""" + self.spoolController.StopSpooling() + #self.OnSpoolingStopped() + + def OnSpoolingStopped(self, **kwargs): + self.bStartSpool.SetLabel('Start') + self.bView.Enable(True) + #self.bStartSpool.Enable(True) + #self.bStartStack.Enable(True) + #self.bStopSpooling.Enable(False) + #self.stSpoolingTo.Enable(False) + #self.stNImages.Enable(False) + self.stSpoolingTo.SetForegroundColour(wx.TheColourDatabase.Find('GREY')) + self.stNImages.SetForegroundColour(wx.TheColourDatabase.Find('GREY')) + + self.stSpoolDirName.SetLabel(self.spoolController.display_dirname) + self.tcSpoolFile.SetValue(self.spoolController.seriesName) + self.UpdateFreeSpace() + + + wx.CallAfter(self.Refresh) + #self.Refresh() + #self.Update() + #self.cascading_layout() + + def OnBAnalyse(self, event): + if self.bAnalyse.GetLabel() == 'Analyse': + self.spoolController.launch_analysis() + if self.spoolController.analysis_mode == 'rule-based': + self.bAnalyse.SetLabel('View Results') + else: + self.spoolController.open_analysis() + + def OnViewButton(self, event): + self.spoolController.open_view() + + + + def _tick(self, **kwargs): + wx.CallAfter(self.Tick) + + def Tick(self, **kwargs): + """Called with each new frame. Updates the number of frames spooled + and disk space remaining""" + dtn = datetime.datetime.now() + + dtt = dtn - self.spoolController.spooler.dtStart + + self.stNImages.SetLabel('%d images spooled in %d seconds' % (self.spoolController.spooler.frame_num, dtt.seconds)) + self.UpdateFreeSpace() + + def OnBSetSpoolDirButton(self, event): + """Set the directory we're spooling into (GUI callback).""" + ndir = wx.DirSelector() + if not ndir == '': + logger.debug('series name %s' % self.spoolController.seriesName) + self.spoolController.SetSpoolDir(ndir) + self.stSpoolDirName.SetLabel(self.spoolController.display_dirname) + self.tcSpoolFile.SetValue(self.spoolController.seriesName) + logger.debug('series name %s' % self.spoolController.seriesName) + + self.UpdateFreeSpace() + + def OnTcSpoolFileText(self, event): + fn = self.tcSpoolFile.GetValue() + if not fn == '': + self.spoolController.seriesName = fn + event.Skip() + + def OnSpoolMethodChanged(self, event): + self.spoolController.SetSpoolMethod(self._get_spool_method()) + if self.spoolController.spoolType == 'Memory': + self.stSpoolDirName.Hide() + self.tcSpoolFile.Hide() + self.stDiskSpace.Hide() + self.bSetSpoolDir.Hide() + else: + self.stSpoolDirName.Show() + self.tcSpoolFile.Show() + self.stDiskSpace.Show() + self.bSetSpoolDir.Show() + + if self.spoolController.spoolType in ['Cluster', 'File']: + self._comp_p.Show() + else: + self._comp_p.Hide() + + self._pan_spool_to.cascading_layout() + + self.stSpoolDirName.SetLabel(self.spoolController.display_dirname) + self.tcSpoolFile.SetValue(self.spoolController.seriesName) + + self.UpdateFreeSpace() + + + + diff --git a/PYME/Acquire/ui/tile_panel.py b/PYME/Acquire/ui/tile_panel.py index 5c346165a..6523e916f 100644 --- a/PYME/Acquire/ui/tile_panel.py +++ b/PYME/Acquire/ui/tile_panel.py @@ -1,5 +1,10 @@ import wx from PYME.Acquire.Utils import tiler +import logging +import os + +logger = logging.getLogger(__name__) + class TilePanel(wx.Panel): def __init__(self, parent, scope): @@ -35,6 +40,19 @@ def __init__(self, parent, scope): hsizer2.Add(self.tYTiles, 0, wx.ALL, 2) vsizer.Add(hsizer2) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + + self.rbSpoolFile = wx.RadioButton(self, -1, 'File', style=wx.RB_GROUP) + #self.rbSpoolFile.Bind(wx.EVT_RADIOBUTTON, self.OnSpoolMethodChanged) + hsizer.Add(self.rbSpoolFile, 1, wx.ALL | wx.EXPAND, 2) + self.rbSpoolCluster = wx.RadioButton(self, -1, 'Cluster') + #self.rbSpoolCluster.Bind(wx.EVT_RADIOBUTTON, self.OnSpoolMethodChanged) + hsizer.Add(self.rbSpoolCluster, 1, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + + self.rbSpoolFile.SetValue(True) + + vsizer.Add(hsizer, 0, wx.EXPAND, 0) + hsizer2 = wx.BoxSizer(wx.HORIZONTAL) hsizer2.Add(wx.StaticText(self, -1, 'Save to:'), 0, wx.ALL, 2) self.tDestination = wx.TextCtrl(self, -1, value='') @@ -62,21 +80,29 @@ def __init__(self, parent, scope): self.SetSizerAndFit(vsizer) - def OnGo(self, event=None): - # run a triggered tile acquisition if the camera is capable - # FIXME - the hasattr test becomes problematic once we add FireSoftwareTrigger to our base camera class (to - # document API) - trigger = hasattr(self.scope.cam, 'FireSoftwareTrigger') + def OnGo(self, event=None): + backend = 'file' + if self.rbSpoolCluster.GetValue(): + backend = 'cluster' + + tile_dir = self.tDestination.GetValue() + if not os.path.isabs(tile_dir): + #make relative to the current spooler directory + try: + tile_dir = os.path.join(self.scope.spoolController.get_dirname(spoolType=backend), tile_dir) + except AttributeError: + #just in case there is no spool controller + pass - self.scope.tiler = tiler.Tiler(self.scope, tile_dir = self.tDestination.GetValue(), + self.scope.tiler = tiler.Tiler(self.scope, tile_dir = tile_dir, n_tiles=(int(self.tXTiles.GetValue()), int(self.tYTiles.GetValue())), - trigger=trigger) + backend=backend) self.bStop.Enable() self.bGo.Disable() self.scope.tiler.on_stop.connect(self._on_stop) - self.scope.tiler.progress.connect(self._update) + self.scope.tiler.on_progress.connect(self._update) self.scope.tiler.start() @@ -91,11 +117,11 @@ def _on_stop(self, *args, **kwargs): self.bGo.Enable() self.scope.tiler.on_stop.disconnect(self._on_stop) - self.scope.tiler.progress.disconnect(self._update) + self.scope.tiler.on_progress.disconnect(self._update) # FIXME - previous delay was 1e3, which seems more reasonable. Do we need a config option (or heuristic) here ? # assume this change was due to the time it takes to build a pyramid after tiling ends. Might ultimately be fixed when we revisit live tiling. - wx.CallAfter(wx.CallLater,1e4, self._launch_viewer) + wx.CallAfter(wx.CallLater,1e3, self._launch_viewer) def _launch_viewer(self): @@ -106,7 +132,7 @@ def _launch_viewer(self): import requests import os - self.scope.tiler.P.update_pyramid() + #self.scope.tiler.P.update_pyramid() #if not self._gui_proc is None: # self._gui_proc.kill() @@ -190,7 +216,7 @@ def OnGo(self, event=None): self.bGo.Disable() self.scope.tiler.on_stop.connect(self._on_stop) - self.scope.tiler.progress.connect(self._update) + self.scope.tiler.on_progress.connect(self._update) self.scope.tiler.start() @@ -270,7 +296,7 @@ def OnGo(self, event=None): self.bGo.Disable() # self.scope.tiler.on_stop.connect(self._on_stop) - # self.scope.tiler.progress.connect(self._update) + # self.scope.tiler.on_progress.connect(self._update) self.scope.tiler.start() def OnStop(self, event=None): @@ -278,10 +304,15 @@ def OnStop(self, event=None): class MultiwellProtocolQueuePanel(wx.Panel): + # TODO - refactor into Acquire.htsms - this is aquiring a bit of cruft that we probably don't want to support in the long term def __init__(self, parent, scope): wx.Panel.__init__(self, parent) self.scope=scope + self._shame_index = 0 + self.scope.multiwellpanel = self + self._drop_wells = [] + self.fast_axis = 'y' vsizer = wx.BoxSizer(wx.VERTICAL) @@ -311,10 +342,23 @@ def __init__(self, parent, scope): hsizer = wx.BoxSizer(wx.HORIZONTAL) hsizer.Add(wx.StaticText(self, -1, 'Nice:'), 0, wx.ALL, 2) - self.nice = wx.TextCtrl(self, -1, value='%d' % 20) + self.nice = wx.TextCtrl(self, -1, value='%d' % 11) hsizer.Add(self.nice, 0, wx.ALL, 2) vsizer.Add(hsizer) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.axis_select_box = wx.ComboBox(self, -1, choices=['x', 'y'], + value='y', size=(65, -1), + style=wx.CB_DROPDOWN | wx.TE_PROCESS_ENTER) + + hsizer.Add(self.axis_select_box, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 2) + self.axis_select_box.Bind(wx.EVT_COMBOBOX, self._on_combo_box) + + self.cb_get_it_done = wx.CheckBox(self, -1, 'Requeue missed') + hsizer.Add(self.cb_get_it_done, 0, wx.ALL, 2) + hsizer.Fit(self) + vsizer.Add(hsizer) + hsizer = wx.BoxSizer(wx.HORIZONTAL) self.queue_button = wx.Button(self, -1, 'Queue') # self.bGo.Disable() @@ -327,9 +371,71 @@ def __init__(self, parent, scope): vsizer.Add(hsizer) self.SetSizerAndFit(vsizer) + + def _on_combo_box(self, event): + cb = event.GetEventObject() + self.fast_axis = cb.GetValue() + + def requeue_missed(self, n_x, n_y, x_spacing, y_spacing, start_pos, protocol_name, nice=20, sleep=1000): + from PYME.Acquire.actions import FunctionAction + from PYME.IO import clusterIO + import posixpath + import time + + logger.debug('requeuing missed wells') + time.sleep(sleep) + + x_wells, y_wells, names = self._get_positions(n_x, n_y, x_spacing, y_spacing, start_pos) + x_wells, y_wells, names = self._pop_wells(x_wells, y_wells, names, self._drop_wells) + + # if a node dies we might lose the detections file, but likely won't + # lose the entire subdirectory + spooldir = self.scope.spoolController.dirname + to_pop = set() + for name in names: + if clusterIO.isdir(posixpath.join(spooldir, name)): + to_pop.add(name) + else: + for shame in range(1, self._shame_index): + if clusterIO.isdir(posixpath.join(spooldir, name + '_%d' % shame)): + to_pop.add(name) + logger.debug('subdirectories present for %d wells' % (len(names) - len(to_pop))) + + # Look for h5 detections too - if we just tiled the last well and hit + # this call we might have a detection file after the sleep but this + # call blocks the acquisition task queue, so we can't have a subdir yet + imaged = clusterIO.cglob(posixpath.join(spooldir, + '[A-Z][0-9]*_detections.h5')) + imaged_wells = [im.split('/')[-1].split('_detections.h5')[0] for im in imaged] + detected = set([fn.split('_')[0] for fn in imaged_wells]) + logger.debug('detection h5 present for an additional %d wells' % (len(detected) - len(to_pop.intersection(detected)))) + to_pop = to_pop.union(detected) + x_wells, y_wells, names = self._pop_wells(x_wells, y_wells, names, list(to_pop)) + + if len(names) < 1: + return + + self._shame_index += 1 + shame_suffix = '_%d' % self._shame_index + names = [name + shame_suffix for name in names] + actions = self._get_action_list(x_wells, y_wells, names, protocol_name) + + actions.append(FunctionAction('turnAllLasersOff', {})) + + # lets just make it recursive for fun + if self.cb_get_it_done.GetValue(): + actions.append(FunctionAction('multiwellpanel.requeue_missed', + {'n_x': n_x, 'n_y': n_y, + 'x_spacing': x_spacing, 'y_spacing': y_spacing, + 'start_pos': start_pos, + 'protocol_name': protocol_name, + 'nice': nice})) + + logger.debug('requeuing %d wells' % len(names)) + self.scope.actions.queue_actions(actions, nice) def OnQueue(self, event=None): - import numpy as np + from PYME.Acquire.actions import FunctionAction from PYME.Acquire import protocol tile_protocols = [p for p in protocol.get_protocol_list() if 'tile' in p] @@ -344,6 +450,8 @@ def OnQueue(self, event=None): protocol_name = dialog.GetStringSelection() dialog.Destroy() + self._shame_index = 0 + x_spacing = float(self.x_spacing_mm.GetValue()) * 1e3 # [mm -> um] y_spacing = float(self.y_spacing_mm.GetValue()) * 1e3 # [mm -> um] n_x = int(self.n_x.GetValue()) @@ -352,6 +460,24 @@ def OnQueue(self, event=None): curr_pos = self.scope.GetPos() + x_wells, y_wells, names = self._get_positions(n_x, n_y, x_spacing, y_spacing, curr_pos) + x_wells, y_wells, names = self._pop_wells(x_wells, y_wells, names, self._drop_wells) + actions = self._get_action_list(x_wells, y_wells, names, protocol_name) + + actions.append(FunctionAction('turnAllLasersOff', {})) + + if self.cb_get_it_done.GetValue(): + actions.append(FunctionAction('multiwellpanel.requeue_missed', + {'n_x': n_x, 'n_y': n_y, + 'x_spacing': x_spacing, 'y_spacing': y_spacing, + 'start_pos': curr_pos, + 'protocol_name': protocol_name, + 'nice': nice})) + + self.scope.actions.queue_actions(actions, nice) + + def _get_positions(self, n_x, n_y, x_spacing, y_spacing, start_pos): + import numpy as np # TODO - making this more flexible orientation wise, numbering for e.g. # 384wp, etc.. This puts H1 of a 96er at the min x, min y well. xind_names = np.array([chr(ord('@') + n) for n in range(1, n_x + 1)[::-1]]) @@ -360,31 +486,71 @@ def OnQueue(self, event=None): x_w = np.arange(0, n_x * x_spacing, x_spacing) y_w = np.arange(0, n_y * y_spacing, y_spacing) - x_wells = [] - y_wells = np.repeat(y_w, n_x) - names = [] - # zig-zag with turns along x - for xi in range(n_y): - if xi % 2: - x_wells.extend(x_w[::-1]) - names.extend([xi_name + yind_names[xi] for xi_name in xind_names[::-1]]) - else: - x_wells.extend(x_w) - names.extend([xi_name + yind_names[xi] for xi_name in xind_names]) - x_wells = np.asarray(x_wells) + + if self.fast_axis == 'x': + # zig-zag with turns along x + x_wells = [] + y_wells = np.repeat(y_w, n_x) + names = [] + for xi in range(n_y): + if xi % 2: + x_wells.extend(x_w[::-1]) + names.extend([xi_name + yind_names[xi] for xi_name in xind_names[::-1]]) + else: + x_wells.extend(x_w) + names.extend([xi_name + yind_names[xi] for xi_name in xind_names]) + x_wells = np.asarray(x_wells) + else: + # zig-zag with turns along y + y_wells = [] + x_wells = np.repeat(x_w, n_y) + names = [] + for yi in range(n_x): + if yi % 2: + y_wells.extend(y_w[::-1]) + names.extend([xind_names[yi] + yi_name for yi_name in yind_names[::-1]]) + else: + y_wells.extend(y_w) + names.extend([xind_names[yi] + yi_name for yi_name in yind_names]) + y_wells = np.asarray(y_wells) # add the current scope position offset - x_wells += curr_pos['x'] - y_wells += curr_pos['y'] + x_wells += start_pos['x'] + y_wells += start_pos['y'] - # queue them all + return x_wells, y_wells, names + + + def _get_action_list(self, x_wells, y_wells, names, protocol_name): + from PYME.Acquire.actions import UpdateState, SpoolSeries + actions = list() for x, y, filename in zip(x_wells, y_wells, names): - args = {'state': {'Positioning.x': x, 'Positioning.y': y}} - self.scope.actions.QueueAction('state.update', args, nice) - args = {'protocol': protocol_name, 'stack': False, - 'doPreflightCheck':False, 'fn': filename} - self.scope.actions.QueueAction('spoolController.StartSpooling', - args, nice) - self.scope.actions.QueueAction('turnAllLasersOff', - {}, nice) + state = UpdateState(state={'Positioning.x': x, 'Positioning.y': y}) + spool = SpoolSeries(fn=filename, + settings=dict(protocol_name=protocol_name, + z_stepped=False), + preflight_mode='warn') + actions.append(state.then(spool)) + return actions + + def _pop_wells(self, x_wells, y_wells, names, to_pop): + import numpy as np + + pop_inds = [] + for well in to_pop: + try: + pop_inds.append(names.index(well)) + except ValueError: + pass + + if len(pop_inds) < 1: + return x_wells, y_wells, names + x_wells = x_wells.tolist() + y_wells = y_wells.tolist() + + for pfn in sorted(pop_inds)[::-1]: + x_wells.pop(pfn) + y_wells.pop(pfn) + names.pop(pfn) + return np.asarray(x_wells), np.asarray(y_wells), names diff --git a/PYME/Acquire/ui/tilesettingsui.py b/PYME/Acquire/ui/tilesettingsui.py new file mode 100644 index 000000000..ac839e8bd --- /dev/null +++ b/PYME/Acquire/ui/tilesettingsui.py @@ -0,0 +1,91 @@ +import wx +from PYME.ui import cascading_layout + + +class TileSettingsUI(cascading_layout.CascadingLayoutPanel): + def __init__(self, parent, scope, **kwargs): + cascading_layout.CascadingLayoutPanel.__init__(self, parent, **kwargs) + self.scope = scope + + if not hasattr(self.scope, 'tile_settings'): + self.scope.tile_settings = {'n_tiles': (10, 10)} + + vsizer = wx.BoxSizer(wx.VERTICAL) + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, label='# steps - x:'), 0, wx.ALIGN_CENTER_VERTICAL) + self.tXSteps = wx.TextCtrl(self, value='10', size=(40, -1)) + self.tXSteps.Bind(wx.EVT_TEXT, self.update_settings) + hsizer.Add(self.tXSteps, 1, wx.ALIGN_CENTER_VERTICAL) + #vsizer.Add(hsizer, 0, wx.EXPAND) + + #hsizer = wx.BoxSizer(wx.HORIZONTAL) + #hsizer.Add(wx.StaticText(self, label='# y steps'), 0, wx.ALIGN_CENTER_VERTICAL) + hsizer.Add(wx.StaticText(self, label=', y:'), 0, wx.ALIGN_CENTER_VERTICAL) + self.tYSteps = wx.TextCtrl(self, value='10', size=(40, -1)) + self.tYSteps.Bind(wx.EVT_TEXT, self.update_settings) + hsizer.Add(self.tYSteps, 1, wx.ALIGN_CENTER_VERTICAL) + vsizer.Add(hsizer, 0, wx.EXPAND) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + hsizer.Add(wx.StaticText(self, label='Tile spacing:'), 0, wx.ALIGN_CENTER_VERTICAL) + self.tTileSpacing = wx.TextCtrl(self, value='0.8', size=(40, -1)) + self.tTileSpacing.Bind(wx.EVT_TEXT, self.update_settings) + self.tTileSpacing.SetToolTip('Spacing between tiles as a fraction of the tile size') + hsizer.Add(self.tTileSpacing, 1, wx.ALIGN_CENTER_VERTICAL) + vsizer.Add(hsizer, 0, wx.EXPAND|wx.TOP, 2) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + self.stRegionSize = wx.StaticText(self, label='Region size: 0.0 x 0.0 um') + hsizer.Add(self.stRegionSize, 0, wx.ALIGN_CENTER_VERTICAL) + vsizer.Add(hsizer, 0, wx.EXPAND|wx.TOP, 5) + + hsizer = wx.BoxSizer(wx.HORIZONTAL) + #hsizer.Add(wx.StaticText(self, label=''), 0, wx.ALIGN_CENTER_VERTICAL) + self.cbSaveRaw = wx.CheckBox(self, label='Save raw frames') + self.cbSaveRaw.SetValue(False) + self.cbSaveRaw.SetToolTip('Save raw frames in addition to the stitched image. Can be useful for debugging and/or high-precision alignment') + self.cbSaveRaw.Bind(wx.EVT_CHECKBOX, self.update_settings) + hsizer.Add(self.cbSaveRaw, 0, wx.ALIGN_CENTER_VERTICAL) + vsizer.Add(hsizer, 0, wx.EXPAND|wx.TOP, 5) + + self.update_from_settings() + self.update_region_size() + + self.SetSizerAndFit(vsizer) + + def update_from_settings(self): + xs, ys = self.scope.tile_settings.get('n_tiles', (10, 10)) + self.tXSteps.SetValue(str(xs)) + self.tYSteps.SetValue(str(ys)) + self.tTileSpacing.SetValue('%3.2f' %self.scope.tile_settings.get('tile_spacing', 0.8)) + + def update_settings(self, event=None): + self.scope.tile_settings['n_tiles'] = (int(self.tXSteps.GetValue()), int(self.tYSteps.GetValue())) + self.scope.tile_settings['tile_spacing'] = float(self.tTileSpacing.GetValue()) + self.scope.tile_settings['save_raw'] = self.cbSaveRaw.GetValue() + + self.update_region_size() + + def update_region_size(self): + import numpy as np + n = np.array(self.scope.tile_settings.get('n_tiles', (10, 10))) + sp = self.scope.tile_settings.get('tile_spacing', 0.8) + fs = np.array(self.scope.frameWrangler.currentFrame.shape[:2])*np.array(self.scope.GetPixelSize()) + #print(((n-1) * sp * fs + fs)) + self.stRegionSize.SetLabel('Region size: %3.1f x %3.1f um' % tuple(n * sp * fs + fs)) + + +class ZTileSettingsUI(cascading_layout.CascadingLayoutPanel): + def __init__(self, parent, scope, **kwargs): + from PYME.Acquire.ui.seqdialog import seqPanel + cascading_layout.CascadingLayoutPanel.__init__(self, parent, **kwargs) + + vsizer = wx.BoxSizer(wx.VERTICAL) + + z_panel = seqPanel(self, scope, mode='compact') + vsizer.Add(z_panel, 0, wx.EXPAND) + + tile_panel = TileSettingsUI(self, scope) + vsizer.Add(tile_panel, 0, wx.EXPAND) + + self.SetSizerAndFit(vsizer) \ No newline at end of file diff --git a/PYME/Acquire/ui/voxelSizeDialog.py b/PYME/Acquire/ui/voxelSizeDialog.py index a20b33d5f..2dccdd2ad 100644 --- a/PYME/Acquire/ui/voxelSizeDialog.py +++ b/PYME/Acquire/ui/voxelSizeDialog.py @@ -83,7 +83,7 @@ def __init__(self, parent, scope): btSizer.Realize() - sizer1.Add(btSizer, 0, wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) + sizer1.Add(btSizer, 0, wx.ALIGN_RIGHT|wx.ALL, 5) self.SetSizer(sizer1) sizer1.Fit(self) diff --git a/PYME/Acquire/webui/__init__.py b/PYME/Acquire/webui/__init__.py index 6576165a4..128cd55aa 100644 --- a/PYME/Acquire/webui/__init__.py +++ b/PYME/Acquire/webui/__init__.py @@ -4,4 +4,33 @@ def load_template(filename): dir = os.path.join(os.path.dirname(__file__), 'templates') with open(os.path.join(dir, filename)) as f: - return f.read() \ No newline at end of file + return f.read() + + +_server_instance = None + +def set_server(server): + """ + Set the server class (used in conjunction with `add_endpoints()` to let hardware drivers etc ... + add endpoints to the server in a backwards compatible way. Such that they a) don't need a reference to the server + and b) this works out to be a no-op for non web-ui instances. + + TODO - do this stuff in an InitGUI or similar step instead? + + Parameters + ---------- + server + + Returns + ------- + + """ + global _server_instance + _server_instance = server + +def add_endpoints(cls, prefix, fail_if_no_server=False): + if _server_instance is None: + if fail_if_no_server: + raise RuntimeError('No server registered') + else: + _server_instance.add_endpoints(cls, prefix) \ No newline at end of file diff --git a/PYME/Acquire/webui/ipy.py b/PYME/Acquire/webui/ipy.py index b5a9f90c9..c2ab338c3 100755 --- a/PYME/Acquire/webui/ipy.py +++ b/PYME/Acquire/webui/ipy.py @@ -158,6 +158,7 @@ def __init__(self, ipy): # it can show e.g. the most recent prompt, rather than absolutely # nothing. self.read_buffer = deque([], maxlen=10) + self.preopen_buffer = deque([]) @property def ptyproc(self): @@ -189,11 +190,15 @@ class IPYTermManager(object): """Base class for a terminal manager.""" def __init__(self, server_url="", ioloop=None, user_ns=None, user_module=None): + from concurrent import futures self.server_url = server_url self.log = logging.getLogger(__name__) self.log.setLevel(logging.WARNING) self.ptys_by_fd = {} + + self._blocking_io_executor_is_external = False + self.blocking_io_executor = futures.ThreadPoolExecutor(max_workers=1) if ioloop is not None: self.ioloop = ioloop @@ -235,9 +240,15 @@ def pty_read(self, fd, events=None): ptywclients = self.ptys_by_fd[fd] try: s = ptywclients.ipy.fr_out.read(65536) + client_list = ptywclients.clients self.log.debug(s) ptywclients.read_buffer.append(s) - for client in ptywclients.clients: + if not client_list: + # No one to consume our output: buffer it. + ptywclients.preopen_buffer.append(s) + return + + for client in client_list: client.on_pty_read(s) except EOFError: self.on_eof(ptywclients) @@ -266,6 +277,8 @@ def client_disconnected(self, websocket): @gen.coroutine def shutdown(self): yield self.kill_all() + if not self._blocking_io_executor_is_external: + self.blocking_io_executor.shutdown(wait=False, cancel_futures=True) @gen.coroutine def kill_all(self): @@ -296,6 +309,15 @@ def create_ipy_server(user_ns=None, user_module=None): #end demo stuff + try: + # notebook < 6.5 + ipstatic = notebook.DEFAULT_STATIC_FILES_PATH + except AttributeError: + # notebook >= 6.5 + import nbclassic + ipstatic = nbclassic.DEFAULT_STATIC_FILES_PATH + + term_manager = IPYTermManager(user_ns=user_ns, user_module=user_module) handlers = [ (r"/websocket", TermSocket, @@ -303,7 +325,7 @@ def create_ipy_server(user_ns=None, user_module=None): (r"/", TerminalPageHandler), #(r"/xstatic/(.*)", tornado_xstatic.XStaticFileHandler, # {'allowed_modules': ['termjs']}), - (r"/ipstatic/(.*)", tornado.web.StaticFileHandler, {'path' : notebook.DEFAULT_STATIC_FILES_PATH}) + (r"/ipstatic/(.*)", tornado.web.StaticFileHandler, {'path' : ipstatic}) ] app = tornado.web.Application(handlers, static_path=STATIC_DIR, template_path=TEMPLATE_DIR, diff --git a/PYME/Acquire/webui/static/css/pymeacquire.css b/PYME/Acquire/webui/static/css/pymeacquire.css index 890687f8f..12eccf193 100644 --- a/PYME/Acquire/webui/static/css/pymeacquire.css +++ b/PYME/Acquire/webui/static/css/pymeacquire.css @@ -60,7 +60,7 @@ body { .uitabs { position:absolute; bottom: 32px; - height:400px; + /*height:400px;*/ width:100%; overflow: hidden; diff --git a/PYME/Acquire/webui/static/js/pymeacquire.js b/PYME/Acquire/webui/static/js/pymeacquire.js index b0c7ff347..8ca6be241 100644 --- a/PYME/Acquire/webui/static/js/pymeacquire.js +++ b/PYME/Acquire/webui/static/js/pymeacquire.js @@ -2,257 +2,374 @@ * Created by david on 12/04/20. */ // Long Polling to update image - function poll_png(){ - $.ajax({ url: "/get_frame_png_b64", success: function(data){ - $("#cam_image").attr("src", "data:image/png;base64,"+data); - }, dataType: "text", complete: function(jqXHR, status){ - if (status == 'success') {poll_png();} else {console.log('Error during image polling, make sure server is up and try refreshing the page');} - }, timeout: 30000 }); +// function html(strings){ +// return strings.raw; +// } + +function poll_png(){ + $.ajax({ url: "/get_frame_png_b64", success: function(data){ + $("#cam_image").attr("src", "data:image/png;base64,"+data); + }, dataType: "text", complete: function(jqXHR, status){ + if (status == 'success') {poll_png();} else {console.log('Error during image polling, make sure server is up and try refreshing the page');} + }, timeout: 30000 }); +} + + + +var _min = 0; +var _max = 1e9; + +function map_array(data, cmin, cmax){ + out = new Uint8ClampedArray(data.length*4); + + //record min and max + _min = 1e9; + _max = 0; + + for (j = 0; j< data.length; j++){ + k = j*4; + v = data[j]; + _min = Math.min(v, _min); + _max = Math.max(v,_max); + v = (v - cmin)/(cmax-cmin); + //console.log(v) + v_ = 255*v; //simple grayscale map - FIXME + out[k] = v_; + out[k+1] = v_; + out[k+2] = v_; + out[k+3] = 255; //alpha } + return out +} +// Long Polling to update image +function poll_array(){ + $.ajax({ + url: "/get_frame_pzf", + success: function(data){ + //console.log(data); + decoded = decode_pzf(data); + //console.log(decoded); + im = new ImageData(map_array(decoded.data, parseFloat($("#display_min").val()), parseFloat($("#display_max").val())), decoded.width, decoded.height); + if ($("#display_autoscale").is(":checked")){ + $("#display_min").val(_min); + $("#display_max").val(_max); + } + var zoom = parseFloat($("#display_zoom").val())/100.; + var canvas = document.getElementById("cam_canvas"); + $("#cam_canvas").attr({width: decoded.width*zoom, height : decoded.height*zoom}); + var ctx = canvas.getContext('2d'); + //ctx.scale(zoom, zoom); + + createImageBitmap(im, options={resizeWidth: decoded.width*zoom, resizeHeight:decoded.height*zoom, resizeQuality:'pixelated'}).then(function(bmp){ + ctx.drawImage(bmp, 0, 0); + }); + //console.log(im); + //ctx.putImageData(im, 0, 0); - var _min = 0; - var _max = 1e9; - - function map_array(data, cmin, cmax){ - out = new Uint8ClampedArray(data.length*4); - - //record min and max - _min = 1e9; - _max = 0; - - for (j = 0; j< data.length; j++){ - k = j*4; - v = data[j]; - _min = Math.min(v, _min); - _max = Math.max(v,_max); - v = (v - cmin)/(cmax-cmin); - //console.log(v) - v_ = 255*v; //simple grayscale map - FIXME - out[k] = v_; - out[k+1] = v_; - out[k+2] = v_; - out[k+3] = 255; //alpha + }, + //dataType: "text", + complete: function(jqXHR, status){ + if (status == 'success') {poll_array();} else {console.log('Error during image polling, make sure server is up and try refreshing the page');} + }, + timeout: 30000, + xhrFields: {responseType: 'arraybuffer'} + }); +} + +poll_array(); + +function log_ajax_error(jqXHR, textStatus, errorThrown){ + console.log(textStatus); + console.log(errorThrown); + console.log(jqXHR); +} + +function update_server_state(state){ + //console.log('updating state', state); + $.ajax({ + url : "/update_scope_state", + data : JSON.stringify(state), + processData: false, + type: 'POST', + error: log_ajax_error, + }) +} + +function update_stack_settings(settings){ + console.log('updating stack', settings); + $.ajax({ + url : "/stack_settings/update", + data : JSON.stringify(settings), + processData: false, + type: 'POST', + error: log_ajax_error, + }) +} + +function update_spooler_settings(settings){ + console.log('updating spooler', settings); + $.ajax({ + url : "/spool_controller/settings", + data : JSON.stringify(settings), + processData: false, + type: 'POST', + error: log_ajax_error, + }) +} + +// + +function start_spooling(filename=null,max_frames=null){ + //console.log('updating state', state); + $.ajax({ + url : "/spool_controller/start_spooling", + //data : JSON.stringify(state), + processData: false, + type: 'GET', + error: log_ajax_error, + }) +} + +Vue.component('position-control', { + props: {'value' : Number, 'axis' : String, 'delta' : {type:[Number,], default: 1.0}}, + template: /* html */ `
+ + +
`, + methods:{ + set_position: function(axis, value){ + //console.log(delta); + //update_server_state(dict_fill('Positioning.' + axis, parseFloat(value))); + update_server_state({['Positioning.' + axis]: parseFloat(value)}); } - - return out - } - - // Long Polling to update image - function poll_array(){ - $.ajax({ - url: "/get_frame_pzf", - success: function(data){ - //console.log(data); - decoded = decode_pzf(data); - //console.log(decoded); - im = new ImageData(map_array(decoded.data, parseFloat($("#display_min").val()), parseFloat($("#display_max").val())), decoded.width, decoded.height); - if ($("#display_autoscale").is(":checked")){ - $("#display_min").val(_min); - $("#display_max").val(_max); - } - var zoom = parseFloat($("#display_zoom").val())/100.; - var canvas = document.getElementById("cam_canvas"); - $("#cam_canvas").attr({width: decoded.width*zoom, height : decoded.height*zoom}); - var ctx = canvas.getContext('2d'); - //ctx.scale(zoom, zoom); - - createImageBitmap(im, options={resizeWidth: decoded.width*zoom, resizeHeight:decoded.height*zoom, resizeQuality:'pixelated'}).then(function(bmp){ - ctx.drawImage(bmp, 0, 0); - }); - //console.log(im); - //ctx.putImageData(im, 0, 0); - - }, - //dataType: "text", - complete: function(jqXHR, status){ - if (status == 'success') {poll_array();} else {console.log('Error during image polling, make sure server is up and try refreshing the page');} - }, - timeout: 30000, - xhrFields: {responseType: 'arraybuffer'} - }); } - - poll_array(); - - function log_ajax_error(jqXHR, textStatus, errorThrown){ - console.log(textStatus); - console.log(errorThrown); - console.log(jqXHR); - } - - function update_server_state(state){ - //console.log('updating state', state); - $.ajax({ - url : "/update_scope_state", - data : JSON.stringify(state), - processData: false, - type: 'POST', - error: log_ajax_error, - }) +}); + +Vue.component('laser-control', { + props: ['power', 'on','name', 'max_power'], + template: /* html */`
+ + +
+ + +
+ +
`, + methods: { + set_laser_power: function (lname, value) { + //update_server_state(dict_fill('Lasers.' + lname + '.Power', parseFloat(value))); + update_server_state({['Lasers.' + lname + '.Power']: parseFloat(value)}); + }, + set_laser_on: function (lname, value) { + //console.log('turning ' + lname + ' on: ' + value); + //update_server_state(dict_fill('Lasers.' + lname + '.On', value)); + update_server_state({['Lasers.' + lname + '.On']: value}); + }, } +}); + +Vue.component('stack-settings', { + props: {value : Object, + show_dwell_time: {type: Boolean, default: false}}, + template: /* html */`
+
+ Axis:   + +
+
+ Mode: + +
+
+
+ Start: + +
+
+
+ End: + +
+
+
+
+
+ Step size [um]: + +
+
+ Num slices: + +
+
+ Dwell time: + +
+
+
+ + `, + methods: { + update_setting: function(propname, value){ + update_stack_settings({[propname] : value}); + } - function dict_fill(key, value){ - var d = {}; - d[key] = value; - return d; } +}); - function start_spooling(filename=null,max_frames=null){ - //console.log('updating state', state); - $.ajax({ - url : "/spool_controller/start_spooling", - //data : JSON.stringify(state), - processData: false, - type: 'GET', - error: log_ajax_error, - }) - } - Vue.component('position-control', { - props: {'value' : Number, 'axis' : String, 'delta' : {type:[Number,], default: 1.0}}, - template: `
- - -
`, - methods:{ - set_position: function(axis, value){ - //console.log(delta); - update_server_state(dict_fill('Positioning.' + axis, parseFloat(value))); - } - } - }); - - Vue.component('laser-control', { - props: ['power', 'on','name', 'max_power'], - template: `
- - -
- - -
- -
`, - methods: { - set_laser_power: function (lname, value) { - update_server_state(dict_fill('Lasers.' + lname + '.Power', parseFloat(value))); - }, - set_laser_on: function (lname, value) { - //console.log('turning ' + lname + ' on: ' + value); - update_server_state(dict_fill('Lasers.' + lname + '.On', value)); +Vue.component('palm-storm-settings', { + props: {spooler: Object, stack: Object}, + computed: { + spool_z_stepping: { + get: function (){ + if (this.spooler.settings.z_stepped){ + return 'true'; + } else return false; }, - } - }); - - var scope_state = {}; - scope_state['Camera.IntegrationTime']=0.1 //default start option - - var app = new Vue({ - el: '#app', - data: { - message: 'Hello Vue!', - state: scope_state + set: function(newValue){ + update_spooler_settings({'z_stepped': newValue=='true'}); + //this.spooler.settings.z_stepped = (newValue == 'true'); } - }); - - var hw = new Vue({ - el: '#hw', - data: { - message: 'Hello Vue!', - state: scope_state, - spooler : {status:{spooling:false,},}, + } + }, + template: /* html */` +
+
Protocol
+
+
+ Protocol File: + +
+
+
+ + +
+
+ + +
+
+ + + +
+
+ ` +}) + + +Vue.component('simulation-settings', { + props: {simcontrol: Object}, + template: /* html */` +
+
Simulation
+
+
+
+ ` +}) + +var scope_state = {}; +scope_state['Camera.IntegrationTime']=0.1; //default start option + + +var app = new Vue({ + el: '#app', + data: { + //message: 'Hello Vue!', + state: scope_state, + spooler : {status:{spooling:false,}, + settings:{z_stepped:false}}, + stack : {}, + }, + computed: { + integration_time_ms: function () { + return this.state['Camera.IntegrationTime']*1000; }, - computed: { - integration_time_ms: function () { - return this.state['Camera.IntegrationTime']*1000; - }, - laser_names: function () { - lks = Object.keys(this.state).filter(function(key){return key.startsWith('Lasers') && key.endsWith('On');}) - laser_info = lks.map(function(k){ - lname= k.split('.')[1]; - return lname; - }); - return laser_info; - } + laser_names: function () { + lks = Object.keys(this.state).filter(function(key){return key.startsWith('Lasers') && key.endsWith('On');}) + laser_info = lks.map(function(k){ + lname= k.split('.')[1]; + return lname; + }); + return laser_info; }, - methods:{ - update_server_state : update_server_state, - set_laser_power: function(lname, value){var key = 'Lasers.' + lname + '.Power'; - var state = {}; - state[key] = parseFloat(value); - update_server_state(state);} , - set_laser_on: function(lname, value){var key = 'Lasers.' + lname + '.On';update_server_state({key: value});}, - } - }); - - function get_state(){ - $.ajax({ - url: "/get_scope_state", - success: function(data){ - app.state=data; - hw.state = data; - //$("#int_time").val(1000*app.state['Camera.IntegrationTime']) + spool_z_stepping: { + get: function (){ + if (this.spooler.settings.z_stepped){ + return 'true'; + } else return false; + }, + set: function(newValue){ + update_spooler_settings({'z_stepped': newValue=='true'}); + //this.spooler.settings.z_stepped = (newValue == 'true'); } - - }) + } + }, + methods:{ + update_server_state : update_server_state, + set_laser_power: function(lname, value){var key = 'Lasers.' + lname + '.Power'; + var state = {}; + state[key] = parseFloat(value); + update_server_state(state);} , + set_laser_on: function(lname, value){var key = 'Lasers.' + lname + '.On';update_server_state({key: value});}, } +}); - get_state(); +//get initial values +$.ajax({url: "/get_scope_state", success: function(data){app.state=data;}}); +$.ajax({url: "/spool_controller/info", success: function(data){app.spooler = data;}}); +$.ajax({url: "/stack_settings/settings", success: function(data){app.stack = data;}}); - function poll_state(){ +function poll_updates(url, attrib){ + var _poll = function(){ $.ajax({ - url: "/scope_state_longpoll", - success: function(data){ - //console.log(data); - app.state=data; - hw.state = data; - //$("#int_time").val(1000*app.state['Camera.IntegrationTime']) - }, - complete: function(jqXHR, status){ - if (status == 'success') {poll_state();} else {console.log('Error during image polling, make sure server is up and try refreshing the page');} - } + url: url, + success: function(data) { + app[attrib] = data; + console.log('updated ' + attrib + ' on ' + app); + }, + complete: function(jqXHR, status){if (status == 'success') {_poll();} else {console.log('Error whilst polling ' + url + ', make sure server is up and try refreshing the page');}} }) - } + }; + _poll(); +} - poll_state(); - - function poll_spooler(){ - $.ajax({ - url: "/spool_controller/info_longpoll", - success: function(data){ - //console.log(data); - //app.state=data; - hw.spooler = data; - //$("#int_time").val(1000*app.state['Camera.IntegrationTime']) - }, - complete: function(jqXHR, status){ - if (status == 'success') {poll_spooler();} else {console.log('Error during image polling, make sure server is up and try refreshing the page');} - } - - }) - } - poll_spooler(); +poll_updates("/scope_state_longpoll", 'state'); +poll_updates("/spool_controller/info_longpoll", 'spooler'); +poll_updates("/stack_settings/settings_longpoll", 'stack'); - $(window).on('load', function(){$("#home-tab").tab('show');}); \ No newline at end of file +$(window).on('load', function(){$("#home-tab").tab('show');}); \ No newline at end of file diff --git a/PYME/Acquire/webui/templates/PYMEAcquire.html b/PYME/Acquire/webui/templates/PYMEAcquire.html index 7e57493bd..584bb02fe 100644 --- a/PYME/Acquire/webui/templates/PYMEAcquire.html +++ b/PYME/Acquire/webui/templates/PYMEAcquire.html @@ -26,7 +26,7 @@ -
+