diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index a7e1617..da0925b 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -12,6 +12,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..45ded4b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Peter Ercius + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 1c62d38..c689055 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A graphical user interface based on ScopeFoundry for viewing TEM data. # Installation First install QT bindings. For example: -`$ pip install PyQt5` +`$ pip install PyQt6` Then install this package and the rest of the dependencies: diff --git a/TemDataBrowser/__init__.py b/TemDataBrowser/__init__.py index d49c23c..c0e4d54 100644 --- a/TemDataBrowser/__init__.py +++ b/TemDataBrowser/__init__.py @@ -3,6 +3,8 @@ __version__ = version("TemDataBrowser") import functools +import json +import re from ScopeFoundry import BaseApp from ScopeFoundry.helper_funcs import load_qt_ui_from_pkg @@ -17,6 +19,91 @@ # Use row-major instead of col-major pg.setConfigOption('imageAxisOrder', 'row-major') +# Every line of the FEI tomography-parameter log is prefixed with a fixed-width +# "MM/DD/YY HH:MM:SS " timestamp; what follows it is indented to show which section a +# parameter belongs to. +_TIMESTAMP_RE = re.compile(r'^\d{2}/\d{2}/\d{2} \d{2}:\d{2}:\d{2} ') +_VALUE_KEY = '_value' + + +def _parse_fei_value(raw): + raw = raw.strip() + if raw == '': + return None + if raw in ('Yes', 'ON'): + return True + if raw in ('No', 'OFF'): + return False + try: + return float(raw) + except ValueError: + return raw + + +def _parse_fei_parameters(lines): + """Parse the vendor tomography-parameter log into a nested dict. + + Section headers (e.g. "STEM imaging mode", "Check Focus") repeat parameter names + like "Periodicity (high tilt range)" under different settings, so a flat dict would + have later sections silently overwrite earlier ones. Indentation depth tells sections + apart from their children, so it is used to nest rather than flatten them. + + A line can be a leaf, a header with no value of its own ("STEM imaging mode"), or + both at once ("Check Focus: Yes" has its own value and also has Periodicity settings + indented beneath it) -- every line is therefore pushed as a potential parent, and + _collapse resolves what it actually turned out to be once all its children are known. + """ + root = {} + stack = [(-1, root)] + for raw_line in lines: + line = _TIMESTAMP_RE.sub('', raw_line) + stripped = line.strip() + if not stripped: + continue + + # The stack must unwind to this line's depth before deciding whether to skip it, + # or a skipped section header (e.g. a "-----" rule right after a depth-1 line) + # would leave a stale frame on the stack and misparent everything that follows. + depth = len(line) - len(line.lstrip(' ')) + while stack[-1][0] >= depth: + stack.pop() + + if set(stripped) == {'-'}: + continue # decorative rule; never has children of its own + parent = stack[-1][1] + + if ':' in stripped: + key, _, value = stripped.partition(':') + key, value = key.strip(), _parse_fei_value(value) + else: + key, value = stripped, None + + node = {_VALUE_KEY: value} + parent[key] = node + stack.append((depth, node)) + + _collapse(root) + return root + + +def _collapse(node): + """Resolve each {_value, ...children} node into its final shape. + + No children and no value -> True (a bare flag like "STEM imaging mode" turned out + to introduce no sub-parameters). No children, a value -> that value. Children and no + value -> a dict of just the children. Both -> a dict of the children plus 'value'. + """ + for key, child in node.items(): + value = child.pop(_VALUE_KEY) + _collapse(child) + if not child: + node[key] = value if value is not None else True + elif value is not None: + child['value'] = value + node[key] = child + else: + node[key] = child + class imageioView(DataBrowserView): """ Handles most normal image types like TIF, PNG, etc.""" @@ -183,16 +270,13 @@ def get_mrc_metadata(path): # Read FEI parameters from .txt file if it exists FEIparameters = Path(path).with_suffix('.txt') if FEIparameters.exists(): - with open(FEIparameters, 'r') as f2: - lines = f2.readlines() - pp1 = list([ii[18:].strip().split(':')] for ii in lines[3:-1]) - pp2 = {} - for ll in pp1: - try: - pp2[ll[0]] = float(ll[1]) - except: - pass # skip lines with no data - meta_data.update(pp2) + try: + with open(FEIparameters, 'r', encoding='utf-8-sig') as f2: + lines = f2.readlines() + except UnicodeDecodeError: + with open(FEIparameters, 'r', encoding='cp1252') as f2: + lines = f2.readlines() + meta_data['fei_parameters'] = _parse_fei_parameters(lines) return meta_data @@ -273,6 +357,8 @@ def on_change_data_filename(self, fname): txt = f'file name = {fname}\n' for k, v in meta_data.items(): + if isinstance(v, dict): + v = json.dumps(v, indent=2, default=str) line = f'{k} = {v}\n' txt += line self.ui.setText(txt) diff --git a/pyproject.toml b/pyproject.toml index de37343..cce7af2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,8 +8,25 @@ authors = [{name = "Peter Ercius", email="percius@lbl.gov"}] description = "Graphical user interface to view transmission electron microscopy data." readme = "README.md" requires-python = ">=3.10" -dependencies = ["numpy","pyqtgraph","ScopeFoundry>=1.5","ncempy>=1.15","scipy","imageio>2.17"] -version = "1.2" +license = "MIT" +license-files = ["LICENSE"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Topic :: Scientific/Engineering :: Visualization", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +dependencies = ["numpy","pyqtgraph","qtpy","ScopeFoundry>=1.5","ncempy>=1.15","scipy","imageio>2.17"] +dynamic = ["version"] + +[tool.setuptools_scm] + +[project.urls] +Repository = "https://github.com/ercius/TemDataBrowser" +Homepage = "https://github.com/ercius/TemDataBrowser" [project.scripts] TemDataBrowser = "TemDataBrowser:open_file"