# coding="utf-8"
"""Higher-level interface for manipulation of scattering geometry.
"""
from typing import List, IO, Tuple
import numpy as np
import json
from sgt import core, pilatus, _cpolarize
[docs]class geometry(object):
"""Interface class for scattering geometry manipulation.
Example:
To create a `geometry` instance,
>>> import sgt
>>> g = sgt.geometry()
Although all necessary parameters may be supplied
as the args of the initializer,
the easiest way is to load the specs from a file.
>>> g.load_specs("Geometry_AgBh.txt")
Make sure to call refresh functions
when you make changes to the geometry.
>>> g.refresh_q()
Mask can be supplied later.
For example, suppose that we already have the mask array
`maskarray`,
>>> g.mask = maskarray
Make sure to refresh polar maps whenever you made
any changes to the geometry.
>>> g.refresh_polar_map()
Finally, it can perform circular averaging.
Suppose that we have the 2D intensity array `i`
and associated error array `e_i`,
>>> i_av, e_i_av = g.circular_average(i, e_i)
The output arrays are also 2D.
For example, ``i_av[k]`` is the q-profile at ``k`` th
azimuthal section.
The arrays along the q and azimuthal angle axes
are stored as ``g.ax_q`` and ``g.ax_azi``,
respectively.
"""
minimal_spec_keys: Tuple = \
("width_px",
"height_px",
"px_width_mm",
"px_height_mm",
"u0_mm",
"v0_mm",
"alpha_deg",
"beta_deg",
"gamma_deg",
"L0_mm",
"lam_ang",
"qmin_anginv",
"qmax_anginv",
"q_number",
"azi_number")
def __init__(self,
width_px: int=1, height_px: int=1,
px_width_mm: float=1.0, px_height_mm: float=1.0,
u0_mm: float=0.0, v0_mm: float=0.0,
alpha_deg: float=0.0, beta_deg: float=0.0, gamma_deg: float=0.0,
L0_mm: float=1.0, lam_ang: float=1.0,
qmin: float=0.0, qmax: float=1.0, N_q: int=100, N_azi: int=1,
mask: np.ndarray|None=None) -> None:
self.specs: dict = {}
self.specs["width_px"] = width_px
self.specs["height_px"] = height_px
self.specs["px_width_mm"] = px_width_mm
self.specs["px_height_mm"] = px_height_mm
self.specs["u0_mm"] = u0_mm
self.specs["v0_mm"] = v0_mm
self.specs["alpha_deg"] = alpha_deg
self.specs["beta_deg"] = beta_deg
self.specs["gamma_deg"] = gamma_deg
self.specs["L0_mm"] = L0_mm
self.specs["lam_ang"] = lam_ang
self.specs["qmin_anginv"] = qmin
self.specs["qmax_anginv"] = qmax
self.specs["q_number"] = N_q
self.specs["azi_number"] = N_azi
if mask is None:
self.mask = core.make_default_mask(width_px, height_px)
else:
self.mask = mask
# vars to be calculated
self._R: np.ndarray = np.zeros((3,3))
self._u: np.ndarray = np.empty((0,0))
self._v: np.ndarray = np.empty((0,0))
self._a: np.ndarray = np.zeros((3,))
self._b: np.ndarray = np.zeros((3,))
self._n: np.ndarray = np.zeros((3,))
self._x: np.ndarray = np.empty((0,0))
self._y: np.ndarray = np.empty((0,0))
self._z: np.ndarray = np.empty((0,0))
self._qx: np.ndarray = np.empty((0,0))
self._qy: np.ndarray = np.empty((0,0))
self._qz: np.ndarray = np.empty((0,0))
self._solid_angle_factor: np.ndarray = np.empty((0,0))
self._map_q: np.ndarray = np.empty((0,0), dtype=int)
self._map_azi: np.ndarray = np.empty((0,0), dtype=int)
self._density: np.ndarray = np.empty((0,0), dtype=int)
self._ax_q: np.ndarray = np.empty((0,0))
self._ax_azi: np.ndarray = np.empty((0,0))
self._normal_incidence_dist: float = 0.0
[docs] def load_specs(self, fp: str|IO) -> None:
"""Loads and applies parameters from a file
It loads parameters from a geometry specification file,
which is a JSON-formatted text file.
The file must contain all keys listed in ``minimal_spec_keys``.
Args:
fp: file-like or path to a JSON-formatted file.
"""
h: dict = {}
missing_keys: List[str] = []
if isinstance(fp, str):
with open(fp, "r") as f:
lines = f.readlines()
else:
lines = fp.readlines()
# for backward compatibility
# older geometry file contains # at every line head
if lines[0].startswith("#"):
jsonfeed = "".join([l.lstrip("#") for l in lines])
else:
jsonfeed = "".join(lines)
h = json.loads(jsonfeed)
missing_keys = [k for k in self.minimal_spec_keys if k not in h]
if missing_keys:
raise ValueError("missing key(s): " + ", ".join(missing_keys))
self.specs.update(h)
[docs] def save_specs(self, fp: str|IO) -> None:
h: dict = {k: v for k, v in self.specs.items()}
hstr: str = json.dumps(h, indent=4, ensure_ascii=False)
hlines: List[str] = hstr.split("\n")
hlines = ["#" + l.strip("\r\n") + "\r\n" for l in hlines]
hstr = "".join(hlines)
if isinstance(fp, str):
with open(fp, "w") as f:
f.write(hstr)
else:
fp.write(hstr)
[docs] def refresh_q(self) -> None:
"""
Note:
``self.mask`` is not used in this method.
"""
self._R = core.make_rotation_matrix(
self.specs["alpha_deg"],
self.specs["beta_deg"],
self.specs["gamma_deg"])
self._u, self._v = core.make_pixel_coords_in_detector_system(
self.specs["width_px"], self.specs["height_px"],
self.specs["px_width_mm"], self.specs["px_height_mm"],
self.specs["u0_mm"], self.specs["v0_mm"]
)
self._a, self._b, self._n = core.make_basis_vectors_on_detector_in_lab_system(self._R)
self._x, self._y, self._z = core.make_pixel_coords_in_lab_system(
self._u, self._v,
self._a, self._b, self._n, self.specs["L0_mm"]
)
self._normal_incidence_dist = core.calc_shortest_dist_to_detector(
self._a, self._b, self._n, self.specs["L0_mm"]
)
self._solid_angle_factor = \
core.make_solid_angle_coverage_correction_factors(
self._x, self._y, self._z, self._normal_incidence_dist
)
self._qx, self._qy, self._qz = core.make_q(
self._x, self._y, self._z, self.specs["lam_ang"]
)
[docs] def refresh_polar_map(self) -> None:
"""
Note:
This method uses ``self.mask``.
"""
assert self._is_ready_for_polar_map() # check
self._map_q, self._map_azi, self._density, self._ax_q, self._ax_azi = \
_cpolarize.calc_polar_map(
self._qx, self._qy, self._qz,
self.mask,
self.specs["qmin_anginv"], self.specs["qmax_anginv"],
self.specs["q_number"], self.specs["azi_number"]
)
[docs] def circular_average(self, intensity: np.ndarray, e_intensity: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Performs circular averaging.
Args:
intensity: intensity array.
e_intensity: intensity error array.
Returns:
Two 2D numpy arrays of the intensity and the error,
both with the shape (``azi_number``, ``q_number``).
"""
return _cpolarize.circular_average(
intensity.astype(np.float64),
e_intensity.astype(np.float64),
self._map_q, self._map_azi, self._density
)
def _is_ready_for_polar_map(self) -> bool:
shape: Tuple[int, int] = (self.specs["height_px"], self.specs["width_px"])
# check mask size
if self.mask.shape != shape:
return False
return True
@property
def R(self) -> np.ndarray: return self._R
@property
def u(self) -> np.ndarray: return self._u
@property
def v(self) -> np.ndarray: return self._v
@property
def x(self) -> np.ndarray: return self._x
@property
def y(self) -> np.ndarray: return self._y
@property
def z(self) -> np.ndarray: return self._z
@property
def qx(self) -> np.ndarray: return self._qx
@property
def qy(self) -> np.ndarray: return self._qy
@property
def qz(self) -> np.ndarray: return self._qz
@property
def solid_angle_factor(self) -> np.ndarray: return self._solid_angle_factor
@property
def map_q(self) -> np.ndarray: return self._map_q
@property
def map_azi(self) -> np.ndarray: return self._map_azi
@property
def density(self) -> np.ndarray: return self._density
@property
def ax_q(self) -> np.ndarray: return self._ax_q
@property
def ax_azi(self) -> np.ndarray: return self._ax_azi
@property
def normal_incidence_dist(self) -> float: return self._normal_incidence_dist
if __name__ == "__main__":
pass