Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Nonius Kappa CCD format module #741

Merged
merged 5 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions newsfragments/741.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Nonius KappaCCD format.
249 changes: 249 additions & 0 deletions src/dxtbx/format/FormatNoniusKappaCCD.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
from __future__ import annotations

import math
import os
import re
import sys
import time

from boost_adaptbx.boost.python import streambuf
from scitbx.array_family import flex

from dxtbx import IncorrectFormatError
from dxtbx.ext import is_big_endian, read_uint16, read_uint16_bs
from dxtbx.format.Format import Format


class FormatNoniusKappaCCD(Format):
"""A class for reading files produced by 1990s era Nonius Kappa CCD instruments."""

@staticmethod
def read_header_lines(image_path):
"""
Adapted from Fabio KcdImage _readheader
"""

hdr_lines = []
end_of_headers = False
linect = 0

Check warning on line 28 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L26-L28

Added lines #L26 - L28 were not covered by tests
with Format.open_file(image_path, "rb") as f:
while not end_of_headers:
one_line = f.readline()
linect += 1
try:
one_line = one_line.decode("ASCII")
except UnicodeDecodeError:
end_of_headers = True

Check warning on line 36 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L31-L36

Added lines #L31 - L36 were not covered by tests
else:
if len(one_line) > 100:
end_of_headers = True

Check warning on line 39 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L39

Added line #L39 was not covered by tests
if not end_of_headers:
if one_line.strip() == "Binned mode":
one_line = "Mode = Binned"

Check warning on line 42 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L42

Added line #L42 was not covered by tests
if "=" not in one_line and linect > 1:
end_of_headers = True

Check warning on line 44 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L44

Added line #L44 was not covered by tests
if not end_of_headers:
hdr_lines.append(one_line)

Check warning on line 46 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L46

Added line #L46 was not covered by tests

return hdr_lines

Check warning on line 48 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L48

Added line #L48 was not covered by tests

@staticmethod
def parse_header(header_lines):
header_dic = {}

Check warning on line 52 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L52

Added line #L52 was not covered by tests

for l in header_lines:
separator = l.find("=")

Check warning on line 55 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L55

Added line #L55 was not covered by tests
if separator == -1:
continue
k = l[:separator].strip()
v = l[separator + 1 :].strip()

Check warning on line 59 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L57-L59

Added lines #L57 - L59 were not covered by tests
if k in header_dic:
header_dic[k] = header_dic[k] + "\n" + v

Check warning on line 61 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L61

Added line #L61 was not covered by tests
else:
header_dic[k] = v

Check warning on line 63 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L63

Added line #L63 was not covered by tests

return header_dic

Check warning on line 65 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L65

Added line #L65 was not covered by tests

@staticmethod
def understand(image_file):
"""
Look for characteristic character sequences in the
header.
"""
try:
with Format.open_file(image_file, "rb") as fh:
tag = fh.read(1024).decode("latin-1", "replace")
except OSError:
return False

Check warning on line 77 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L76-L77

Added lines #L76 - L77 were not covered by tests
if tag[0:2] != "No":
return False
if re.search("^Kappa-support angle", tag, re.MULTILINE) is None:
return False

Check warning on line 81 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L81

Added line #L81 was not covered by tests
if re.search("^Binned mode", tag, re.MULTILINE) is None:
return False

Check warning on line 83 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L83

Added line #L83 was not covered by tests
if re.search("^Shutter = Closed", tag, re.MULTILINE) is not None:
return False # ignore dark current measurement
return True

Check warning on line 86 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L85-L86

Added lines #L85 - L86 were not covered by tests

def __init__(self, image_file, **kwargs):
"""Initialise the image structure from the given file."""

if not self.understand(image_file):
raise IncorrectFormatError(self, image_file)
super().__init__(str(image_file), **kwargs)

Check warning on line 93 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L92-L93

Added lines #L92 - L93 were not covered by tests

def _start(self):
self.headers = self.parse_header(self.read_header_lines(self._image_file))

Check warning on line 96 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L96

Added line #L96 was not covered by tests

def _goniometer(self):
"""Return model for a kappa goniometer"""
alpha = float(self.headers["Kappa-support angle"])
omega = float(self.headers["Omega start"])
kappa = float(self.headers["Kappa start"])
phi = float(self.headers["Phi start"])
direction = "-z" #

Check warning on line 104 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L100-L104

Added lines #L100 - L104 were not covered by tests

if "Phi scan range" in self.headers:
scan_axis = "phi"

Check warning on line 107 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L107

Added line #L107 was not covered by tests
else:
scan_axis = "omega"

Check warning on line 109 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L109

Added line #L109 was not covered by tests

return self._goniometer_factory.make_kappa_goniometer(

Check warning on line 111 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L111

Added line #L111 was not covered by tests
alpha, omega, kappa, phi, direction, scan_axis
)

def _detector(self):
"""It appears that pixel size reported in the header does not
account for binning, which doubles the size in both dimensions."""

pix_fac = 0.001

Check warning on line 119 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L119

Added line #L119 was not covered by tests
if self.headers["Mode"] == "Binned":
pix_fac = 0.002

Check warning on line 121 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L121

Added line #L121 was not covered by tests

pixel_size = [

Check warning on line 123 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L123

Added line #L123 was not covered by tests
float(self.headers["pixel X-size (um)"]) * pix_fac,
float(self.headers["pixel Y-size (um)"]) * pix_fac,
]

image_size = [

Check warning on line 128 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L128

Added line #L128 was not covered by tests
float(self.headers["X dimension"]),
float(self.headers["Y dimension"]),
]

beam_centre = (

Check warning on line 133 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L133

Added line #L133 was not covered by tests
image_size[0] * pixel_size[0] * 0.5,
image_size[1] * pixel_size[1] * 0.5,
)

gain = self.headers.get("Detector gain (ADU/count)", "1.0")
gain = float(gain)

Check warning on line 139 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L138-L139

Added lines #L138 - L139 were not covered by tests

return self._detector_factory.two_theta(

Check warning on line 141 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L141

Added line #L141 was not covered by tests
sensor="CCD",
distance=float(self.headers["Dx start"]),
beam_centre=beam_centre,
fast_direction="+y", # Increasing to the right
slow_direction="+x", # Down, same as rotation axis
two_theta_direction="+x", # Same sense as omega and phi
two_theta_angle=float(self.headers["Theta start"]) * 2.0,
pixel_size=pixel_size,
image_size=image_size,
gain=gain,
identifier=self.headers["Detector ID"],
trusted_range=(0.0, 131070.0), # May readout UInt16 twice
)

def _beam(self):
"""Return a simple model for a lab-based beam. As dxtbx
has no laboratory target model, take the weighted values
of the alpha1/alpha2 wavelengths."""

a1 = float(self.headers["Alpha1"])
a2 = float(self.headers["Alpha2"])
wt = float(self.headers["Alpha1/Alpha2 ratio"])
wavelength = (wt * a1 + a2) / (wt + 1.0)
direction = [0.0, 0.0, 1.0]
polarisation, pol_dir = self.get_beam_polarisation()
return self._beam_factory.complex(

Check warning on line 167 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L161-L167

Added lines #L161 - L167 were not covered by tests
sample_to_source=direction,
wavelength=wavelength,
polarization_plane_normal=pol_dir,
polarization_fraction=polarisation,
)

def _scan(self):
"""All scan dates will be in the range 1969-2068 as per
Python strptime. Please retire your CCD before 2069."""

if "Phi scan range" in self.headers:
rotax = "Phi"

Check warning on line 179 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L179

Added line #L179 was not covered by tests
else:
rotax = "Omega"

Check warning on line 181 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L181

Added line #L181 was not covered by tests

osc_start = float(self.headers["%s start" % rotax])
osc_range = float(self.headers["%s scan range" % rotax])
exposure_time = float(self.headers["Exposure time"])

Check warning on line 185 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L183-L185

Added lines #L183 - L185 were not covered by tests

simpletime = self.headers["Date"]
epoch = time.mktime(time.strptime(simpletime, "%a %m/%d/%y %I:%M:%S %p"))

Check warning on line 188 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L187-L188

Added lines #L187 - L188 were not covered by tests

return self._scan_factory.single_file(

Check warning on line 190 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L190

Added line #L190 was not covered by tests
self._image_file, exposure_time, osc_start, osc_range, epoch
)

def get_beam_polarisation(self):
"""Polarisation for single reflection off graphite 002
monochromator as per manual. Hard-coded angles for Mo, Cu and
Ag targets."""

if self.headers["Target material"] == "MO":
two_theta = 12.2 # From manual

Check warning on line 200 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L200

Added line #L200 was not covered by tests
elif self.headers["Target material"] == "CU":
two_theta = 26.6 # From manual

Check warning on line 202 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L202

Added line #L202 was not covered by tests
elif self.headers["Target material"] == "AG":
two_theta = 9.62 # Calculated

Check warning on line 204 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L204

Added line #L204 was not covered by tests

# Assume an ideally imperfect monochromator, with the axis
# of rotation of the monochromator in the horizontal direction
# in a laboratory setup.

pol_frac = 1.0 / (1 + math.cos(two_theta * math.pi / 180) ** 2)
pol_dir = [0.0, 1.0, 0.0]
return pol_frac, pol_dir

Check warning on line 212 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L210-L212

Added lines #L210 - L212 were not covered by tests

def get_raw_data(self):
"""Return raw data from a Nonius CCD frame."""

# Frame file contains a series of readouts, each pixel is
# an unsigned little-endian 16-bit integer

dim1 = int(self.headers["X dimension"])
dim2 = int(self.headers["Y dimension"])
nbReadOut = int(self.headers["Number of readouts"])
expected_size = dim1 * dim2 * 2 * nbReadOut

Check warning on line 223 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L220-L223

Added lines #L220 - L223 were not covered by tests

# Not clear if the image offset is present in the header,
# therefore we seek from the end of the file.

with self.open_file(self._image_file, "rb") as infile:
fileSize = os.stat(self._image_file)[6]
infile.seek(fileSize - expected_size, os.SEEK_SET)

Check warning on line 230 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L229-L230

Added lines #L229 - L230 were not covered by tests

for i in range(nbReadOut):
if is_big_endian():
raw_data = read_uint16_bs(streambuf(infile), dim1 * dim2)

Check warning on line 234 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L234

Added line #L234 was not covered by tests
else:
raw_data = read_uint16(streambuf(infile), dim1 * dim2)

Check warning on line 236 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L236

Added line #L236 was not covered by tests

if i == 0:
data = raw_data

Check warning on line 239 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L239

Added line #L239 was not covered by tests
else:
data += raw_data

Check warning on line 241 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L241

Added line #L241 was not covered by tests

data.reshape(flex.grid(dim2, dim1))
return data

Check warning on line 244 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L243-L244

Added lines #L243 - L244 were not covered by tests


if __name__ == "__main__":
for arg in sys.argv[1:]:
print(FormatNoniusKappaCCD.understand(arg))

Check warning on line 249 in src/dxtbx/format/FormatNoniusKappaCCD.py

View check run for this annotation

Codecov / codecov/patch

src/dxtbx/format/FormatNoniusKappaCCD.py#L249

Added line #L249 was not covered by tests
Loading