nbs-bl 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nbs_bl/__init__.py +15 -0
- nbs_bl/beamline.py +450 -0
- nbs_bl/configuration.py +838 -0
- nbs_bl/detectors.py +89 -0
- nbs_bl/devices/__init__.py +12 -0
- nbs_bl/devices/detectors.py +154 -0
- nbs_bl/devices/motors.py +242 -0
- nbs_bl/devices/sampleholders.py +360 -0
- nbs_bl/devices/shutters.py +120 -0
- nbs_bl/devices/slits.py +51 -0
- nbs_bl/gGrEqns.py +171 -0
- nbs_bl/geometry/__init__.py +0 -0
- nbs_bl/geometry/affine.py +197 -0
- nbs_bl/geometry/bars.py +189 -0
- nbs_bl/geometry/frames.py +534 -0
- nbs_bl/geometry/linalg.py +138 -0
- nbs_bl/geometry/polygons.py +56 -0
- nbs_bl/help.py +126 -0
- nbs_bl/hw.py +270 -0
- nbs_bl/load.py +113 -0
- nbs_bl/motors.py +19 -0
- nbs_bl/planStatus.py +5 -0
- nbs_bl/plans/__init__.py +8 -0
- nbs_bl/plans/batches.py +174 -0
- nbs_bl/plans/conditions.py +77 -0
- nbs_bl/plans/flyscan_base.py +180 -0
- nbs_bl/plans/groups.py +55 -0
- nbs_bl/plans/maximizers.py +423 -0
- nbs_bl/plans/metaplans.py +179 -0
- nbs_bl/plans/plan_stubs.py +246 -0
- nbs_bl/plans/preprocessors.py +160 -0
- nbs_bl/plans/scan_base.py +58 -0
- nbs_bl/plans/scan_decorators.py +524 -0
- nbs_bl/plans/scans.py +145 -0
- nbs_bl/plans/suspenders.py +87 -0
- nbs_bl/plans/time_estimation.py +168 -0
- nbs_bl/plans/xas.py +123 -0
- nbs_bl/printing.py +221 -0
- nbs_bl/qt/models/beamline.py +11 -0
- nbs_bl/qt/models/energy.py +53 -0
- nbs_bl/qt/widgets/energy.py +225 -0
- nbs_bl/queueserver.py +249 -0
- nbs_bl/redisDevice.py +96 -0
- nbs_bl/run_engine.py +63 -0
- nbs_bl/samples.py +130 -0
- nbs_bl/settings.py +68 -0
- nbs_bl/shutters.py +39 -0
- nbs_bl/sim/__init__.py +2 -0
- nbs_bl/sim/config/polphase.nc +0 -0
- nbs_bl/sim/energy.py +403 -0
- nbs_bl/sim/manipulator.py +14 -0
- nbs_bl/sim/utils.py +36 -0
- nbs_bl/startup.py +27 -0
- nbs_bl/status.py +114 -0
- nbs_bl/tests/__init__.py +0 -0
- nbs_bl/tests/modify_regions.py +160 -0
- nbs_bl/tests/test_frames.py +99 -0
- nbs_bl/tests/test_panels.py +69 -0
- nbs_bl/utils.py +235 -0
- nbs_bl-0.2.0.dist-info/METADATA +71 -0
- nbs_bl-0.2.0.dist-info/RECORD +64 -0
- nbs_bl-0.2.0.dist-info/WHEEL +4 -0
- nbs_bl-0.2.0.dist-info/entry_points.txt +2 -0
- nbs_bl-0.2.0.dist-info/licenses/LICENSE +13 -0
nbs_bl/gGrEqns.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import scipy.optimize as opt
|
|
3
|
+
import bluesky.plan_stubs as bps
|
|
4
|
+
from .printing import run_report
|
|
5
|
+
|
|
6
|
+
run_report(__file__)
|
|
7
|
+
|
|
8
|
+
#!/usr/bin/env python3
|
|
9
|
+
# -*- coding: utf-8 -*-
|
|
10
|
+
"""
|
|
11
|
+
basic general equations applying to gratings and grating instruments
|
|
12
|
+
assumes all angular arguments are passed as degrees
|
|
13
|
+
assumes length units are in mm
|
|
14
|
+
@author: dvorak
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def getBetaDeg(en_eV, alpha_deg, k_invmm, m):
|
|
19
|
+
# calculate beta in deg
|
|
20
|
+
# en_eV: energy units of electron Volts
|
|
21
|
+
# alpha_deg: incident angle in degrees
|
|
22
|
+
# k_invmm: central line density in mm^-1
|
|
23
|
+
# m: diffraction order unitless integer
|
|
24
|
+
hc = 0.0012398 # in units of eV mm
|
|
25
|
+
lambda_mm = hc / en_eV # wavelength in mm
|
|
26
|
+
alpha = np.radians(alpha_deg) # alpha angle in radians
|
|
27
|
+
beta = np.arcsin((m * k_invmm * lambda_mm - np.sin(alpha))) # beta angle in radians
|
|
28
|
+
return np.degrees(beta)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ruben2005eqn8m(en_eV, cff, k_invmm, m):
|
|
32
|
+
# eqn 8, doi:10.1016/j.nima.2004.09.007, to calculate alpha from cff
|
|
33
|
+
# generalized to include higher orders
|
|
34
|
+
# en_eV is the energy in electron volts
|
|
35
|
+
# cff is unitless constant of fixed focus
|
|
36
|
+
# k_invmm is central line density in mm-1
|
|
37
|
+
# m is the diffraction order, integer, unitless
|
|
38
|
+
# returns alpha in degrees
|
|
39
|
+
# works for grazing incidence, 2000 eV, 1800 mm-1, cff=2, m=+1 thru +5
|
|
40
|
+
# works for grazing incidence, 2000 eV, 1800 mm-1, cff=0.2, m=-1 thru -5
|
|
41
|
+
aa = 1 / (cff ** 2 - 1) ## unitless
|
|
42
|
+
hc = 0.0012398 # in units of eV mm
|
|
43
|
+
lambda_mm = hc / en_eV ## in mm
|
|
44
|
+
return np.degrees(np.arcsin(-m * k_invmm * lambda_mm * aa + np.sqrt(1 + (cff * m * k_invmm * lambda_mm * aa) ** 2)))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_mirror_grating_angles(en_eV, cff, k_invmm, m):
|
|
48
|
+
a = ruben2005eqn8m(en_eV, cff, k_invmm, m) # twice the angle between mirror and grating in equation land
|
|
49
|
+
b = getBetaDeg(en_eV, a, k_invmm, m) # angle of pre-mirror (in degrees) in equation land
|
|
50
|
+
mirror_angle = -(180 - a + b) / 2 # tangle of the mirror pitch for the actual motor (degrees)
|
|
51
|
+
grating_angle = -90 - b # angle of the grating pitch for the actual motor (degrees)
|
|
52
|
+
return [mirror_angle, grating_angle]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def energy(mirror,grating, k_invmm, m):
|
|
56
|
+
hc = 0.0012398 # in units of eV mm
|
|
57
|
+
b = np.radians(-90 - grating)
|
|
58
|
+
a = np.radians(2 * mirror + 90 - grating)
|
|
59
|
+
return hc * k_invmm * m / (np.sin(a) + np.sin(b))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def find_best_offsets(mirror_pitches, grating_pitches, mguesses, eVs, k_invmm):
|
|
63
|
+
'''
|
|
64
|
+
given a bunch of measurements of mirror and grating pitches, fits the most likely constant offsets
|
|
65
|
+
for the mirror and grating pitch. Each measurement can be at a different order(m) and a different energy
|
|
66
|
+
the grating parameters should not change, so only the constant line density is needed
|
|
67
|
+
|
|
68
|
+
mirror_pitches : list or array of length n
|
|
69
|
+
experimental mirror pitch angles
|
|
70
|
+
|
|
71
|
+
grating_pitches : list or array of length n
|
|
72
|
+
experimental grating pitch angles
|
|
73
|
+
|
|
74
|
+
m_guesses : list or array of length n
|
|
75
|
+
the diffraction order which was scanned
|
|
76
|
+
|
|
77
|
+
eVs : list or array of length n
|
|
78
|
+
the energy of each measurement (allows for different calibrants)
|
|
79
|
+
|
|
80
|
+
k_invmm : single number the central spacing of the grating in mm^-1
|
|
81
|
+
|
|
82
|
+
output: the result of numpy.optimize = has a lot of components which might be useful, the most obvious of which are
|
|
83
|
+
output.x the optimized offsets of the mirror, and grating pitch
|
|
84
|
+
output.success wether the optimize function thinks that it succeeded or not. experince shows that good fits are still possible when this is false
|
|
85
|
+
'''
|
|
86
|
+
fit_result = opt.minimize(diffraction_pitch_offset_error_func,
|
|
87
|
+
x0=[0.0001, -0.0001],
|
|
88
|
+
args=(
|
|
89
|
+
mirror_pitches,
|
|
90
|
+
grating_pitches,
|
|
91
|
+
mguesses,
|
|
92
|
+
eVs,
|
|
93
|
+
k_invmm
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
return fit_result
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def diffraction_pitch_offset_error_func(
|
|
101
|
+
fit_elements,
|
|
102
|
+
mirror_pitches,
|
|
103
|
+
grating_pitches,
|
|
104
|
+
m_guesses,
|
|
105
|
+
en_eVs,
|
|
106
|
+
k_invmm):
|
|
107
|
+
'''
|
|
108
|
+
fit_elements, an 2 element array:
|
|
109
|
+
mirror angle offset,
|
|
110
|
+
grating angle offset
|
|
111
|
+
|
|
112
|
+
mirror_pitches : list or array of length n
|
|
113
|
+
experimental mirror pitch angles
|
|
114
|
+
|
|
115
|
+
grating_pitches : list or array of length n
|
|
116
|
+
experimental grating pitch angles
|
|
117
|
+
|
|
118
|
+
m_guesses : list or array of length n
|
|
119
|
+
the diffraction order which was scanned
|
|
120
|
+
|
|
121
|
+
en_eV : list or array of length n
|
|
122
|
+
the energy of each measurement (allows for different calibrants)
|
|
123
|
+
|
|
124
|
+
k_invmm : single number the central spacing of the grating in mm^-1
|
|
125
|
+
'''
|
|
126
|
+
error = 0
|
|
127
|
+
for mp, gp, m, en_eV in zip(mirror_pitches, grating_pitches, m_guesses, en_eVs):
|
|
128
|
+
mir = mp - fit_elements[0] # the test mirror position
|
|
129
|
+
grat = gp - fit_elements[1] # the test grating position
|
|
130
|
+
en_theoretical = energy(mir,grat, k_invmm, m) # what energy these test positions would produce
|
|
131
|
+
error += (en_theoretical - en_eV) ** 2
|
|
132
|
+
return error
|
|
133
|
+
# for the RSoXS grating:
|
|
134
|
+
# RE(tune_pgm(cs=[1.35,1.4,1.45,1.5,1.55],ms=[1,1,1,1,1],energy=291.65,pol=90,k=250))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def set_pgm_offsets(error_object, energy_object):
|
|
138
|
+
yield from bps.mvr(energy_object.monoen.mirror2.user_offset,-error_object.x[0]) # right now we have to set the negative of the fit value as the delta in the offset
|
|
139
|
+
yield from bps.mvr(energy_object.monoen.grating.user_offset,-error_object.x[1])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_grating_fit(numpoints, minc, maxc, minm, maxm, energy, k, noise, moffset, goffset):
|
|
143
|
+
# create some fake measurements of grating and mirror positions to test the fit function
|
|
144
|
+
|
|
145
|
+
cs = np.linspace(minc, maxc, numpoints) # used to generate test pairs of pitches
|
|
146
|
+
ms = np.round(np.linspace(minm, maxm, numpoints))
|
|
147
|
+
evs = np.ones(numpoints) * energy
|
|
148
|
+
merr = np.random.normal(0, noise, numpoints)
|
|
149
|
+
gerr = np.random.normal(0, noise, numpoints)
|
|
150
|
+
|
|
151
|
+
mirrors, gratings = list(zip(*[get_mirror_grating_angles(energy, c, k, m) for c, m in zip(cs, ms)]))
|
|
152
|
+
mirrors += merr + moffset
|
|
153
|
+
gratings += gerr + goffset
|
|
154
|
+
return find_best_offsets(mirrors, gratings, ms, evs, k)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
'''
|
|
160
|
+
mirror positions: [-3.6558274763042604, -3.3921585294849734, -3.1876054822737245, -3.0234213066329048, -2.8882065368134207, -2.774584471608641, -2.6775464182938222, -2.5935586706589646]
|
|
161
|
+
grating positions: [-4.107032615400001, -3.880574386500001, -3.709388039800004, -3.5753463783000043, -3.4671052284000012, -3.379157631200002, -3.3047008284000015, -3.242455403500003]
|
|
162
|
+
energy positions: [291.65, 291.65, 291.65, 291.65, 291.65, 291.65, 291.65, 291.65]
|
|
163
|
+
orders: [1, 1, 1, 1, 1, 1, 1, 1]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
mirror positions: [-3.234388850111962, -3.0810948898872326, -2.9527235798924423, -4.572999932948626, -4.3563799893796045, -4.1749665476012225]
|
|
167
|
+
grating positions: [-3.7760457088000052, -3.6498169308000072, -3.546186037900007, -5.3394722246000015, -5.160893796100005, -5.014527626700001]
|
|
168
|
+
energy positions: [291.65, 291.65, 291.65, 291.65, 291.65, 291.65]
|
|
169
|
+
orders: [1, 1, 1, 2, 2, 2]
|
|
170
|
+
|
|
171
|
+
'''
|
|
File without changes
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class NullFrame:
|
|
5
|
+
def __init__(self, dim):
|
|
6
|
+
self.dim = dim
|
|
7
|
+
self.origin = np.zeros(dim)
|
|
8
|
+
|
|
9
|
+
def get_global(self):
|
|
10
|
+
return self
|
|
11
|
+
|
|
12
|
+
def to_global(self, coords):
|
|
13
|
+
return coords
|
|
14
|
+
|
|
15
|
+
def from_global(self, coords):
|
|
16
|
+
return coords
|
|
17
|
+
|
|
18
|
+
def make_child_frame(self, *axes, origin=None):
|
|
19
|
+
return Frame(*axes, origin=origin, parent=self)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Frame:
|
|
23
|
+
def __init__(self, *axes, origin=None, parent=None):
|
|
24
|
+
if not axes and origin is None:
|
|
25
|
+
raise ValueError("Either axes or origin must be provided")
|
|
26
|
+
|
|
27
|
+
if origin is not None:
|
|
28
|
+
self.origin = np.array(origin)
|
|
29
|
+
self.dim = len(origin)
|
|
30
|
+
|
|
31
|
+
if not axes:
|
|
32
|
+
axes = [
|
|
33
|
+
tuple([1 if i == j else 0 for i in range(self.dim)])
|
|
34
|
+
for j in range(self.dim)
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
self.axes = tuple(self._ensure_axis(ax) for ax in axes)
|
|
38
|
+
else:
|
|
39
|
+
self.axes = tuple(self._ensure_axis(ax) for ax in axes)
|
|
40
|
+
self.dim = self.axes[0].dim
|
|
41
|
+
self.origin = np.zeros(self.dim)
|
|
42
|
+
|
|
43
|
+
if parent is not None:
|
|
44
|
+
self.parent = parent
|
|
45
|
+
if self.parent.dim != self.dim:
|
|
46
|
+
raise ValueError("Frame must have same dimension as parent")
|
|
47
|
+
else:
|
|
48
|
+
self.parent = NullFrame(self.dim)
|
|
49
|
+
|
|
50
|
+
if len(self.origin) != self.dim:
|
|
51
|
+
raise ValueError("Origin must have same dimension as axes")
|
|
52
|
+
|
|
53
|
+
if any(axis.dim != self.dim for axis in self.axes):
|
|
54
|
+
raise ValueError("All axes must have the same dimension")
|
|
55
|
+
|
|
56
|
+
if len(self.axes) != self.dim:
|
|
57
|
+
raise ValueError("Number of axes must equal the dimension of each axis")
|
|
58
|
+
|
|
59
|
+
self.A = np.column_stack(
|
|
60
|
+
[axis.coords for axis in self.axes] + [np.append(self.origin, 1)]
|
|
61
|
+
)
|
|
62
|
+
self.Ainv = np.linalg.inv(self.A)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def _ensure_axis(ax):
|
|
66
|
+
"""
|
|
67
|
+
Convert coordinate tuple to Axis object if necessary.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
ax : Axis or tuple
|
|
72
|
+
The axis or coordinates to convert.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
Axis
|
|
77
|
+
An Axis object.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(ax, Axis):
|
|
80
|
+
return ax
|
|
81
|
+
elif isinstance(ax, (tuple, list, np.ndarray)):
|
|
82
|
+
return Axis.from_coords(*ax)
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError(
|
|
85
|
+
f"Invalid axis type: {type(ax)}. Expected Axis or coordinate tuple."
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def get_global(self):
|
|
89
|
+
return self.parent.get_global()
|
|
90
|
+
|
|
91
|
+
def to_parent(self, coords):
|
|
92
|
+
if len(coords) != self.dim:
|
|
93
|
+
raise ValueError("Coordinates must have same Dimension as Frame")
|
|
94
|
+
return np.dot(self.A, np.append(coords, 1))[:-1]
|
|
95
|
+
|
|
96
|
+
def to_global(self, coords):
|
|
97
|
+
if len(coords) != self.dim:
|
|
98
|
+
raise ValueError("Coordinates must have same Dimension as Frame")
|
|
99
|
+
return self.parent.to_global(self.to_parent(coords))
|
|
100
|
+
|
|
101
|
+
def to_frame(self, coords, frame):
|
|
102
|
+
if self.get_global() != frame.get_global():
|
|
103
|
+
raise ValueError("Frames must have same global frame")
|
|
104
|
+
global_coords = self.to_global(coords)
|
|
105
|
+
return frame.from_global(global_coords)
|
|
106
|
+
|
|
107
|
+
def from_parent(self, coords):
|
|
108
|
+
"""
|
|
109
|
+
coords in parent frame
|
|
110
|
+
"""
|
|
111
|
+
return np.dot(self.Ainv, np.append(coords, 1))[:-1]
|
|
112
|
+
|
|
113
|
+
def from_global(self, coords):
|
|
114
|
+
"""
|
|
115
|
+
coords in parent frame
|
|
116
|
+
"""
|
|
117
|
+
parent_coords = self.parent.from_global(coords)
|
|
118
|
+
return self.from_parent(parent_coords)
|
|
119
|
+
|
|
120
|
+
def make_child_frame(self, *axes, origin=None):
|
|
121
|
+
return Frame(*axes, origin=origin, parent=self)
|
|
122
|
+
|
|
123
|
+
def rotate_in_plane(self, coords, phi, ax1=0, ax2=1):
|
|
124
|
+
"""
|
|
125
|
+
Rotates around z-axis by default
|
|
126
|
+
"""
|
|
127
|
+
M = np.zeros((self.dim, self.dim))
|
|
128
|
+
for i in range(self.dim):
|
|
129
|
+
if i != ax1 and i != ax2:
|
|
130
|
+
M[i, i] = 1
|
|
131
|
+
M[ax1, ax1] = np.cos(phi)
|
|
132
|
+
M[ax1, ax2] = -np.sin(phi)
|
|
133
|
+
M[ax2, ax1] = np.sin(phi)
|
|
134
|
+
M[ax2, ax2] = np.cos(phi)
|
|
135
|
+
return np.dot(M, np.array(coords))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Axis:
|
|
139
|
+
def __init__(self, *coords):
|
|
140
|
+
self.coords = np.array(coords + (0,))
|
|
141
|
+
self.dim = len(coords)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_coords(cls, *coords):
|
|
145
|
+
"""
|
|
146
|
+
Create an Axis object from a list of coordinates, normalizing them.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
coords : list or array-like
|
|
151
|
+
The coordinates to create the Axis from.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
Axis
|
|
156
|
+
A new Axis object with normalized coordinates.
|
|
157
|
+
|
|
158
|
+
"""
|
|
159
|
+
coords = np.array(coords)
|
|
160
|
+
norm = np.linalg.norm(coords)
|
|
161
|
+
if norm == 0:
|
|
162
|
+
raise ValueError("Cannot create an Axis with zero-length vector")
|
|
163
|
+
normalized_coords = coords / norm
|
|
164
|
+
return cls(*normalized_coords)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def angle_between_vectors_2d(v1, v2):
|
|
168
|
+
"""
|
|
169
|
+
Calculate the angle between two 2D vectors in the x-y plane.
|
|
170
|
+
|
|
171
|
+
Parameters:
|
|
172
|
+
v1, v2 : array-like
|
|
173
|
+
The two vectors to compare. Each should be a 2D vector [x, y].
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
float
|
|
177
|
+
The angle between the vectors in radians.
|
|
178
|
+
"""
|
|
179
|
+
# Ensure the vectors are numpy arrays
|
|
180
|
+
v1 = np.array(v1)
|
|
181
|
+
v2 = np.array(v2)
|
|
182
|
+
|
|
183
|
+
# Calculate the angle using atan2
|
|
184
|
+
angle = np.arctan2(v2[1], v2[0]) - np.arctan2(v1[1], v1[0])
|
|
185
|
+
|
|
186
|
+
# Normalize the angle to be between -pi and pi
|
|
187
|
+
return angle % (2 * np.pi)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def find_rotation(child_frame, child_ax, parent_frame, parent_ax, around_ax=2):
|
|
191
|
+
origin = child_frame.to_frame([0, 0, 0], parent_frame)
|
|
192
|
+
vector = child_frame.to_frame(child_ax, parent_frame) - origin
|
|
193
|
+
|
|
194
|
+
angle = angle_between_vectors_2d(
|
|
195
|
+
np.delete(vector, around_ax), np.delete(parent_ax, around_ax)
|
|
196
|
+
)
|
|
197
|
+
return angle
|
nbs_bl/geometry/bars.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
import csv
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class GeometryBase(ABC):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def make_sample_frame(self, position):
|
|
8
|
+
"""
|
|
9
|
+
Create a sample frame for the given position.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
position : Any
|
|
14
|
+
The position for which to create a sample frame.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
Frame
|
|
19
|
+
The created sample frame.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def generate_geometry(self):
|
|
25
|
+
"""
|
|
26
|
+
Generate the geometry for the holder.
|
|
27
|
+
"""
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_geometry(self):
|
|
32
|
+
"""
|
|
33
|
+
Get the geometry of the holder.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
Any
|
|
38
|
+
The geometry of the holder.
|
|
39
|
+
"""
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
def attach_manipulator(self, manipframe):
|
|
43
|
+
self.manip_frame = manipframe
|
|
44
|
+
self.generate_geometry()
|
|
45
|
+
|
|
46
|
+
def read_sample_file(self, filename):
|
|
47
|
+
extension = filename.split(".")[-1]
|
|
48
|
+
if extension in ["csv"] and hasattr(self, "read_sample_csv"):
|
|
49
|
+
return self.read_sample_csv(filename)
|
|
50
|
+
else:
|
|
51
|
+
raise AttributeError(
|
|
52
|
+
f"File had extension {extension}, but this geometry has no read method"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AbsoluteBar(GeometryBase):
|
|
57
|
+
"""
|
|
58
|
+
This is a geometry that assumes that the position is absolute, and will never attempt
|
|
59
|
+
to do any coordinate transformations.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
def make_sample_frame(self, position):
|
|
66
|
+
"""
|
|
67
|
+
Always assume that the position is absolute, and return the coordinates
|
|
68
|
+
"""
|
|
69
|
+
return position
|
|
70
|
+
|
|
71
|
+
def generate_geometry(self):
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def get_geometry(self):
|
|
75
|
+
side_md = {}
|
|
76
|
+
side_frames = {}
|
|
77
|
+
return side_md, side_frames
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Standard4SidedBar(GeometryBase):
|
|
81
|
+
def __init__(self, width, length):
|
|
82
|
+
self.length = length
|
|
83
|
+
self.sides = 4
|
|
84
|
+
self.width = width
|
|
85
|
+
|
|
86
|
+
def make_sample_frame(self, position):
|
|
87
|
+
side = position.get("side")
|
|
88
|
+
# origin = position.get("origin", "bar")
|
|
89
|
+
# if origin in ["abs", "absolute"]:
|
|
90
|
+
# x, y, z, r = position.get("coordinates")
|
|
91
|
+
# origin = (x, y, z)
|
|
92
|
+
# parent_frame = self.manip_frame.parent
|
|
93
|
+
# sample_frame = parent_frame.make_child_frame(origin=origin)
|
|
94
|
+
|
|
95
|
+
# else:
|
|
96
|
+
x1, y1, x2, y2 = position.get("coordinates")
|
|
97
|
+
z = position.get("thickness", 0)
|
|
98
|
+
origin = (0.5 * (x1 + x2), 0.5 * (y1 + y2), z)
|
|
99
|
+
parent_frame = self.side_frames[int(side) - 1]
|
|
100
|
+
sample_frame = parent_frame.make_child_frame(origin=origin)
|
|
101
|
+
return sample_frame
|
|
102
|
+
|
|
103
|
+
def generate_geometry(self):
|
|
104
|
+
"""Very brute force, could be refined to be more general"""
|
|
105
|
+
axes = [(1, 0, 0), (0, 0, 1), (0, -1, 0)]
|
|
106
|
+
origin = (0, 0, -1 * self.length)
|
|
107
|
+
self.bar_frame = self.manip_frame.make_child_frame(*axes, origin=origin)
|
|
108
|
+
side_axes = [
|
|
109
|
+
[(0, 0, 1), (0, 1, 0), (-1, 0, 0)],
|
|
110
|
+
[(-1, 0, 0), (0, 1, 0), (0, 0, -1)],
|
|
111
|
+
[(0, 0, -1), (0, 1, 0), (1, 0, 0)],
|
|
112
|
+
[(1, 0, 0), (0, 1, 0), (0, 0, 1)],
|
|
113
|
+
]
|
|
114
|
+
hw = self.width / 2.0
|
|
115
|
+
side_origins = [(-hw, 0, -hw), (hw, 0, -hw), (hw, 0, hw), (-hw, 0, hw)]
|
|
116
|
+
self.side_frames = [
|
|
117
|
+
self.bar_frame.make_child_frame(*axes, origin=origin)
|
|
118
|
+
for axes, origin in zip(side_axes, side_origins)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def get_geometry(self):
|
|
122
|
+
# Implement the method here
|
|
123
|
+
side_md = {}
|
|
124
|
+
side_frames = {}
|
|
125
|
+
for n in range(self.sides):
|
|
126
|
+
side_frame = self.side_frames[n]
|
|
127
|
+
side_str = f"side{n+1}" # Humans one-index sides...
|
|
128
|
+
side_dict = {
|
|
129
|
+
"name": side_str,
|
|
130
|
+
"id": side_str,
|
|
131
|
+
"description": side_str,
|
|
132
|
+
"position": [],
|
|
133
|
+
}
|
|
134
|
+
side_md[side_str] = side_dict
|
|
135
|
+
side_frames[side_str] = side_frame
|
|
136
|
+
return side_md, side_frames
|
|
137
|
+
|
|
138
|
+
def read_sample_csv(self, filename):
|
|
139
|
+
with open(filename, "r") as f:
|
|
140
|
+
sampleReader = csv.reader(f, skipinitialspace=True)
|
|
141
|
+
samplelist = [row for row in sampleReader]
|
|
142
|
+
rownames = [n for n in samplelist[0] if n != ""]
|
|
143
|
+
# rownames = ["sample_id", "sample_name", "side", "x1", "y1", "x2", "y2",
|
|
144
|
+
# "t", "tags"]
|
|
145
|
+
samples = {}
|
|
146
|
+
for sample in samplelist[1:]:
|
|
147
|
+
sample_id = sample[0]
|
|
148
|
+
sample_dict = {
|
|
149
|
+
key: sample[rownames.index(key)]
|
|
150
|
+
for key in rownames[1:]
|
|
151
|
+
if sample[rownames.index(key)] != ""
|
|
152
|
+
}
|
|
153
|
+
coordinates = (
|
|
154
|
+
float(sample_dict.pop("x1")),
|
|
155
|
+
float(sample_dict.pop("y1")),
|
|
156
|
+
float(sample_dict.pop("x2")),
|
|
157
|
+
float(sample_dict.pop("y2")),
|
|
158
|
+
)
|
|
159
|
+
thickness = sample_dict.pop("thickness", 0)
|
|
160
|
+
side = sample_dict.pop("side")
|
|
161
|
+
position = {
|
|
162
|
+
"side": side,
|
|
163
|
+
"coordinates": coordinates,
|
|
164
|
+
"thickness": thickness,
|
|
165
|
+
}
|
|
166
|
+
sample_dict["position"] = position
|
|
167
|
+
samples[sample_id] = sample_dict
|
|
168
|
+
if "sample_name" in sample_dict:
|
|
169
|
+
name = sample_dict.pop("sample_name")
|
|
170
|
+
sample_dict["name"] = name
|
|
171
|
+
return samples
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Bar1d(GeometryBase):
|
|
175
|
+
def __init__(self):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def generate_geometry(self):
|
|
179
|
+
pass
|
|
180
|
+
|
|
181
|
+
def get_geometry(self):
|
|
182
|
+
side_md = {}
|
|
183
|
+
side_frames = {}
|
|
184
|
+
return side_md, side_frames
|
|
185
|
+
|
|
186
|
+
def make_sample_frame(self, position):
|
|
187
|
+
x = position.get("coordinates")
|
|
188
|
+
child_frame = self.manip_frame.make_child_frame(origin=x)
|
|
189
|
+
return child_frame
|