Skip to content

Commit 67e0624

Browse files
committed
add max_displacement_ratio parameter for custom scaling of displacement_ratios
1 parent fc08051 commit 67e0624

File tree

2 files changed

+78
-2
lines changed

2 files changed

+78
-2
lines changed

src/ti_plm/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
This is the main module defining the PLM class. The PLM class leverages the 'param' library to parameterize a PLM device, such as its resolution, pixel pitch, phase state levels, etc. Each parameter is defined at the class level and includes default values and detailed documentation about what that parameter is. The `param` library enforces type checking and makes it easy to define a dependency graph through function decorators.
33
"""
4+
45
from importlib.metadata import version
56
import param
67
import numpy as np
@@ -34,6 +35,13 @@ class PLM(param.Parameterized):
3435
default=np.array([])
3536
)
3637

38+
max_displacement_ratio = param.Number(
39+
label='Max Displacement Ratio',
40+
doc='Optional max displacement ratio override. If set, this value will be used to scale the `displacement_ratios` array. Otherwise, the values will be scaled to the number of states minus one, e.g. 15/16 for 4-bit devices.',
41+
allow_None=True,
42+
default=None
43+
)
44+
3745
memory_lut = param.Array(
3846
label='Memory LUT',
3947
doc='Lookup table for values that are written to the PLM electrodes under each mirror corresponding to each displacement level. These values are typically not in monotonically increasing order.',
@@ -75,7 +83,7 @@ def size(self):
7583
def area(self):
7684
return np.prod(self.size())
7785

78-
@param.depends('displacement_ratios', 'phase_range', watch=True, on_init=True)
86+
@param.depends('displacement_ratios', 'max_displacement_ratio', 'phase_range', watch=True, on_init=True)
7987
def _update_phase_buckets(self):
8088
"""Cache the phase bucket array for use in the quantize operation.
8189
@@ -88,8 +96,12 @@ def _update_phase_buckets(self):
8896
# save number of bits for later use by other class methods
8997
self._n_bits = len(self.displacement_ratios)
9098

99+
# if max_displacement_ratio is set, use it to scale displacement ratios
100+
# otherwise, scale to the number of states minus one (e.g. 15/16 for 4-bit devices)
101+
ratio_scale = self.max_displacement_ratio if self.max_displacement_ratio is not None else (self._n_bits - 1) / self._n_bits
102+
91103
# scale displacements between phase_range min and max such that the full displacement range represents one less bit than the available bit depth
92-
phase_disp = self.phase_range[0] + (self.displacement_ratios * (self._n_bits - 1) / self._n_bits) * (self.phase_range[-1] - self.phase_range[0])
104+
phase_disp = self.phase_range[0] + self.displacement_ratios * ratio_scale * (self.phase_range[-1] - self.phase_range[0])
93105
phase_disp = np.hstack([phase_disp, self.phase_range[-1]])
94106

95107
# use average value of each phase level and the level above it to create buckets

tests/test_plm_devices.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ def test_p67():
116116
assert np.array_equal(plm.size(), [8640e-6, 13824e-6])
117117
assert approx(plm.area()) == 1.1943935e-4
118118

119+
119120
def test_p67_minus_pi_to_pi():
120121
from ti_plm import PLM
121122

@@ -210,3 +211,66 @@ def test_p67_3d():
210211
# test full algorithm
211212
out = plm.process_phase_map(phase, replicate_bits=False, enforce_shape=False)
212213
assert np.array_equal(out, bits_expected)
214+
215+
216+
def test_p67_custom():
217+
from ti_plm import PLM
218+
219+
plm = PLM.from_db(
220+
'p67',
221+
name='Custom p67',
222+
displacement_ratios=np.array([0.0, 0.0126, 0.0259, 0.0495, 0.0710, 0.0878, 0.1382, 0.2153, 0.3274, 0.3610, 0.4204, 0.5046, 0.5916, 0.6730, 0.8254, 1.0]),
223+
max_displacement_ratio=0.8
224+
)
225+
226+
# create test phase map of phase values associated with each displacement level of a p67 SHV device
227+
phase = np.array([[0.0, 0.0126, 0.0259, 0.0495, 0.0710, 0.0878, 0.1382, 0.2153, 0.3274, 0.3610, 0.4204, 0.5046, 0.5916, 0.6730, 0.8254, 1.0]]) * 0.8 * 2 * np.pi
228+
229+
# define expect phase state index corresponding to each phase value above
230+
phase_idx_expected = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]])
231+
232+
# define expected memory values for each displacement level
233+
memory_expected = np.array([[3, 2, 1, 7, 0, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12]])
234+
235+
# define expected bits we should get for the given phase map, which accounts for memory mapping and bit layout on electrodes under PLM mirrors
236+
bits_expected_unflipped = np.array([
237+
# Phase state index
238+
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
239+
# Memory value
240+
# 3 2 1 7 0 6 5 4 11 10 9 8 15 14 13 12
241+
# Electrode cell values
242+
[1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1],
243+
[1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1]
244+
], dtype=np.uint8)
245+
bits_expected = np.flip(bits_expected_unflipped, -1) # flip along column axis (horizontal) for p67 plm
246+
247+
# test individual functions related to calculating bits from phase map
248+
phase_idx = plm.quantize(phase)
249+
assert np.array_equal(phase_idx, phase_idx_expected)
250+
memory = plm.memory_lut[phase_idx]
251+
assert np.array_equal(memory, memory_expected)
252+
bits = plm.electrode_map(phase_idx)
253+
assert np.array_equal(bits, bits_expected)
254+
255+
# test full algorithm
256+
out = plm.process_phase_map(phase, replicate_bits=False, enforce_shape=False)
257+
assert np.array_equal(out, bits_expected)
258+
259+
# test phase wrapping (2pi --> 0)
260+
assert np.array_equal(
261+
plm.process_phase_map(np.array([[[2 * np.pi]]]), replicate_bits=False, enforce_shape=False),
262+
np.array([[ # note this is a flipped 0 index phase state
263+
[0, 1],
264+
[0, 1]
265+
]])
266+
)
267+
268+
# make sure shape enforcement is working
269+
# calling process_phase_map without enforce_shape=False will throw an exception if the input phase map shape doesn't match plm.shape
270+
from ti_plm import TIPLMException
271+
with raises(TIPLMException):
272+
plm.process_phase_map(phase)
273+
274+
# make sure size and area are calculated correctly
275+
assert np.array_equal(plm.size(), [8640e-6, 13824e-6])
276+
assert approx(plm.area()) == 1.1943935e-4

0 commit comments

Comments
 (0)