Skip to content

Commit aafc04e

Browse files
Baharisstefsmeets
andauthored
Generalize the PETS input writer and remove legacy/unused code (#134)
* Add generalized handling for PETS input using a dedicated factory * Since we need to support python 3.7 and 3.8, move csv file inside py * Fix name of produced pets file to `pets.pts` * Fix creating `pets.pts` if prefix or suffix are not defined * Split PETS affix to blocks pre-adding: rejects only duplicate lines, not all affix * Add doc, tests, move csv to resources, bump min Python version to 3.9 * GitHub tests fail for Python 3.13, may be not supported by some libraries yet * Remove `[tool.setuptools.package-data]` from `pyproject.toml` Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com> * Fix typo in src/instamatic/processing/ImgConversion.py Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com> * Move `PetsInputWarning` to the top of the `PETS_input_factory.py` file * Make `PETS_KEYWORDS` uppercase, fix `PetsKeywords.from_file` type hint * Bump the suggested conda-forge Python version to 3.12 --------- Co-authored-by: Stef Smeets <stefsmeets@users.noreply.github.com>
1 parent a8975d9 commit aafc04e

File tree

15 files changed

+456
-93
lines changed

15 files changed

+456
-93
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
strategy:
2323
fail-fast: false
2424
matrix:
25-
python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12']
25+
python-version: ['3.9', '3.10', '3.11', '3.12', ]
2626

2727
steps:
2828
- uses: actions/checkout@v3

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ include src/instamatic/config/camera/*.yaml
2727
include src/instamatic/config/microscope/*.yaml
2828
include src/instamatic/config/scripts/*.md
2929
include src/instamatic/neural_network/*.p
30+
include src/instamatic/processing/PETS_input_keywords.csv

docs/config.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,42 @@ This file holds the specifications of the camera. This file is must be located t
249249
DiffShift: {gridsize: 5, stepsize: 300}
250250
```
251251

252+
**pets_prefix**
253+
: Arbitrary information to be added at the beginning of the `.pts` file created after an experiment. The prefix can include any [valid PETS2 input lines](http://pets.fzu.cz/download/pets2_manual.pdf). In the case of duplicate commands, prefix lines take precedence over hard-coded and suffix commands, and prevent the latter ones from being added. Additionally, this field can contain new python-style [replacement fields](https://pyformat.info/) which, if present among the `ImgConversion` instance attributes, will be filled automatically after each experiment (see the `pets_suffix` example). A typical `pets_prefix`, capable of overwriting the default detector specification output can look like this:
254+
```yaml
255+
pets_prefix: "noiseparameters 4.2 0\nreflectionsize 8\ndetector asi"
256+
```
257+
258+
**pets_suffix**
259+
: Arbitrary information to be added at the end of the `.pts` file created after an experiment. Similarly to the `pets_prefix`, the suffix can include any [valid PETS2 input lines](http://pets.fzu.cz/download/pets2_manual.pdf) as well as new python-style [replacement fields](https://pyformat.info/). In contrast to prefix, any duplicate commands added to suffix will be ignored. This field can be useful to add backup or meta information about the experiment:
260+
```yaml
261+
pets_suffix: |
262+
cifentries
263+
_exptl_special_details
264+
;
265+
{method} data collected using Instamatic.
266+
Tilt step: {osc_angle:.3f} deg
267+
Exposure: {headers[0][ImageExposureTime]:.6f} s per frame
268+
;
269+
_diffrn_ambient_temperature ?
270+
_diffrn_source 'Lanthanum hexaboride cathode'
271+
_diffrn_source_voltage 200
272+
_diffrn_radiation_type electron
273+
_diffrn_radiation_wavelength 0.0251
274+
_diffrn_measurement_device 'Transmission electron microscope'
275+
_diffrn_measurement_device_type 'FEI Tecnai G2 20'
276+
_diffrn_detector 'ASI Cheetah'
277+
_diffrn_measurement_method '{method}'
278+
_diffrn_measurement_specimen_support 'Cu grid with amorphous carbon foil'
279+
_diffrn_standards_number 0
280+
endcifentries
281+
282+
badpixels
283+
359 32
284+
279 513
285+
endbadpixels
286+
```
287+
252288
## microscope.yaml
253289

254290
This file holds all the specifications of the microscope as necessary. It is important to set up the camera lengths, magnifications, and magnification modes. This file is must be located the `microscope/camera` directory, and can have any name as defined in `settings.yaml`.

environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ channels:
44
- conda-forge
55
- defaults
66
dependencies:
7-
- python==3.7
7+
- python==3.12

pyproject.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ name = "instamatic"
88
version = "2.1.1"
99
description = "Python program for automated electron diffraction data collection"
1010
readme = "readme.md"
11-
requires-python = ">=3.7"
11+
requires-python = ">=3.9"
1212
authors = [
1313
{name = "Stef Smeets", email = "s.smeets@esciencecenter.nl"},
1414
]
@@ -24,11 +24,10 @@ keywords = [
2424
]
2525
license = {text = "BSD License"}
2626
classifiers = [
27-
"Programming Language :: Python :: 3.7",
28-
"Programming Language :: Python :: 3.8",
2927
"Programming Language :: Python :: 3.9",
3028
"Programming Language :: Python :: 3.10",
3129
"Programming Language :: Python :: 3.11",
30+
"Programming Language :: Python :: 3.12",
3231
"Development Status :: 5 - Production/Stable",
3332
"Intended Audience :: Science/Research",
3433
"License :: OSI Approved :: BSD License",
@@ -123,8 +122,13 @@ publishing = [
123122
# setup
124123
"instamatic.autoconfig" = "instamatic.config.autoconfig:main"
125124

125+
[tool.setuptools]
126+
packages = ["instamatic"]
127+
package-dir = {"" = "src"}
128+
include-package-data = true
129+
126130
[tool.ruff]
127-
target-version = 'py37'
131+
target-version = 'py39'
128132
line-length = 96
129133

130134
[tool.ruff.lint]

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ conda create -n instamatic python=3.11
3939
conda activate instamatic
4040
```
4141

42-
Install using pip, works with python versions 3.7 or newer:
42+
Install using pip, works with python versions 3.9 or newer:
4343

4444
```bash
4545
pip install instamatic

src/instamatic/_collections.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

3+
import string
34
from collections import UserDict
4-
from typing import Any
5+
from typing import Any, Tuple
56

67

78
class NoOverwriteDict(UserDict):
@@ -11,3 +12,29 @@ def __setitem__(self, key: Any, value: Any) -> None:
1112
if key in self.data:
1213
raise KeyError(f'Key "{key}" already exists and cannot be overwritten.')
1314
super().__setitem__(key, value)
15+
16+
17+
class PartialFormatter(string.Formatter):
18+
"""`str.format` alternative, allows for partial replacement of {fields}"""
19+
20+
def __init__(self, missing: str = '{{{}}}') -> None:
21+
super().__init__()
22+
self.missing: str = missing # used instead of missing values
23+
24+
def get_field(self, field_name: str, args, kwargs) -> Tuple[Any, str]:
25+
"""When field can't be found, return placeholder text instead."""
26+
try:
27+
obj, used_key = super().get_field(field_name, args, kwargs)
28+
return obj, used_key
29+
except (KeyError, AttributeError, IndexError, TypeError):
30+
return self.missing.format(field_name), field_name
31+
32+
def format_field(self, value: Any, format_spec: str) -> str:
33+
"""If the field was not found, format placeholder as string instead."""
34+
try:
35+
return super().format_field(value, format_spec)
36+
except (ValueError, TypeError):
37+
return str(value)
38+
39+
40+
partial_formatter = PartialFormatter()

src/instamatic/_typing.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from __future__ import annotations
22

3+
import os
4+
from typing import Union
5+
36
from typing_extensions import Annotated
47

8+
AnyPath = Union[str, bytes, os.PathLike]
59
int_nm = Annotated[int, 'Length expressed in nanometers']
610
float_deg = Annotated[float, 'Angle expressed in degrees']

src/instamatic/processing/ImgConversion.py

Lines changed: 32 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import logging
55
import time
66
from datetime import datetime
7-
from math import cos
7+
from pathlib import Path
88

99
import numpy as np
1010

1111
from instamatic import config
12+
from instamatic._typing import AnyPath
1213
from instamatic.formats import read_tiff, write_adsc, write_mrc, write_tiff
1314
from instamatic.processing.flatfield import apply_flatfield_correction
15+
from instamatic.processing.PETS_input_factory import PetsInputFactory
1416
from instamatic.processing.stretch_correction import affine_transform_ellipse_to_circle
1517
from instamatic.tools import (
1618
find_beam_center,
@@ -131,6 +133,7 @@ def __init__(
131133
rotation_axis: float, # radians, specifies the position of the rotation axis
132134
acquisition_time: float, # seconds, acquisition time (exposure time + overhead)
133135
flatfield: str = 'flatfield.tiff',
136+
method: str = 'continuous-rotation 3D ED', # or 'stills' or 'precession', used for CIF/documentation
134137
):
135138
if flatfield is not None:
136139
flatfield, h = read_tiff(flatfield)
@@ -188,6 +191,7 @@ def __init__(
188191
# self.rotation_speed = get_calibrated_rotation_speed(osc_angle / self.acquisition_time)
189192

190193
self.name = 'Instamatic'
194+
self.method = method
191195

192196
from .XDS_template import (
193197
XDS_template, # hook XDS_template here, because it is difficult to override as a global
@@ -644,93 +648,36 @@ def write_beam_centers(self, path: str) -> None:
644648

645649
np.savetxt(path / 'beam_centers.txt', centers, fmt='%10.4f')
646650

647-
def write_pets_inp(self, path: str, tiff_path: str = 'tiff') -> None:
648-
"""Write PETS input file `pets.pts` in directory `path`"""
649-
if self.start_angle > self.end_angle:
650-
sign = -1
651-
else:
652-
sign = 1
653-
654-
omega = np.degrees(self.rotation_axis)
655-
656-
# for pets, 0 <= omega <= 360
657-
if omega < 0:
658-
omega += 360
659-
elif omega > 360:
660-
omega -= 360
661-
662-
with open(path / 'pets.pts', 'w') as f:
663-
date = str(time.ctime())
664-
print(
665-
'# PETS input file for Rotation Electron Diffraction generated by `instamatic`',
666-
file=f,
667-
)
668-
print(f'# {date}', file=f)
669-
print('# For definitions of input parameters, see:', file=f)
670-
print('# http://pets.fzu.cz/ ', file=f)
671-
print('', file=f)
672-
print(f'lambda {self.wavelength}', file=f)
673-
print(f'Aperpixel {self.pixelsize}', file=f)
674-
print(f'phi {float(self.osc_angle) / 2}', file=f)
675-
print(f'omega {omega}', file=f)
676-
print('bin 1', file=f)
677-
print('reflectionsize 20', file=f)
678-
print('noiseparameters 3.5 38', file=f)
679-
print('', file=f)
680-
# print("reconstructions", file=f)
681-
# print("endreconstructions", file=f)
682-
# print("", file=f)
683-
# print("distortions", file=f)
684-
# print("enddistortions", file=f)
685-
# print("", file=f)
686-
print('imagelist', file=f)
687-
for i in self.observed_range:
688-
fn = f'{i:05d}.tiff'
689-
angle = self.start_angle + sign * self.osc_angle * i
690-
print(f'{tiff_path}/{fn} {angle:10.4f} 0.00', file=f)
691-
print('endimagelist', file=f)
651+
def write_pets_inp(self, path: AnyPath, tiff_path: str = 'tiff') -> None:
652+
sign = 1 if self.start_angle < self.end_angle else -1
653+
omega = np.degrees(self.rotation_axis) % 360
692654

693-
def write_pets2_inp(self, path: str, tiff_path: str = 'tiff') -> None:
694-
"""Write PETS 2 input file `pets.pts2` in directory `path`"""
695-
path.mkdir(exist_ok=True, parents=True)
696-
697-
if self.start_angle > self.end_angle:
698-
sign = -1
655+
if 'continuous' in self.method:
656+
geometry = 'continuous'
657+
elif 'precess' in self.method:
658+
geometry = 'precession'
699659
else:
700-
sign = 1
701-
702-
omega = np.degrees(self.rotation_axis)
660+
geometry = 'static'
661+
662+
p = PetsInputFactory()
663+
p.add('geometry', geometry)
664+
p.add('lambda', self.wavelength)
665+
p.add('Aperpixel', self.pixelsize)
666+
p.add('phi', float(self.osc_angle) / 2)
667+
p.add('omega', omega)
668+
p.add('bin', 1)
669+
p.add('reflectionsize', 20)
670+
p.add('noiseparameters', 3.5, 38)
671+
p.add('')
672+
673+
s = []
674+
for i in self.observed_range:
675+
angle = self.start_angle + sign * self.osc_angle * i
676+
s.append(f'{tiff_path}/{i:05d}.tiff {angle:10.4f} 0.00')
677+
p.add('imagelist', *s)
703678

704-
with open(path / 'pets.pts2', 'w') as f:
705-
date = str(time.ctime())
706-
print(
707-
'# PETS 2 input file for Rotation Electron Diffraction generated by `instamatic`',
708-
file=f,
709-
)
710-
print(f'# {date}', file=f)
711-
print('# For definitions of input parameters, see:', file=f)
712-
print('# http://pets.fzu.cz/ ', file=f)
713-
print('', file=f)
714-
print('geometry continuous', file=f)
715-
print(f'lambda {self.wavelength}', file=f)
716-
print(f'Aperpixel {self.pixelsize}', file=f)
717-
print(f'phi {float(self.osc_angle) / 2}', file=f)
718-
print(f'omega {omega}', file=f)
719-
print('bin 1', file=f)
720-
print('reflectionsize 15', file=f)
721-
print('noiseparameters 25 10', file=f)
722-
print('i/sigma 5.00 10.00', file=f)
723-
print('', file=f)
724-
print('imagelist', file=f)
725-
tiff_set = {0}
726-
last_img = len(self.observed_range)
727-
tiff_set.update(self.observed_range)
728-
tiff_set.remove(last_img)
729-
for i in tiff_set:
730-
fn = f'{i:04d}.tiff'
731-
angle = self.start_angle + sign * self.osc_angle * i
732-
print(f'{tiff_path}/{fn} {angle:10.4f} 0.00', file=f)
733-
print('endimagelist', file=f)
679+
with open(Path(path) / 'pets.pts', 'w') as f:
680+
f.write(str(p.compile(self.__dict__)))
734681

735682
def write_REDp_shiftcorrection(self, path: str) -> None:
736683
"""Write .sc (shift correction) file for REDp in directory `path`"""

src/instamatic/processing/ImgConversionDM.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def __init__(
2424
pixelsize: float = None, # p/Angstrom, size of the pixels (overrides camera_length)
2525
physical_pixelsize: float = None, # mm, physical size of the pixels (overrides camera length)
2626
wavelength: float = None, # Angstrom, relativistic wavelength of the electron beam
27+
method: str = 'continuous-rotation 3D ED', # or 'stills' or 'precession', used for CIF/documentation
2728
):
2829
if flatfield is not None:
2930
flatfield, h = read_tiff(flatfield)
@@ -69,6 +70,7 @@ def __init__(
6970
logger.debug(f'Primary beam at: {self.mean_beam_center}')
7071

7172
self.name = 'DigitalMicrograph'
73+
self.method = method
7274

7375
from .XDS_templateDM import XDS_template
7476

0 commit comments

Comments
 (0)