├── phox ├── __init__.py ├── apps │ ├── __init__.py │ ├── opow.py │ ├── viz.py │ └── onn.py ├── model │ ├── __init__.py │ ├── legacy.py │ └── phase.py ├── typing.py ├── experiment │ ├── __init__.py │ ├── activephotonicsimager.py │ └── amf420mesh.py ├── instrumentation │ ├── __init__.py │ ├── lightwavemultimeter.py │ ├── control.py │ ├── laser.py │ ├── serial.py │ ├── stage.py │ └── camera.py └── utils.py ├── runtime.txt ├── Procfile ├── .gitignore ├── setup.py ├── serve_amf420.py ├── .ipynb_checkpoints └── serve_amf420-checkpoint.py ├── README.md └── scripts ├── mesh_blender.py └── coherent_meas_utils.py /phox/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /phox/apps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /runtime.txt: -------------------------------------------------------------------------------- 1 | python-3.9.7 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: python serve_sim.py $PORT -------------------------------------------------------------------------------- /phox/model/__init__.py: -------------------------------------------------------------------------------- 1 | # from .mesh import Mesh 2 | -------------------------------------------------------------------------------- /phox/typing.py: -------------------------------------------------------------------------------- 1 | from typing import * 2 | import numpy as np 3 | 4 | Arraylike = Union[np.ndarray, List, Tuple] 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py? 2 | *~ 3 | *.swp 4 | .cache 5 | __pycache__ 6 | *.egg-info 7 | .env 8 | .env* 9 | .idea 10 | .DS_Store 11 | data/* 12 | configs/* -------------------------------------------------------------------------------- /phox/experiment/__init__.py: -------------------------------------------------------------------------------- 1 | from .activephotonicsimager import ActivePhotonicsImager 2 | from .amf420mesh import AMF420Mesh, PhaseShifter, PhaseCalibration 3 | -------------------------------------------------------------------------------- /phox/instrumentation/__init__.py: -------------------------------------------------------------------------------- 1 | from .camera import XCamera 2 | from .control import NIDAQControl 3 | from .stage import ASI 4 | from .laser import LaserHP8164A 5 | from .lightwavemultimeter import LightwaveMultimeterHP8163A 6 | # from .piezo import APTPiezo 7 | -------------------------------------------------------------------------------- /phox/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from .typing import Arraylike 3 | 4 | 5 | def minmax_scale(img: np.ndarray): 6 | return (img - np.min(img)) / (np.max(img) - np.min(img)) 7 | 8 | 9 | def min_phase_diff(phase): 10 | return np.minimum(np.abs(phase), np.abs(2 * np.pi + phase), np.abs(-2 * np.pi + phase)) -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | project_name = "phox" 5 | 6 | setup( 7 | name=project_name, 8 | version="0.1", 9 | packages=find_packages(), 10 | install_requires=[ 11 | 'numpy>=1.19', 12 | 'scipy>=1.7.1', 13 | 'dphox>=0.0.1a4', 14 | 'simphox>=0.0.1a6', 15 | 'holoviews' 16 | ], 17 | ) 18 | -------------------------------------------------------------------------------- /serve_amf420.py: -------------------------------------------------------------------------------- 1 | from phox.experiment import AMF420Mesh 2 | import holoviews as hv 3 | import panel as pn 4 | import pickle 5 | 6 | if __name__ == '__main__': 7 | hv.extension('bokeh') 8 | 9 | with open('configs/ps_calibration_1560nm_20220313.p', 'rb') as f: 10 | ps_calibration = pickle.load(f) 11 | 12 | chip = AMF420Mesh( 13 | home=(0.0, 0.0), # mm 14 | interlayer_xy=(0.0015, -0.3095), # mm 15 | spot_rowcol=(436, 25), 16 | interspot_xy=(-67, 298), 17 | stage_port='/dev/ttyUSB2', 18 | laser_port='/dev/ttyUSB0', 19 | ps_calibration=ps_calibration, 20 | integration_time=1000 21 | ) 22 | 23 | pn.serve(chip.default_panel(), start=True, show=False, port=5006, 24 | websocket_origin="localhost:4444", 25 | ssl_certfile='/home/exx/ssl/jupyter_cert.pem', 26 | ssl_keyfile='/home/exx/ssl/jupyter_cert.key') 27 | -------------------------------------------------------------------------------- /.ipynb_checkpoints/serve_amf420-checkpoint.py: -------------------------------------------------------------------------------- 1 | from phox.experiment import AMF420Mesh 2 | import holoviews as hv 3 | import panel as pn 4 | import pickle 5 | 6 | if __name__ == '__main__': 7 | hv.extension('bokeh') 8 | 9 | with open('configs/ps_calibration_1560nm_20220313.p', 'rb') as f: 10 | ps_calibration = pickle.load(f) 11 | 12 | chip = AMF420Mesh( 13 | home=(0.0, 0.0), # mm 14 | interlayer_xy=(0.0015, -0.3095), # mm 15 | <<<<<<< HEAD:.ipynb_checkpoints/serve_amf420-checkpoint.py 16 | spot_xy=(436, 25), 17 | ======= 18 | spot_rowcol=(436, 25), 19 | >>>>>>> 5dddfbef682aefac8e625e90e101b0d74a87f167:serve_amf420.py 20 | interspot_xy=(-67, 298), 21 | stage_port='/dev/ttyUSB2', 22 | laser_port='/dev/ttyUSB0', 23 | ps_calibration=ps_calibration, 24 | integration_time=1000 25 | ) 26 | 27 | pn.serve(chip.default_panel(), start=True, show=False, port=5006, 28 | websocket_origin="localhost:4444", 29 | ssl_certfile='/home/exx/ssl/jupyter_cert.pem', 30 | ssl_keyfile='/home/exx/ssl/jupyter_cert.key') 31 | -------------------------------------------------------------------------------- /phox/instrumentation/lightwavemultimeter.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Union 3 | import numpy as np 4 | 5 | from .serial import SerialMixin 6 | 7 | 8 | class LightwaveMultimeterHP8163A(SerialMixin): 9 | def __init__(self, port: str = '/dev/ttyUSB2', source_idx: int = 2, gpib_addr: int = 18): 10 | self.source_idx = source_idx 11 | self.gpib_addr = gpib_addr 12 | SerialMixin.__init__(self, 13 | port=port, 14 | id_command='*IDN?', 15 | id_response='HP8163A', 16 | terminator='\r' 17 | ) 18 | 19 | def setup(self): 20 | self.write('++auto 1') 21 | self.write(f'++addr {self.gpib_addr}') 22 | 23 | def power(self, meas_time: float = 0.5) -> float: 24 | """Get power of laser (in mW) 25 | 26 | Args: 27 | meas_time: Amount of time to measure powers 28 | 29 | Returns: 30 | Laser power in mW 31 | 32 | """ 33 | 34 | self.write(f'read{self.source_idx}:pow?') 35 | time.sleep(meas_time) 36 | return float(self.read_until('\n')[0]) 37 | 38 | # self.write(f'sens{self.source_idx}:func:stat logg,star') 39 | # time.sleep(meas_time) 40 | # self.write(f'sens{self.source_idx}:func:res?') 41 | # self._ser.read() 42 | # n = int(self._ser.read()) 43 | # self._ser.read(n) 44 | # b = self._ser.read_until(b'\n') 45 | # return np.frombuffer(b[:-1], dtype=np.float32) 46 | 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phox 2 | 3 | Base repository for simulation and control of photonic devices. 4 | 5 | NOTE: this module is fairly raw and at the moment is only meant for replicating results in our papers: 6 | 7 | 8 | ## Installation 9 | 10 | Install in your python environment using: 11 | 12 | `pip install -e phox` 13 | 14 | You can then change `phox` if necessary. 15 | When importing `phox`, you can now treat it as any other module. 16 | No filepath setting necessary because `phox` will be in your environment's `site-packages` directory. 17 | 18 | ## Git Workflow 19 | 20 | ### Adding a new feature branch 21 | 22 | ``` 23 | git pull # update local based on remote 24 | git checkout develop # start branch from develop 25 | git checkout -b feature/feature-branch-name 26 | ``` 27 | 28 | Do all work on branch. After your changes, from the root folder, execute the following: 29 | 30 | ``` 31 | git add . && git commit -m 'insert your commit message here' 32 | ``` 33 | 34 | 35 | ### Rebasing and pull request 36 | 37 | First you need to edit your commit history by "squashing" commits. 38 | You should be in your branch `feature/feature-branch-name`. 39 | First look at your commit history to see how many commits you've made in your feature branch: 40 | 41 | ``` 42 | git log 43 | ``` 44 | Count the number of commits you've made and call that N. 45 | Now, execute the following: 46 | 47 | ``` 48 | git rebase -i HEAD~N 49 | ``` 50 | Squash any insignificant commits (or all commits into a single commit if you like). 51 | A good tutorial is provided 52 | [here](https://medium.com/@slamflipstrom/a-beginners-guide-to-squashing-commits-with-git-rebase-8185cf6e62ec). 53 | 54 | Now, you should rebase on top of the `develop` branch by executing: 55 | ``` 56 | git rebase develop 57 | ``` 58 | You will need to resolve any conflicts that arise manually during this rebase process. 59 | 60 | Now you will force-push this rebased branch using: 61 | ``` 62 | git push --set-upstream origin feature/feature-branch-name 63 | git push -f 64 | ``` 65 | 66 | Then you must submit a pull request using this [link](https://github.com/solgaardlab/simphox/pulls). 67 | 68 | ### Updating develop and master 69 | 70 | The admin of this repository is responsible for updating `develop` (unstable release) 71 | and `master` (stable release). 72 | This happens automatically once the admin approves pull request. 73 | 74 | ``` 75 | git checkout develop 76 | git merge feature/feature-branch-name 77 | ``` 78 | 79 | To update master: 80 | ``` 81 | git checkout master 82 | git merge develop 83 | ``` 84 | 85 | As a rule, only one designated admin should have permissions to do these steps. -------------------------------------------------------------------------------- /phox/model/legacy.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def dc(epsilon): 5 | return np.array([ 6 | [np.cos(np.pi / 4 + epsilon), 1j * np.sin(np.pi / 4 + epsilon)], 7 | [1j * np.sin(np.pi / 4 + epsilon), np.cos(np.pi / 4 + epsilon)] 8 | ]) 9 | 10 | 11 | def ps(upper, lower): 12 | return np.array([ 13 | [np.exp(1j * upper), 0], 14 | [0, np.exp(1j * lower)] 15 | ]) 16 | 17 | 18 | def mzi(theta, phi, n=2, i=0, j=None, 19 | theta_upper=0, phi_upper=0, epsilon=0, dtype=np.complex128): 20 | j = i + 1 if j is None else j 21 | epsilon = epsilon if isinstance(epsilon, tuple) else (epsilon, epsilon) 22 | mat = np.eye(n, dtype=dtype) 23 | mzi_mat = dc(epsilon[1]) @ ps(theta_upper, theta) @ dc(epsilon[0]) @ ps(phi_upper, phi) 24 | mat[i, i], mat[i, j] = mzi_mat[0, 0], mzi_mat[0, 1] 25 | mat[j, i], mat[j, j] = mzi_mat[1, 0], mzi_mat[1, 1] 26 | return mat 27 | 28 | 29 | def balanced_tree(n): 30 | # this is just defined for powers of 2 for simplicity 31 | assert np.floor(np.log2(n)) == np.log2(n) 32 | return [(2 * j * (2 ** k), 2 * j * (2 ** k) + (2 ** k)) 33 | for k in range(int(np.log2(n))) 34 | for j in reversed(range(n // (2 * 2 ** k)))], n 35 | 36 | 37 | def diagonal_tree(n, m=0): 38 | return [(i, i + 1) for i in reversed(range(m, n - 1))], n 39 | 40 | 41 | def mesh(thetas, phis, network, phases=None, epsilons=None): 42 | ts, n = network 43 | u = np.eye(n) 44 | epsilons = np.zeros_like(thetas) if epsilons is None else epsilons 45 | for theta, phi, t, eps in zip(thetas, phis, ts, epsilons): 46 | u = mzi(theta, phi, n, *t, eps) @ u 47 | if phases is not None: 48 | u = np.diag(phases) @ u 49 | return u 50 | 51 | 52 | def nullify(vector, i, j=None): 53 | n = len(vector) 54 | j = i + 1 if j is None else j 55 | theta = -np.arctan2(np.abs(vector[i]), np.abs(vector[j])) * 2 56 | phi = np.angle(vector[i]) - np.angle(vector[j]) 57 | nullified_vector = mzi(theta, phi, n, i, j) @ vector 58 | return np.mod(theta, 2 * np.pi), np.mod(phi, 2 * np.pi), nullified_vector 59 | 60 | 61 | # assumes topologically-ordered tree (e.g. above tree functions) 62 | def analyze(v, tree): 63 | ts, n = tree 64 | thetas, phis = np.zeros(len(ts)), np.zeros(len(ts)) 65 | for i, t in enumerate(ts): 66 | thetas[i], phis[i], v = nullify(v, *t) 67 | return thetas, phis, v[0] 68 | 69 | 70 | def reck(u): 71 | thetas, phis, mzi_lists = [], [], [] 72 | n = u.shape[0] 73 | for i in range(n - 1): 74 | tree = diagonal_tree(n, i) 75 | mzi_lists_i, _ = tree 76 | thetas_i, phis_i, _ = analyze(u.T[i], tree) 77 | u = mesh(thetas_i, phis_i, tree) @ u 78 | thetas.extend(thetas_i) 79 | phis.extend(phis_i) 80 | mzi_lists.extend(mzi_lists_i) 81 | phases = np.angle(np.diag(u)) 82 | return np.asarray(thetas), np.asarray(phis), (mzi_lists, n), phases 83 | 84 | 85 | def generate(thetas, phis, tree, epsilons=None): 86 | return mesh(thetas, phis, tree, epsilons)[0] 87 | 88 | 89 | def random_complex(n): 90 | return np.random.randn(n) + np.random.randn(n) * 1j 91 | -------------------------------------------------------------------------------- /scripts/mesh_blender.py: -------------------------------------------------------------------------------- 1 | from dphox.demo import mesh as device 2 | import bpy 3 | import pickle 4 | import numpy as np 5 | 6 | from matplotlib import cm 7 | 8 | # turn on bloom 9 | bpy.context.scene.render.engine = 'BLENDER_EEVEE' 10 | bpy.context.scene.eevee.use_bloom = True 11 | 12 | all_powers = [] 13 | all_ps = [] 14 | 15 | for material in bpy.data.materials: 16 | material.user_clear() 17 | bpy.data.materials.remove(material) 18 | 19 | for mesh in bpy.data.meshes: 20 | mesh.user_clear() 21 | bpy.data.meshes.remove(mesh) 22 | 23 | for collection in bpy.data.collections: 24 | if collection.name != 'Collection': 25 | collection.user_clear() 26 | bpy.data.collections.remove(collection) 27 | 28 | with open(bpy.path.abspath('//mesh_image_self_config.p'), 'rb') as f: 29 | data = pickle.load(f) 30 | 31 | for step in data: 32 | powers = step['mesh'] 33 | powers = powers / np.sum(powers, axis=0) 34 | powers = powers[::-1] 35 | all_powers.append(powers) 36 | all_ps.append(step['ps']) 37 | 38 | power_array, ps_array = device.demo_3d_arrays() 39 | 40 | 41 | def create_emission_shader(color, strength, mat_name): 42 | # create a new material resource (with its 43 | # associated shader) 44 | mat = bpy.data.materials.new(mat_name) 45 | # enable the node-graph edition mode 46 | mat.use_nodes = True 47 | 48 | # clear all starter nodes 49 | nodes = mat.node_tree.nodes 50 | nodes.clear() 51 | 52 | # add the Emission node 53 | node_emission = nodes.new(type="ShaderNodeEmission") 54 | # (input[0] is the color) 55 | node_emission.inputs[0].default_value = color 56 | # (input[1] is the strength) 57 | node_emission.inputs[1].default_value = strength 58 | 59 | # add the Output node 60 | node_output = nodes.new(type="ShaderNodeOutputMaterial") 61 | 62 | # link the two nodes 63 | links = mat.node_tree.links 64 | link = links.new(node_emission.outputs[0], node_output.inputs[0]) 65 | 66 | # return the material reference 67 | return mat 68 | 69 | 70 | def power_material(power, name): 71 | power = 0 if power < 0.05 else power 72 | color = np.abs(power) * np.array((1, 0.5, 0, 1)) 73 | return create_emission_shader(color, 10, name) 74 | 75 | 76 | def ps_material(ps, name): 77 | ps /= np.pi * 2 78 | color = np.abs(ps) * np.array((0, 1, 0, 1)) 79 | return create_emission_shader(color, 2, name) 80 | 81 | 82 | for i in range(6): 83 | for j in range(19): 84 | power_array[i][j] = power_array[i][j].apply_translation((-device.center[0], -device.center[1], 0)) 85 | ps_array[i][j] = ps_array[i][j].apply_translation((-device.center[0], -device.center[1], 0)) 86 | 87 | for idx in range(5): 88 | mesh_collection = bpy.data.collections.new(f'mesh_collection_{idx}') 89 | bpy.context.scene.collection.children.link(mesh_collection) 90 | for i in range(6): 91 | for j in range(19): 92 | geom = power_array[i][j] 93 | new_mesh = bpy.data.meshes.new(f'wg_{i}_{j}_{idx}') 94 | new_mesh.from_pydata(geom.vertices, geom.edges, geom.faces) 95 | new_mesh.update() 96 | si = bpy.data.objects.new(f'si_{i}_{j}_{idx}', new_mesh) 97 | mesh_collection.objects.link(si) 98 | power_mat = power_material(all_powers[idx][i, j], f'light_{i}_{j}_{idx}') 99 | si.data.materials.append(power_mat) 100 | if (j, i) in all_ps[-1]: 101 | ps_mat = ps_material(all_ps[idx][(j, i)], f'phase_{i}_{j}_{idx}') 102 | geom = ps_array[-i - 1][j] 103 | new_mesh = bpy.data.meshes.new(f'ps_{i}_{j}_{idx}') 104 | new_mesh.from_pydata(geom.vertices, geom.edges, geom.faces) 105 | new_mesh.update() 106 | heater = bpy.data.objects.new(f'heater_{i}_{j}_{idx}', new_mesh) 107 | mesh_collection.objects.link(heater) 108 | heater.data.materials.append(ps_mat) -------------------------------------------------------------------------------- /phox/instrumentation/control.py: -------------------------------------------------------------------------------- 1 | import nidaqmx 2 | import nidaqmx.system 3 | 4 | import numpy as np 5 | from typing import Callable, Optional, Tuple 6 | from nidaqmx.constants import AcquisitionType 7 | from nidaqmx.stream_readers import AnalogMultiChannelReader 8 | import time 9 | import panel as pn 10 | 11 | 12 | class NIDAQControl: 13 | def __init__(self, vmin: float = 0, vmax: float = 5, sample_rate: int = 10000, num_samples: int = 1000, channels: Tuple = (28, 24, 16, 20)): 14 | self.system = nidaqmx.system.System.local() 15 | self.ao_channels = [channel for device in self.system.devices 16 | for channel in device.ao_physical_chans] 17 | self.ai_channels = [channel for device in self.system.devices 18 | for channel in device.ai_physical_chans] 19 | self.vmax = vmax 20 | self.vmin = vmin 21 | self.sample_rate = sample_rate 22 | self.num_samples = num_samples 23 | self.oscope = nidaqmx.Task() 24 | for chan in channels: 25 | self.oscope.ai_channels.add_ai_voltage_chan(self.ai_channels[chan].name, max_val=2, min_val=0) 26 | self.oscope.timing.cfg_samp_clk_timing( 27 | rate=sample_rate, 28 | sample_mode=nidaqmx.constants.AcquisitionType.CONTINUOUS, 29 | samps_per_chan=num_samples) 30 | self.meas_buffer = np.zeros((len(channels), num_samples), np.float64) 31 | reader = AnalogMultiChannelReader(self.oscope.in_stream) 32 | def oscope_cb(*args): 33 | reader.read_many_sample(self.meas_buffer, num_samples) 34 | return 0 35 | self.oscope.register_every_n_samples_acquired_into_buffer_event(sample_interval=num_samples, callback_method=oscope_cb) 36 | # self.oscope.start() 37 | 38 | def reset(self): 39 | for device in self.system.devices: 40 | device.reset_device() 41 | 42 | def ttl_toggle(self, chan: int): 43 | with nidaqmx.Task() as task: 44 | task.ao_channels.add_ao_voltage_chan(self.ao_channels[chan].name) 45 | task.write(5) 46 | task.write(0) 47 | 48 | def continuous_slider(self, chan: int, name: str, vlim: Tuple[float, float] = (0, 5), default_v: float = 0): 49 | def change_voltage(*events): 50 | for event in events: 51 | if event.name == 'value': 52 | with nidaqmx.Task() as task: 53 | task.ao_channels.add_ao_voltage_chan(self.ao_channels[chan].name) 54 | task.write(event.new) 55 | voltage = pn.widgets.FloatSlider(start=vlim[0], end=vlim[1], step=0.01, 56 | value=default_v, name=name, format='1[.]000') 57 | # voltage.param.watch(change_voltage, 'value') 58 | return voltage 59 | 60 | def write_chan(self, chan: int, voltages: np.ndarray, n_callback: Optional[Tuple[Callable, int]] = None, 61 | sweep_time: float = 10) -> int: 62 | """Write voltages to channel 63 | 64 | Args: 65 | chan: Channel to write 66 | voltages: Voltages sent to channel 67 | n_callback: A tuple of num samples and callback function 68 | sweep_time: Sweep time in seconds 69 | 70 | Returns: 71 | Number of written samples 72 | 73 | """ 74 | if np.sum(voltages > self.vmax) > 0: 75 | raise ValueError(f'All voltages written to channel must be <= {self.vmax}.') 76 | if np.sum(voltages < self.vmin) > 0: 77 | raise ValueError(f'All voltages written to channel must be >= {self.vmin}.') 78 | 79 | num_voltages = 1 if not isinstance(voltages, np.ndarray) else voltages.size 80 | task = nidaqmx.Task() 81 | task.ao_channels.add_ao_voltage_chan(self.ao_channels[chan].name) 82 | if n_callback is not None: 83 | task.timing.cfg_samp_clk_timing(rate=num_voltages / sweep_time, sample_mode=AcquisitionType.CONTINUOUS) 84 | task.register_every_n_samples_transferred_from_buffer_event(*n_callback) 85 | num_samples = task.write(voltages) 86 | task.start() 87 | time.sleep(num_samples / task.timing.samp_clk_rate) 88 | task.close() 89 | return num_samples 90 | 91 | def read_chan(self, chan: int, num_voltages: int, rate: float = 100000, average: bool = True) -> int: 92 | """Read voltages from channel 93 | 94 | Args: 95 | chan: Channel to write 96 | num_voltages: Number of voltages to read 97 | rate: Number of voltages read per second 98 | average: Whether to average the voltages read out 99 | 100 | Returns: 101 | Number of written samples 102 | 103 | """ 104 | with nidaqmx.Task() as task: 105 | task.ai_channels.add_ai_voltage_chan(self.ai_channels[chan].name) 106 | task.timing.cfg_samp_clk_timing(rate=rate, sample_mode=AcquisitionType.CONTINUOUS) 107 | voltages = task.read(num_voltages) 108 | return np.mean(voltages) if average else voltages 109 | 110 | -------------------------------------------------------------------------------- /phox/instrumentation/laser.py: -------------------------------------------------------------------------------- 1 | import time 2 | from typing import Union, Tuple 3 | import panel as pn 4 | 5 | from .serial import SerialMixin 6 | 7 | 8 | class LaserHP8164A(SerialMixin): 9 | def __init__(self, port: str = '/dev/ttyUSB2', source_idx: int = 0, gpib_addr: int = 9): 10 | """Agilent Laser Module with Holoviz GUI interface 11 | 12 | Args: 13 | port: serial port string 14 | source_idx: source index on the machine (slot in which the laser is located) 15 | """ 16 | self.source_idx = source_idx 17 | self.gpib_addr = gpib_addr 18 | SerialMixin.__init__(self, 19 | port=port, 20 | id_command='*IDN?', 21 | id_response='HP8164A', 22 | terminator='\r' 23 | ) 24 | self.write(f'sour{self.source_idx}:wav?') 25 | self._wavelength = float(self.read_until('\n')[0]) * 1e6 26 | self.write(f'sour{self.source_idx}:pow?') 27 | self._power = float(self.read_until('\n')[0]) * 1000 28 | self.write(f'sour{self.source_idx}:pow:stat?') 29 | self._state = int(self.read_until('\n')[0]) 30 | 31 | def setup(self): 32 | self.write('++auto 1') 33 | self.write(f'++addr {self.gpib_addr}') 34 | 35 | @property 36 | def on(self) -> bool: 37 | return self.state == 1 38 | 39 | @property 40 | def state(self) -> int: 41 | """ Get state of laser (on, off) 42 | 43 | Returns: 44 | 0 (off) or 1 (on) 45 | 46 | """ 47 | return self._state 48 | 49 | @state.setter 50 | def state(self, state: int): 51 | """ Set state of laser (on, off) 52 | 53 | Args: 54 | on: 55 | 56 | """ 57 | self.write(f'sour{self.source_idx}:pow:stat {state}') 58 | self._state = state 59 | 60 | @property 61 | def power(self) -> float: 62 | """ Power of laser in mW 63 | 64 | Returns: 65 | Laser power in mW 66 | """ 67 | return self._power 68 | 69 | @power.setter 70 | def power(self, power: float): 71 | """Set power of the laser (in mW) 72 | 73 | Args: 74 | power: laser power in mW 75 | 76 | """ 77 | self.write(f'sour{self.source_idx}:pow {power}mW') 78 | self._power = power 79 | 80 | @property 81 | def wavelength(self): 82 | """Set wavelength of the laser in nm 83 | 84 | Returns: 85 | Wavelength in um 86 | 87 | """ 88 | return self._wavelength 89 | 90 | @wavelength.setter 91 | def wavelength(self, wavelength: float): 92 | """Set wavelength of the laser in um 93 | 94 | Args: 95 | wavelength: wavelength in um 96 | 97 | Returns: 98 | 99 | """ 100 | self.write(f'sour{self.source_idx}:wav {wavelength * 1000}NM') 101 | self._wavelength = wavelength 102 | 103 | def sweep_wavelength(self, start_wavelength: float, stop_wavelength: float, step: float, 104 | speed: float, timeout: float): 105 | """ 106 | 107 | Args: 108 | start_wavelength: 109 | stop_wavelength: 110 | step: 111 | speed: 112 | timeout: 113 | 114 | Returns: 115 | 116 | """ 117 | self.write(f'wav:swe:star {start_wavelength}nm') 118 | self.write(f'wav:swe:stop {stop_wavelength}nm') 119 | self.write(f'wav:swe:step {step}nm') 120 | self.write(f'wav:swe:spe {speed}nm/s') 121 | self.write('wav:swe 1') 122 | time.sleep(timeout) 123 | self.write('wav:swe 0') 124 | 125 | def wavelength_panel(self, wavelength_range: Tuple[float, float] = (1.530, 1.584), dlam: float = 0.001): 126 | """ 127 | Panel for dispersion handling 128 | 129 | Args: 130 | wavelength_range: wavelength range 131 | dlam: lambda step size for adjustment 132 | 133 | Returns: 134 | 135 | """ 136 | dispersion = pn.widgets.FloatSlider(start=wavelength_range[0], end=wavelength_range[1], step=dlam, 137 | value=self.wavelength, name=r'Wavelength (um)', 138 | format='1[.]000') 139 | 140 | def change_wavelength(*events): 141 | for event in events: 142 | if event.name == 'value': 143 | self.wavelength = event.new 144 | 145 | dispersion.param.watch(change_wavelength, 'value') 146 | 147 | return dispersion 148 | 149 | def power_panel(self, power_range: Tuple[float, float] = (0.05, 4.25), interval=0.001): 150 | """ 151 | 152 | Args: 153 | power_range: Range of powers 154 | interval: Interval between the powers that can be specified 155 | 156 | Returns: 157 | Panel for jupyter notebook for controlling the laser 158 | 159 | """ 160 | power = pn.widgets.FloatSlider(start=power_range[0], end=power_range[1], step=interval, 161 | value=self.power, name='Power (mW)', format='1[.]000') 162 | state = pn.widgets.Toggle(name='Laser Enable', value=bool(self.state)) 163 | 164 | def change_power(*events): 165 | for event in events: 166 | if event.name == 'value': 167 | self.power = event.new 168 | 169 | def change_state(*events): 170 | for event in events: 171 | if event.name == 'value': 172 | self.state = int(event.new) 173 | 174 | state.param.watch(change_state, 'value') 175 | power.param.watch(change_power, 'value') 176 | return pn.Column(power, state) 177 | -------------------------------------------------------------------------------- /phox/apps/opow.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import numpy as np 3 | 4 | from dataclasses import dataclass 5 | from scipy.linalg import block_diag 6 | import random 7 | 8 | 9 | def data_to_sha256_vec(data): 10 | hasher1 = hashlib.sha3_256() 11 | hasher1.update(data.encode('ascii')) 12 | b = np.frombuffer(np.binary_repr(int(hasher1.hexdigest(), 16)).encode('utf-8'), dtype='S1').astype(int) 13 | return np.hstack((np.zeros(256 - b.size), b)) 14 | 15 | 16 | def sha256_str(val: str): 17 | val = val.encode('utf-8') if isinstance(val, str) else val 18 | return np.binary_repr(int(hashlib.sha256(val).hexdigest(), 16)).encode('utf-8') 19 | 20 | 21 | def double_sha256_str(val: str): 22 | return sha256_str(sha256_str(val)) 23 | 24 | 25 | @dataclass 26 | class LHNode: 27 | left: "LHNode" 28 | right: "LHNode" 29 | value: bytes 30 | 31 | 32 | @dataclass 33 | class LHMerkleTree: 34 | """For computational efficiency, stores large amount of transaction data in Merkle trees. 35 | 36 | Attributes: 37 | values: The list of values to store in the Merkle tree (forming the leaves of Merkle tree). 38 | 39 | """ 40 | values: list[str] 41 | 42 | def __post_init__(self) -> None: 43 | leaves: list[LHNode] = [LHNode(None, None, double_sha256_str(e)) for e in self.values] 44 | if len(leaves) % 2 == 1: 45 | leaves.append(leaves[-1:][0]) # duplicate last elem if odd number of elements 46 | self.root: LHNode = self._build_tree(leaves) 47 | 48 | def _build_tree(self, nodes: list[LHNode]) -> LHNode: 49 | if len(nodes) == 2: 50 | return LHNode(nodes[0], nodes[1], double_sha256_str(nodes[0].value + nodes[1].value)) 51 | left = self._build_tree(nodes[:len(nodes) // 2]) 52 | right = self._build_tree(nodes[len(nodes) // 2:]) 53 | value: bytes = double_sha256_str(left.value + right.value) 54 | return LHNode(left, right, value) 55 | 56 | @property 57 | def root_hash(self) -> bytes: 58 | return self.root.value 59 | 60 | 61 | @dataclass 62 | class LHBlock: 63 | """The LightHash block 64 | 65 | Attributes: 66 | data: The data organized using a LightHash Merkle Tree 67 | hash: The hash of this block (updated in init method upon solving the puzzle) 68 | last_block: The last block in this blockchain 69 | difficulty: The difficulty of the problem/puzzle based on number of zero bits at beginning. 70 | nonce: The varying number in the block that is used as a tuning knob to solve the puzzle 71 | n: number of inputs and outputs for the optical device 72 | k: the numerical resolution, or number of integers spaced two apart symmetrically about 0. 73 | 74 | """ 75 | data: LHMerkleTree 76 | hash: str = None # this will be overwritten during block creation 77 | last_block: "LHBlock" = None # genesis block if None 78 | difficulty: int = 6 # this is much larger in practice 79 | nonce: int = 0 80 | n: int = 64 81 | k: int = 2 82 | 83 | def __post_init__(self): 84 | n, k = self.n, self.k 85 | self.last_hash = self.last_block.hash if self.last_block is not None else (np.binary_repr(0) * 256) 86 | np.random.seed(int(self.last_hash) % 32) # use the previous hash to seed the randomness of Q 87 | 88 | # generate Q matrix and find all necessary constants via random trials 89 | q = block_diag(*[random_lh_matrix(n, n, k).squeeze() for _ in range(256 // n)]) 90 | x = q[:n, :n] @ random_lh_input(n, 10000, False) 91 | hist, bins = np.histogram(np.abs(x.flatten()), bins=int(np.max(x)), density=True) 92 | cdf = np.cumsum(hist) 93 | thresh = bins[np.argmin(np.abs(cdf - 0.5))] 94 | idx = int(thresh) 95 | self.yth = (bins[idx] + bins[idx + 2]) / 2 96 | self.q = q 97 | 98 | solved = False 99 | while not solved: 100 | self.nonce = random.getrandbits(256) 101 | sol = lighthash(q, self.lighthash_input, self.yth) 102 | solved = sol < 2 ** (256 - self.difficulty) 103 | self.hash = np.binary_repr(sol) 104 | 105 | @property 106 | def lighthash_input(self): 107 | return self.last_hash + self.data.root_hash.decode('utf-8') + np.binary_repr(self.nonce) 108 | 109 | 110 | def lighthash(q, data, yth): 111 | """The core LightHash function is simulated on a digital platform here. 112 | 113 | Args: 114 | q: LightHash matrix 115 | data: Data to solve lighthash 116 | yth: Threshold 117 | 118 | Returns: 119 | The lighthash output bits. 120 | 121 | """ 122 | b_in = data_to_sha256_vec(data) 123 | x = 2 * b_in - 1 124 | y = q @ x 125 | b = (y > yth) 126 | b_out = np.logical_xor(b_in, b).astype(int) 127 | return b_out.dot(2 ** np.arange(b_out.size, dtype=object)[::-1]) 128 | 129 | 130 | def generate_random_lh_transactions(people, limit: int=100, num_tx: int =100): 131 | """Generate random transactions to tes the LightHash emulator. 132 | 133 | Args: 134 | people: The people in the universe for these transactions 135 | limit: Limit of the number of coins that can be sent to people 136 | num_tx: Number of total transactions in this segment 137 | 138 | Returns: 139 | 140 | """ 141 | transaction_data = "" 142 | for i in range(num_tx): 143 | person_a, person_b = np.random.choice(people, 2, replace=False) 144 | amount = np.random.choice(limit) 145 | transaction_data += f"{person_a} sent {person_b} {amount} coin.\n" 146 | return transaction_data 147 | 148 | 149 | def random_lh_matrix(n_vecs, n, num_vals, normed=False): 150 | offset = num_vals - 1 151 | v = (2 * np.random.randint(num_vals, size=(n_vecs, n)) - offset) 152 | return v / np.sqrt(np.sum(v ** 2, axis=1)[:, np.newaxis]) if normed else v 153 | 154 | 155 | def random_lh_input(n, n_trials, normed=True): 156 | v = (2 * np.random.randint(2, size=(n, n_trials)) - 1) 157 | return v / np.sqrt(n) if normed else v 158 | -------------------------------------------------------------------------------- /phox/model/phase.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from typing import Tuple, Optional 3 | from scipy.optimize import curve_fit 4 | from scipy.signal import find_peaks_cwt 5 | 6 | 7 | class PhaseCalibration: 8 | def __init__(self, vs: np.ndarray, powers: np.ndarray, spot: Tuple[int, int], 9 | coefficients: Optional[np.ndarray] = None, a: Optional[float] = None, b: Optional[float] = None, 10 | p0: Tuple[float, ...] = (1, 0, 0, 0.3, 0, 0)): 11 | """Phase calibration object consisting of voltages and camera spot power measurements, 12 | We assume that light enters the lower (smaller idx) input. 13 | 14 | Args: 15 | vs: voltages 16 | powers: camera spot powers (tuple of 18 spot powers for a 6x3 array... currently hardcoded!) 17 | spot: Tuple of layer, index of the waveguide mode for the bottom port of the calibration 18 | coefficients: p and q coefficients for the polynomial fit for phase v voltage and voltage v phase 19 | a: amplitude for the calibration 20 | b: offset for the calibration 21 | p0: initial guess for the calibration of the phase shifter 22 | """ 23 | self.vs = vs 24 | self.vmin = np.min(self.vs) 25 | self.vmax = np.max(self.vs) 26 | self.powers = np.fliplr(powers.reshape((6, 3, -1))) 27 | self.spot = spot 28 | self.idx = spot[1] 29 | if coefficients is None: 30 | fit = self.fit(p0) 31 | self.coefficients, self.a, self.b = fit[0], fit[1], fit[2] # tuple assignment being weird? 32 | else: 33 | self.coefficients, self.a, self.b = coefficients, a, b 34 | vs, sr = self.vs, self.lower_split_ratio 35 | self.top_peaks = find_peaks_cwt(sr, widths=[800])[1] 36 | self.bottom_peaks = find_peaks_cwt(-sr, widths=[800]) 37 | 38 | def fit(self, p0: Tuple[float, ...], tol: float = 0.01, 39 | overwrite_shift: bool = True) -> Tuple[np.ndarray, float, float]: 40 | # automatically find the first lower peak 41 | lower_v = self.vs[find_peaks_cwt(-self.lower_split_ratio, widths=[800])[0]] 42 | 43 | p0_ = (*p0[:-1], -lower_v) if overwrite_shift else p0 44 | print(f'Initial params: {p0_}') 45 | p, pcov = curve_fit(cal_v_power, self.vs, self.lower_split_ratio, p0=p0_) 46 | error = np.sqrt(np.sum(np.diag(pcov))) 47 | 48 | if error > 3 * tol: 49 | print(f'Trying some more initial conditions... error {error} is >3x more than the tolerable error {tol}') 50 | 51 | delta = 0 52 | while error > 3 * tol and p0_[-1] < 0 and delta < 2 * np.pi: 53 | delta += 0.05 54 | p, pcov = curve_fit(cal_v_power, self.vs, self.lower_split_ratio, p0=(*p0_[:-1], p0_[-1] + delta)) 55 | error = np.sqrt(np.sum(np.diag(pcov))) 56 | 57 | delta = 0 58 | # exhaust some more initial conditions before going to the next step 59 | while error > 3 * tol and p0_[-1] > -5 and delta < -2 * np.pi: 60 | delta -= 0.05 61 | p, pcov = curve_fit(cal_v_power, self.vs, self.lower_split_ratio, p0=(*p0_[:-1], p0_[-1] + delta)) 62 | error = np.sqrt(np.sum(np.diag(pcov))) 63 | 64 | print(f'Final p: {p}') 65 | 66 | if error > 3 * tol: 67 | raise RuntimeError(f'FATAL: The error {error} is too big, need to recalibrate...') 68 | if error > tol: 69 | print(f'WARNING: final error {error} is more than the tolerable error {tol}') 70 | print(f'Fit v2p function with error {error}!') 71 | 72 | q, qcov = curve_fit(cal_phase_v, np.polyval(p[2:], self.vs), self.vs ** 2) 73 | qerror = np.sqrt(np.sum(np.diag(qcov))) 74 | print(f'Fit p2v function with error {qerror}!') 75 | 76 | print(f'Final q: {q}') 77 | 78 | if error > 3 * tol: 79 | raise RuntimeError(f'FATAL: The error {error} is too big, need to recalibrate...') 80 | 81 | # ensure all curves are in the approx the same voltage range 82 | vmin = np.sqrt(np.abs(np.polyval(q, 0))) 83 | vmax = np.sqrt(np.abs(np.polyval(q, np.pi))) 84 | print(f'The 0π, 2π voltages are now {vmin, vmax}') 85 | if vmax > self.vmax: 86 | p[-1] += np.pi 87 | q, _ = curve_fit(cal_phase_v, np.polyval(p[2:], self.vs), self.vs ** 2) 88 | vmin = np.sqrt(np.abs(np.polyval(q, 0))) 89 | vmax = np.sqrt(np.abs(np.polyval(q, np.pi))) 90 | print(f'The 0π, 2π voltages are now {vmin, vmax}') 91 | return np.vstack([p[2:], q]), p[0], p[1] 92 | 93 | @property 94 | def lower_split_ratio(self) -> np.ndarray: 95 | return self.powers[self.idx, 2] / (self.powers[self.idx + 1, 2] + self.powers[self.idx, 2]) 96 | 97 | @property 98 | def upper_split_ratio(self) -> np.ndarray: 99 | return 1 - self.lower_split_ratio 100 | 101 | @property 102 | def split_ratio_fit(self) -> np.ndarray: 103 | return cal_v_power(self.vs, self.a, self.b, *self.coefficients[0]) 104 | 105 | @property 106 | def upper_out(self) -> np.ndarray: 107 | return self.powers[self.idx + 1, 2] / self.powers[self.idx, 0] 108 | 109 | @property 110 | def lower_out(self) -> np.ndarray: 111 | return self.powers[self.idx, 2] / self.powers[self.idx, 0] 112 | 113 | @property 114 | def upper_arm(self) -> np.ndarray: 115 | return self.powers[self.idx, 1] / (self.powers[self.idx, 1] + self.powers[self.idx + 1, 1]) 116 | 117 | @property 118 | def lower_arm(self) -> np.ndarray: 119 | return 1 - self.upper_arm 120 | 121 | @property 122 | def total_arm(self) -> np.ndarray: 123 | return (self.powers[self.idx, 1] + self.powers[self.idx + 1, 1]) / (self.powers[self.idx, 0]) 124 | 125 | @property 126 | def total_out(self) -> np.ndarray: 127 | return (self.powers[self.idx, 2] + self.powers[self.idx + 1, 2]) / (self.powers[self.idx, 0]) 128 | 129 | def raw(self, bottom=False, idx=0) -> np.ndarray: 130 | return self.powers[self.idx + bottom, idx] 131 | 132 | @property 133 | def dict(self) -> dict: 134 | return { 135 | 'vs': self.vs, 136 | 'powers': np.fliplr(self.powers), 137 | 'coefficients': self.coefficients, 138 | 'spot': self.spot, 139 | 'a': self.a, 140 | 'b': self.b 141 | } 142 | 143 | def v2p(self, voltage: float) -> np.ndarray: 144 | """Voltage to phase conversion for a give phase shifter 145 | 146 | Args: 147 | voltage: voltage to convert 148 | 149 | Returns: 150 | Phase converted from voltage 151 | 152 | """ 153 | p, _ = self.coefficients 154 | return np.polyval(p, voltage) * 2 155 | 156 | def p2v(self, phase: float) -> np.ndarray: 157 | """Phase to voltage conversion for a give phase shifter 158 | 159 | Args: 160 | phase: phase to convert 161 | 162 | Returns: 163 | Voltage converted from phase 164 | 165 | """ 166 | _, q = self.coefficients 167 | return np.sqrt(np.abs(np.polyval(q, phase / 2))) # abs suppresses in case negative value 168 | 169 | def p2v_lookup(self, phase): 170 | sr = self.lower_split_ratio 171 | idx_pi = self.top_peaks[0] 172 | idx_0, idx_2pi = self.bottom_peaks[0], self.bottom_peaks[1] 173 | if isinstance(phase, np.ndarray): 174 | phase_pi = phase[phase >= np.pi] 175 | phase_0 = phase[phase < np.pi] 176 | i_0 = np.argmin( 177 | np.abs(np.sin(phase_0[:, np.newaxis] / 2) ** 2 - sr[idx_0:idx_pi][np.newaxis, :]), axis=1) 178 | i_pi = np.argmin( 179 | np.abs(np.sin(phase_pi[:, np.newaxis] / 2) ** 2 - sr[idx_pi:idx_2pi][np.newaxis, :]), axis=1) 180 | v_0 = self.vs[idx_0 + i_0] 181 | v_pi = self.vs[idx_pi + i_pi] 182 | v = np.zeros_like(phase) 183 | v[phase >= np.pi] = v_pi 184 | v[phase < np.pi] = v_0 185 | else: 186 | if 0 <= phase < np.pi: 187 | v = self.vs[idx_0 + np.argmin(np.abs(np.sin(phase / 2) ** 2 - sr[idx_0:idx_pi]))] 188 | else: 189 | v = self.vs[idx_pi + np.argmin(np.abs(np.sin(phase / 2) ** 2 - sr[idx_pi:idx_2pi]))] 190 | return v 191 | 192 | 193 | def cal_v_power(v, a, b, p0, p1, p2, p3): 194 | # take the absolute value of a to ensure a is positive! 195 | # need to also take absolute value of p3 196 | # ensure that the shifted phase always goes from +0 to +np.pi to ensure it is within desired range 197 | return np.abs(a) * np.sin(p0 * v ** 3 + p1 * v ** 2 + p2 * v - np.abs(p3)) ** 2 + b 198 | 199 | 200 | def cal_phase_v(x, q0, q1, q2, q3): 201 | return q0 * x ** 3 + q1 * x ** 2 + q2 * x + q3 202 | -------------------------------------------------------------------------------- /phox/instrumentation/serial.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Joe Landry' 2 | 3 | import serial 4 | from serial import rs485 5 | import logging 6 | import time 7 | import re 8 | import threading 9 | 10 | from typing import Tuple, Optional, Union 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | 15 | class SerAttr(object): 16 | """ 17 | Enum for base attributes 18 | """ 19 | 20 | port = 'port' 21 | id_command = 'id_command' 22 | id_response = 'id_response' 23 | default_timeout = 'default_timeout' 24 | terminator = 'terminator' 25 | baudrate = 'baudrate' 26 | bytesize = 'bytesize' 27 | parity = 'parity' 28 | xonxoff = 'xonxoff' 29 | rtscts = 'rtscts' 30 | stopbits = 'stopbits' 31 | 32 | 33 | class SerialMixin(object): 34 | """ 35 | Serial: Base class for all base communications based devices. Handles opening 36 | of ports, as well as reading and writing. 37 | """ 38 | 39 | def __init__(self, port: str = '/dev/ttyUSB0', id_command: str = '', id_response: str = '', 40 | default_timeout: float = 0.1, terminator: str = '\r', baudrate: int = 115200, 41 | bytesize: int = 8, parity: str = 'N', xonxoff: bool = False, rtscts: bool = False, stopbits: int = 1): 42 | """ 43 | Sets up basic functionality for base communications 44 | """ 45 | self.lock = threading.Lock() 46 | 47 | self.port = port # Port (usually 'COMX' on Windows) 48 | self.id_command = id_command # Command used to verify device communication 49 | self.id_response = id_response # Response used to verify device communication 50 | self.default_timeout = default_timeout 51 | self.terminator = terminator # Character to send at the end of each line 52 | self.baudrate = baudrate 53 | self.bytesize = bytesize 54 | self.parity = parity 55 | self.xonxoff = xonxoff 56 | self.rtscts = rtscts 57 | self.stopbits = stopbits 58 | self._ser = serial.Serial( 59 | port=port, 60 | baudrate=baudrate, 61 | bytesize=bytesize, 62 | parity=parity, 63 | stopbits=stopbits, 64 | xonxoff=xonxoff, 65 | rtscts=rtscts 66 | ) # Serial object provided by pyserial 67 | 68 | self._is_verified = False 69 | self._maxBuffer = 4096 # Default device output buffer size 70 | self._isOnline = False # True when device is verified by instrument returning ID response. 71 | self._ser.write_timeout = .5 # [s] Write timeout. Necessary for some instruments (depending on handshaking?) 72 | 73 | def enable_rs485(self): 74 | self._ser.rs485_mode = rs485.RS485Settings(True, True) 75 | 76 | def open(self): 77 | """ 78 | 1. Attempts to open the port with the given settings. 79 | 2. Asks for the instrument's ID to make sure it can send/receive commands 80 | :return: None 81 | """ 82 | if self._ser.is_open: 83 | self._ser.close() 84 | try: 85 | self._ser.open() 86 | self.verify() 87 | except serial.serialutil.SerialException: 88 | logger.warning('%s could not be opened for %s. Might be off or disconnected.', 89 | self._ser.port, self.__class__.__name__) 90 | 91 | def verify(self): 92 | message, matched = self.write(self.id_command).read_until(self.id_response) 93 | if matched: 94 | logger.info('Verified device operation') 95 | self._is_verified = True 96 | else: 97 | self._is_verified = False 98 | logger.error('Failed verification. Device might be off.') 99 | 100 | def close(self): 101 | self._ser.close() 102 | 103 | def write(self, cmd: str): 104 | """ 105 | Attempts to send command to device. No command is sent if the port isn't open. 106 | :param cmd: string; command 107 | :return: None 108 | """ 109 | self.lock.acquire() 110 | self._open_check() 111 | self.flush() # Flush before writing to clear any data in input buffer 112 | if self._ser.is_open: 113 | try: 114 | self._ser.write(f'{cmd}{self.terminator}'.encode()) 115 | logger.info(f'Sent command: {cmd}') 116 | except serial.serialutil.SerialTimeoutException: 117 | logger.warning('Could not send command. Timeout exception.') 118 | else: 119 | logger.warning(f'Command "{cmd}" not sent. Port closed.') 120 | self.lock.release() 121 | return self 122 | 123 | def read_until(self, expr: str, group_num: int = None, timeout: int = 0) -> Tuple[ 124 | Optional[Union[str, tuple]], bool]: 125 | # """ 126 | # Reads output until a specific phrase is found. If no timeout is provided, the config timeout is used 127 | # :param expr: string after which to stop reading bytes 128 | # :param group_num: group number 129 | # :param timeout: timeout [s] 130 | # :return: tuple; (message, success); message is the message that was read corresponding to the groupNumber. 131 | # success is whether or not there was a match to the expression. 132 | # """ 133 | """Reads output until a specific phrase is found. If no timeout is provided, the config timeout is used 134 | 135 | Args: 136 | expr: string after which to stop reading bytes 137 | group_num: group number 138 | timeout: timeout [s] 139 | 140 | Returns: 141 | (message, success); message is the message that was read corresponding to the groupNumber. 142 | success is whether or not there was a match to the expression. 143 | 144 | """ 145 | self.lock.acquire() 146 | if not self._ser.is_open: 147 | self.lock.release() 148 | return None, False 149 | timeout = timeout if timeout != 0 else self.default_timeout # Use default timeout if one is not provided 150 | message = '' 151 | if self._ser.is_open: 152 | start_time = time.time() 153 | while time.time() - start_time < timeout: 154 | message = f'{message}{self._ser.read(self._ser.in_waiting).decode("utf-8")}' 155 | match = re.search(expr, message) 156 | if match is not None: 157 | logger.debug(f"Matched expression: {expr}") 158 | if group_num is None: 159 | self.lock.release() 160 | return message, True 161 | elif isinstance(group_num, (list, tuple)): 162 | self.lock.release() 163 | return tuple(match.group(i) for i in group_num), True 164 | else: 165 | self.lock.release() 166 | return match.group(group_num), True 167 | logger.warning(f'No regex match for: {expr}') 168 | self.lock.release() 169 | return message, False 170 | 171 | def setup(self): 172 | pass 173 | 174 | def read(self, num_bytes: int = 0, timeout: int = 0): 175 | """Reads specified number of bytes from the stream and then returns. If numBytes is 0, read will timeout after 176 | the specified timeout. If timeout is 0, the default timeout is used. 177 | 178 | Args: 179 | num_bytes: 180 | timeout: 181 | 182 | Returns: 183 | 184 | """ 185 | self.lock.acquire() 186 | if not self._ser.is_open: 187 | self.lock.release() 188 | return None 189 | start_time = time.time() 190 | message = '' 191 | total_bytes_read = 0 192 | timeout = timeout if timeout != 0 else self.default_timeout # Set the timeout for this read 193 | while time.time() - start_time < timeout: 194 | bytes_waiting = self._ser.in_waiting 195 | if (bytes_waiting > (num_bytes - total_bytes_read)) & (num_bytes != 0): 196 | message += self._ser.read(num_bytes - total_bytes_read) 197 | self.lock.release() 198 | return message 199 | else: 200 | message += self._ser.read(bytes_waiting) 201 | total_bytes_read += bytes_waiting 202 | self.lock.release() 203 | return message 204 | 205 | def flush(self): 206 | """ 207 | Simply flushes the input buffer (the output of the device) so it isn't read during subsequent calls 208 | :return: None 209 | """ 210 | if self._ser.is_open: 211 | self._ser.flushInput() 212 | logger.debug('Flushed input buffer.') 213 | 214 | def is_online(self): 215 | return self._ser.is_open 216 | 217 | def is_verified(self): 218 | return self._is_verified 219 | 220 | def _open_check(self): 221 | if not self._ser.is_open: 222 | logger.warning('{0} is offline, cannot read.'.format(self.__class__.__name__)) 223 | return False 224 | return True 225 | 226 | def connect(self): 227 | if not self.is_online(): 228 | self.open() 229 | else: 230 | self.verify() 231 | if self.is_verified(): 232 | self.setup() 233 | return self 234 | -------------------------------------------------------------------------------- /phox/experiment/activephotonicsimager.py: -------------------------------------------------------------------------------- 1 | from ..instrumentation import ASI, NIDAQControl, XCamera, LaserHP8164A, LightwaveMultimeterHP8163A 2 | from typing import Tuple, Callable, Optional, List 3 | import numpy as np 4 | import time 5 | 6 | from skimage import measure 7 | from shapely.geometry import Polygon 8 | 9 | import logging 10 | logger = logging.getLogger() 11 | logger.setLevel(logging.WARN) 12 | 13 | 14 | class ActivePhotonicsImager: 15 | def __init__(self, home: Tuple[float, float] = (0, 0), 16 | stage_port: str = '/dev/ttyUSB0', laser_port: str = '/dev/ttyUSB1', lmm_port: str = '/dev/ttyUSB2', 17 | camera_calibration_filepath: Optional[str] = None, integration_time: int = 20000, 18 | plim: Tuple[float, float] = (0.05, 4.25), vmax: float = 6): 19 | """Active photonics imager, incorporating stage, camera livestream, and voltage control. 20 | 21 | Args: 22 | home: Home position for the stage 23 | stage_port: Stage serial port str 24 | laser_port: Laser serial port str 25 | lmm_port: Laser multimeter serial port str 26 | camera_calibration_filepath: Camera calibration file (for Xenics bobcat camera) 27 | integration_time: Integration time for the camera 28 | plim: Allowed laser power limits (min, max) 29 | vmax: Maximum allowed voltage 30 | """ 31 | self.home = home 32 | logger.info('Connecting to camera...') 33 | if 'camera' not in self.__dict__: 34 | self.camera = XCamera(integration_time=integration_time) 35 | self.integration_time = integration_time 36 | self.camera.start() 37 | if camera_calibration_filepath is not None: 38 | self.camera.load_calibration(camera_calibration_filepath) 39 | logger.info('Connecting to stage...') 40 | self.stage = ASI(port=stage_port) 41 | self.stage.connect() 42 | logger.info('Connecting to mesh voltage control...') 43 | self.control = NIDAQControl(0, vmax) 44 | logger.info('Connecting to laser...') 45 | self.laser = LaserHP8164A(port=laser_port) 46 | self.laser.connect() 47 | logger.info('Connecting to lightwave multimeter') 48 | if lmm_port is not None: 49 | self.lmm = LightwaveMultimeterHP8163A(port=lmm_port) 50 | self.lmm.connect() 51 | logger.info('Turning laser off...') 52 | self.laser.state = 0 53 | time.sleep(1) 54 | logger.info('Taking camera reference...') 55 | self.camera.background_reference = self.camera.frame() 56 | time.sleep(1) 57 | logger.info('Turning laser back on...') 58 | self.laser.state = 1 59 | self.plim = plim 60 | self.vmax = vmax 61 | 62 | def go_home(self): 63 | self.stage.move(*self.home) 64 | 65 | def sweep_voltage(self, voltages: np.ndarray, centers: List[Tuple[int, int]], channel: int, pbar: Callable, 66 | window_size: int, wait_time: float = 1, 67 | integration_time: Optional[int] = None) -> Tuple[np.ndarray, np.ndarray]: 68 | """Sweep a phase shifter voltage and monitor the output spots at specified centers for each voltage. 69 | 70 | Args: 71 | voltages: Voltages 72 | centers: Centers for which to extract grating spots (power and window) 73 | channel: Control channel to sweep 74 | pbar: Progress bar 75 | window_size: Window size 76 | wait_time: Wait time between voltage setting and image 77 | integration_time: Integration time for the sweep (default to current camera integration time) 78 | 79 | Returns: 80 | A tuple of powers and windows containing the spots 81 | 82 | """ 83 | spots = [] 84 | self.camera.set_integration_time(self.integration_time if integration_time is None else integration_time) 85 | iterator = pbar(voltages) if pbar is not None else voltages 86 | for v in iterator: 87 | self.control.write_chan(channel, v) 88 | time.sleep(wait_time) 89 | img = self.camera.frame() 90 | spots.append([_get_grating_spot(img, center, window_size) for center in centers]) 91 | return np.asarray([[s[c][0] for s in spots] for c in range(len(centers))]),\ 92 | np.asarray([[s[c][1] for s in spots] for c in range(len(centers))]) 93 | 94 | def centers(self, threshold: int = 0) -> List[Tuple[int, int]]: 95 | """Determine the input and output centers for the MZI (avoids needing to hard-code positions) 96 | 97 | Args: 98 | threshold: Threshold 99 | 100 | Returns: 101 | a list of center pixel locations 102 | 103 | """ 104 | self.camera.set_integration_time(self.integration_time) 105 | img = self.camera.frame() 106 | time.sleep(0.2) 107 | contours = [Polygon(np.fliplr(contour)) 108 | for contour in measure.find_contours(img, threshold) if len(contour) > 3] 109 | contours = [contour for contour in contours if contour.area > 2] 110 | contour_centers = [(int(contour.centroid.y), int(contour.centroid.x)) for contour in contours] 111 | return contour_centers 112 | 113 | def spot_saturation(self, center: Tuple[int, int], window_size: int = 10, 114 | plim: Tuple[float, float] = (0.05, 4.25), n_steps: int = 421, 115 | pbar: Optional[Callable] = None, wait_time: float = 1, 116 | init_wait_time: float = 2) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 117 | """Measure spot saturation in a given grating spot (should be done for each grating spot!) 118 | 119 | Args: 120 | center: (x, y) for the center of the calibration image 121 | window_size: window size in the calibration image to compute the power 122 | plim: power range (pmin, pmax) for the calibration 123 | n_steps: number of steps in the sweep (resolution) 124 | pbar: callable for progressbar (for longer calibrations) 125 | wait_time: integration time for the laser power calibration 126 | init_wait_time: initial wait time 127 | 128 | Returns: 129 | A tuple of laser powers and measured powers associated with the calibration 130 | 131 | """ 132 | laser_powers = np.linspace(*plim, n_steps) 133 | measured_powers = [] 134 | measured_windows = [] 135 | self.camera.set_integration_time(self.integration_time) 136 | iterator = pbar(laser_powers) if pbar is not None else laser_powers 137 | self.laser.power = plim[0] 138 | time.sleep(init_wait_time) 139 | for power in iterator: 140 | self.laser.power = power 141 | time.sleep(wait_time) 142 | img = self.camera.frame() 143 | power, window = _get_grating_spot(img, center, window_size) 144 | measured_powers.append(power) 145 | measured_windows.append(window) 146 | return laser_powers, np.asarray(measured_powers), np.stack(measured_windows) 147 | 148 | def dispersion(self, centers: List[Tuple[int, int]], window_size: int = 20, 149 | wlim: Tuple[float, float] = (1.53, 1.57), n_steps: int = 401, 150 | integration_time: int = 4000, pbar: Optional[Callable] = None, wait_time: float = 1, 151 | init_wait_time: float = 2) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: 152 | """Determine the dispersion relationship 153 | 154 | Args: 155 | centers: list of (x, y) for the center of the dispersion images 156 | window_size: window size for spots in the image to compute the power 157 | wlim: wavelength range (wmin, wmax) for the calibration 158 | n_steps: number of steps in the sweep (resolution) 159 | n_avg: number of times to average to determine the power 160 | integration_time: integration time for the laser power calibration 161 | pbar: callable for progressbar (for longer calibrations) 162 | 163 | Returns: 164 | A tuple of wavelengths, and measured powers associated with the calibration 165 | 166 | """ 167 | wavelengths = np.linspace(*wlim, n_steps) 168 | measured_powers = [] 169 | measured_windows = [] 170 | self.camera.set_integration_time(integration_time) 171 | iterator = pbar(wavelengths) if pbar is not None else wavelengths 172 | self.laser.wavelength = wlim[0] 173 | time.sleep(init_wait_time) 174 | for wavelength in iterator: 175 | self.laser.wavelength = wavelength 176 | time.sleep(wait_time) 177 | img = self.camera.frame() 178 | res = [_get_grating_spot(img, center, window_size) for center in centers] 179 | power, window = [r[0] for r in res], [r[1] for r in res] 180 | measured_powers.append(np.asarray(power)) 181 | measured_windows.append(np.asarray(window)) 182 | return wavelengths, np.asarray(measured_powers), np.asarray(measured_windows) 183 | 184 | def shutdown(self): 185 | """Stop the camera and close the stage. 186 | 187 | """ 188 | self.camera.stop() 189 | self.stage.close() 190 | 191 | 192 | 193 | def _get_grating_spot(img: np.ndarray, center: Tuple[int, int], window_dim: Tuple[int, int]) -> Tuple[np.ndarray, np.ndarray]: 194 | window = img[center[0] - window_dim[0] // 2:center[0] - window_dim[0] // 2 + window_dim[0], 195 | center[1] - window_dim[1] // 2:center[1] - window_dim[1] // 2 + window_dim[1]] 196 | power = np.sum(window) 197 | return power, window 198 | -------------------------------------------------------------------------------- /scripts/coherent_meas_utils.py: -------------------------------------------------------------------------------- 1 | from tqdm import tqdm 2 | import time 3 | import numpy as np 4 | import os 5 | import pickle 6 | import pdb 7 | 8 | """ 9 | class initialization: 10 | chip: AFM chip handle 11 | backward: whether it is backward or not 12 | 13 | usage: 14 | "reconstruct_field" function output a normalized 15 | reconstructed field with 4 dimensions, phase reference 16 | is the 5th output port 17 | 18 | Ground truth: 19 | if set input to v, matrix to u (with gamma) 20 | for forward pass, GT field is normalize[(u @ v) * np.exp(-1j * gamma)] 21 | for backward pass, GT field is normalize[u.T @ (v * np.exp(-1j * gamma))] 22 | """ 23 | 24 | 25 | class CoherentMeas: 26 | def __init__(self, chip, backward=False): 27 | self.chip = chip 28 | self.backward = backward 29 | self.out_mzi_idx_forward = {10: 4, 11: 4, 12: 2, 13: 3, 14: 2, 15: 2, 16: 1, 17: 1} 30 | self.out_mzi_idx_backward = {1: 1, 2: 1, 3: 2, 4: 2, 5: 3, 6: 3, 7: 4, 8: 4} 31 | 32 | def get_phi_down(self, powers): 33 | """ 34 | get phase measurement result for single phase sensor 35 | """ 36 | pu = np.arctan2((powers[1, 0] - powers[3, 0]), \ 37 | (powers[0, 0] - powers[2, 0])) - np.pi 38 | pd = np.arctan2((powers[1, 1] - powers[3, 1]), \ 39 | (powers[0, 1] - powers[2, 1])) - np.pi 40 | return pd 41 | 42 | def set_bar(self, c): 43 | """ 44 | set output MZI at column c to bar state 45 | """ 46 | if self.backward: 47 | row = self.out_mzi_idx_backward[c] 48 | else: 49 | row = self.out_mzi_idx_forward[c] 50 | self.chip.ps[(c, row)].phase = np.pi 51 | 52 | def get_input_cmeas(self, c, avg_num): 53 | """ 54 | measure input power at column c 55 | """ 56 | ## set all columns after c to bar state 57 | if self.backward: 58 | for cc in np.arange(1, c): 59 | self.set_bar(cc) 60 | else: 61 | for cc in np.arange(c, 18): 62 | self.set_bar(cc) 63 | time.sleep(0.05) 64 | 65 | ## measure power at end of mesh 66 | inp_power = np.zeros(6) 67 | for ii in range(avg_num): 68 | if self.backward: 69 | inp_power += self.chip.fractional_left 70 | else: 71 | inp_power += self.chip.fractional_right 72 | inp_power /= avg_num 73 | return inp_power 74 | 75 | def config_layer(self, col, avg_num=1): 76 | """ 77 | nullify layer (col, col+1). The MZI starts at left side of col, 78 | ends at left side of col+2 79 | 80 | avg_num: each power measurement can be repeated several times 81 | and averaged to reduce noise, not needed if signal is strong 82 | """ 83 | if self.backward: 84 | col1 = col 85 | col2 = col - 1 86 | row1 = self.out_mzi_idx_backward[col1] 87 | row2 = self.out_mzi_idx_backward[col2] 88 | else: 89 | col1 = col 90 | col2 = col + 1 91 | row1 = self.out_mzi_idx_forward[col1] 92 | row2 = self.out_mzi_idx_forward[col2] 93 | 94 | ## measure input power at column col 95 | inp_power = self.get_input_cmeas(col, avg_num) 96 | 97 | """ 98 | set theta = pi/2, set phi to 0, pi/2, 99 | pi, 3pi/2 to measure phase 100 | """ 101 | self.chip.ps[(col2, row2)].phase = np.pi / 2 102 | time.sleep(0.05) 103 | 104 | powers = [] 105 | for jj in range(4): 106 | self.chip.ps[(col1, row1)].phase = np.pi / 2 * jj 107 | time.sleep(0.05) 108 | 109 | pp = np.zeros(6) 110 | for ii in range(avg_num): 111 | if self.backward: 112 | pp += self.chip.fractional_left 113 | else: 114 | pp += self.chip.fractional_right 115 | pp /= avg_num 116 | powers.append(pp) 117 | powers = np.asarray(powers) 118 | 119 | ## compute phase and set theta, phi to nullify 120 | phi = self.get_phi_down(powers[:, row2 - 1:row2 + 1]) 121 | self.chip.ps[(col1, row1)].phase = phi + np.pi 122 | time.sleep(0.05) 123 | theta = 2 * np.arctan2(inp_power[row2], inp_power[row2 - 1]) 124 | self.chip.ps[(col2, row2)].phase = theta + np.pi 125 | time.sleep(0.05) 126 | 127 | ## measure output power, can be used as sanity check 128 | out_power = np.zeros(6) 129 | for ii in range(avg_num): 130 | if self.backward: 131 | out_power += self.chip.fractional_left 132 | else: 133 | out_power += self.chip.fractional_right 134 | out_power /= avg_num 135 | return phi, theta, out_power, powers 136 | 137 | def back_prop_layer(self, inp_field, phase_save, col): 138 | """ 139 | after self-config, back propagate through column col 140 | to reconstruct the input field 141 | """ 142 | if self.backward: 143 | col1 = col 144 | col2 = col - 1 145 | row1 = self.out_mzi_idx_backward[col1] 146 | row2 = self.out_mzi_idx_backward[col2] 147 | phi = phase_save[(col1, row1)] 148 | theta = phase_save[(col2, row2)] 149 | else: 150 | col1 = col 151 | col2 = col + 1 152 | row1 = self.out_mzi_idx_forward[col1] 153 | row2 = self.out_mzi_idx_forward[col2] 154 | phi = phase_save[(col1, row1)] 155 | theta = phase_save[(col2, row2)] 156 | 157 | if col == 12: 158 | H1 = np.asarray([[np.exp(-1j * phi), 0], [0, 1]]) 159 | else: 160 | H1 = np.asarray([[1, 0], [0, np.exp(-1j * phi)]]) 161 | H2 = np.asarray([[1, 0], [0, np.exp(-1j * theta)]]) 162 | B = np.asarray([[1, -1j], [-1j, 1]]) / np.sqrt(2) 163 | M = H1 @ B @ H2 @ B 164 | out_field = M @ np.asarray([inp_field, 0]).astype(np.complex64) 165 | return out_field 166 | 167 | def meas_inp_power(self, avg_num): 168 | ## set all output MZI to bar state 169 | if self.backward: 170 | for cc in np.arange(1, 9): 171 | self.set_bar(cc) 172 | else: 173 | for cc in np.arange(10, 18): 174 | self.set_bar(cc) 175 | time.sleep(0.05) 176 | 177 | ## measure field power (matrix output) 178 | meas_vec = np.zeros(6) 179 | for nn in range(avg_num): 180 | if self.backward: 181 | meas_vec += self.chip.fractional_left 182 | else: 183 | meas_vec += self.chip.fractional_right 184 | meas_vec = meas_vec / avg_num 185 | return meas_vec 186 | 187 | def reconstruct_field(self, log_data=False, log_dir=None, avg_num=1): 188 | """ 189 | log_data: whether log data 190 | log_dir: to save all intermediate results and measurement results 191 | 192 | return: normalized field reconstruction, 4 dimensions 193 | """ 194 | if self.backward: 195 | self.chip.to_layer(0) 196 | time.sleep(1.0) 197 | else: 198 | self.chip.to_layer(16) 199 | time.sleep(1.0) 200 | 201 | ## measure input power 202 | inp_power_all = self.meas_inp_power(avg_num) 203 | 204 | ## self-config to measure phase 205 | if self.backward: 206 | phi, theta, out_power, meas_power = self.config_layer(8, avg_num) 207 | phi, theta, out_power, meas_power = self.config_layer(6, avg_num) 208 | phi, theta, out_power, meas_power = self.config_layer(4, avg_num) 209 | phi, theta, out_power, meas_power = self.config_layer(2, avg_num) 210 | else: 211 | phi, theta, out_power, meas_power = self.config_layer(10, avg_num) 212 | phi, theta, out_power, meas_power = self.config_layer(12, avg_num) 213 | phi, theta, out_power, meas_power = self.config_layer(14, avg_num) 214 | phi, theta, out_power, meas_power = self.config_layer(16, avg_num) 215 | 216 | phase_save = {} 217 | if self.backward: 218 | for cc in range(1, 9): 219 | kk = (cc, self.out_mzi_idx_backward[cc]) 220 | phase_save[kk] = self.chip.ps[kk].phase 221 | else: 222 | for cc in range(10, 18): 223 | kk = (cc, self.out_mzi_idx_forward[cc]) 224 | phase_save[kk] = self.chip.ps[kk].phase 225 | 226 | # pdb.set_trace() 227 | inp_field = 1 228 | rec_field = [] 229 | if self.backward: 230 | for col in [2, 4, 6, 8]: 231 | out_field = self.back_prop_layer(inp_field, phase_save, col) 232 | rec_field.append(out_field[0]) 233 | inp_field = out_field[1] 234 | rec_field.append(out_field[1]) 235 | else: 236 | for col in [16, 14, 12, 10]: 237 | out_field = self.back_prop_layer(inp_field, phase_save, col) 238 | rec_field.append(out_field[0]) 239 | inp_field = out_field[1] 240 | rec_field.append(out_field[1]) 241 | 242 | if log_data and (log_dir is not None): 243 | np.save(os.path.join(log_dir, 'inp_power.npy'), inp_power_all) 244 | with open(os.path.join(log_dir, 'mesh_phase.pickle'), 'wb') as f: 245 | pickle.dump(phase_save, f) 246 | np.save(os.path.join(log_dir, 'out_power.npy'), out_power) 247 | np.save(os.path.join(log_dir, 'rec_field.npy'), rec_field) 248 | 249 | inp_phase = np.angle(rec_field) - np.angle(rec_field)[-1] + np.pi 250 | inp_phase = np.mod(inp_phase[:4], 2 * np.pi) 251 | inp_mag = np.sqrt(np.abs(inp_power_all[:4])) 252 | rec_field_norm = inp_mag * np.exp(1j * inp_phase) 253 | rec_field_norm = rec_field_norm / np.linalg.norm(rec_field_norm) 254 | return rec_field_norm 255 | -------------------------------------------------------------------------------- /phox/instrumentation/stage.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Joe Landry' 2 | 3 | from typing import Tuple 4 | 5 | from .serial import SerialMixin 6 | import logging as logger 7 | import abc 8 | import numpy as np 9 | import time 10 | import panel as pn 11 | 12 | ERROR_CODES = { 13 | -1: 'Unknown Command', 14 | -2: 'Unrecognized Axis Parameter', 15 | -3: 'Missing Parameters', 16 | -4: 'Parameter Out of Range', 17 | -5: 'Operation Failed', 18 | -6: 'Undefined Error D:', 19 | -21: 'Serial Command Halted by HALT', 20 | } 21 | 22 | fpr = r'[-+]?(?:[0-9]*[.])?[0-9]+' 23 | 24 | 25 | class Axis(object): 26 | X = 0 27 | Y = 1 28 | 29 | 30 | class Stage(object): 31 | 32 | @abc.abstractmethod 33 | def status(self, axis): raise NotImplementedError 34 | 35 | @abc.abstractmethod 36 | def move(self, **kwargs): raise NotImplementedError 37 | 38 | @abc.abstractmethod 39 | def move_rel(self, **kwargs): raise NotImplementedError 40 | 41 | 42 | class ASI(SerialMixin, Stage): 43 | MAX_SPEED = 7.5 # [mm/s] 44 | 45 | """ 46 | Object responsible for managing ASI Imaging stage. 47 | """ 48 | 49 | def __init__(self, port='/dev/ttyUSB2', x_limits=(-10, 10), y_limits=(-20, 20)): 50 | SerialMixin.__init__(self, port) 51 | self.stage_config = { 52 | 'X Limits': x_limits, 53 | 'Y Limits': y_limits, 54 | 'Zero Button': 0, 55 | 'Fast Axis': Axis.X 56 | } 57 | self.x_limits = x_limits 58 | self.y_limits = y_limits 59 | self._is_on = False 60 | 61 | def status(self, axis): 62 | return Command(send_expr=f'I {axis}', read_expr='Maintain code', timeout=1.0).execute(self) 63 | 64 | def move(self, x=None, y=None): 65 | """ 66 | Move to a specific stage location 67 | :param x: x position [mm] 68 | :param y: y position [mm] 69 | :return: 70 | """ 71 | args = ['' if x is None else f'X={self.mm_to_encoder(x)}', 72 | '' if y is None else f'Y={self.mm_to_encoder(y)}'] 73 | Command(send_expr='MOVE {0} {1}'.format(*args), read_expr=':A', timeout=1).execute(self) 74 | 75 | def move_rel(self, x=None, y=None): 76 | 77 | args = ['' if x is None else f'X={self.mm_to_encoder(x)}', 78 | '' if y is None else f'Y={self.mm_to_encoder(y)}'] 79 | Command(send_expr='R {0} {1}'.format(*args), read_expr=':A', timeout=1).execute(self) 80 | 81 | def reset(self): 82 | Command(send_expr='~', read_expr='RESET:', timeout=5.0).execute(self) 83 | 84 | def where(self): 85 | read_expr = f':A\s({fpr})\s({fpr})\s\\r\\n' 86 | ret = Command(send_expr='WHERE X Y', read_expr=read_expr, group=(1, 2)).execute(self) 87 | return tuple(self.encoder_to_mm(float(_)) for _ in ret) 88 | 89 | def halt(self): 90 | Command(send_expr='HALT', read_expr=r'(:A|:N-21)').execute(self) 91 | 92 | def speed(self): 93 | read_expr = f':A\sX=({fpr})\s+Y=({fpr})\s\\r\\n' 94 | ret = Command(send_expr='S X? Y?', read_expr=read_expr, group=(1, 2)).execute(self) 95 | return tuple((float(_)) for _ in ret) 96 | 97 | def set_speed(self, x, y): 98 | """ 99 | Set stage speed 100 | :param x: x-axis speed [mm/s] 101 | :param y: y-axis speed [mm/s] 102 | :return: 103 | """ 104 | args = ['' if x is None else 'X={0}'.format(np.clip(x, 0.0, self.MAX_SPEED)), 105 | '' if y is None else 'Y={0}'.format(np.clip(y, 0.0, self.MAX_SPEED))] 106 | Command(send_expr='S {0} {1}'.format(*args), read_expr=':A').execute(self) 107 | 108 | def who(self): 109 | return Command(send_expr='WHO', read_expr=r':A\s(\S+)\s+\r\n', group=1).execute(self) 110 | 111 | def info(self, y: bool = False): 112 | return Command(send_expr='INFO Y' if y else 'INFO X', read_expr=f':A\s{fpr}\\r\\n', group=1).execute(self) 113 | 114 | def version(self): 115 | return Command(send_expr='VERSION', read_expr=r':A Version:\s(\S+)\s', group=1).execute(self) 116 | 117 | def is_moving(self): 118 | return Command(send_expr='/', read_expr=r'(\w)\r\n', group=1).execute(self) == 'B' 119 | 120 | def wait_until_stopped(self, interval: float = 0.1): 121 | while True: 122 | time.sleep(interval) 123 | if not self.is_moving(): 124 | break 125 | 126 | def set_home(self): 127 | raise NotImplementedError('Needs to be defined') 128 | 129 | def home(self): 130 | read_expr = f':A\sX=({fpr})\s+Y=({fpr})\s\\r\\n' 131 | ret = Command(send_expr='HM X? Y?', read_expr=read_expr, group=(1, 2)).execute(self) 132 | return tuple((float(_)) for _ in ret) 133 | 134 | def zero(self): 135 | Command(send_expr='Z', read_expr=':A').execute(self) 136 | 137 | def kp(self, val: float, y: bool = True): 138 | return Command(send_expr=f'KP Y={val}' if y else f'KP X={val}', read_expr=':A').execute(self) 139 | 140 | def set_limits(self, x_lim=None, y_lim=None): 141 | low_args, high_args = [], [] 142 | 143 | def format_lim_args(lim, axis): 144 | if (len(lim) != 2) | (lim[0] > lim[1]): 145 | raise ValueError(f'{axis} Limit not formatted properly') 146 | low_args.append(f'{axis}={lim[0]}') 147 | high_args.append(f'{axis}={lim[1]}') 148 | 149 | if x_lim is not None: 150 | format_lim_args(x_lim, 'X') 151 | if y_lim is not None: 152 | format_lim_args(y_lim, 'Y') 153 | 154 | Command(send_expr='SL {0} {1}'.format(*low_args), read_expr=':A').execute(self) 155 | Command(send_expr='SU {0} {1}'.format(*high_args), read_expr=':A').execute(self) 156 | 157 | def setup(self): 158 | self.set_button_enable(zero=self.stage_config['Zero Button']) 159 | self.set_limits(x_lim=self.stage_config['X Limits'], y_lim=self.stage_config['Y Limits']) 160 | 161 | # --- Scanning --- 162 | def setup_scan(self, x_lim, y_lim, num_lines=1, serpentine=False): 163 | # X, Y, Z = 0 define unused axes. F=0 means raster, 1 means serpentine 164 | Command(send_expr='SN X=1 Y=2 Z=0 F={0}'.format(int(serpentine)), read_expr=':A').execute( 165 | self) # TODO: Change fast axis using config file 166 | Command(send_expr='NR X={0} Y={1}'.format(x_lim[0], x_lim[1])).execute(self) 167 | Command(send_expr='NV X={0} Y={1} Z={2} F=1.0'.format(y_lim[0], y_lim[1], num_lines)).execute(self) 168 | Command(send_expr='TTL X=1', read_expr=':A').execute(self) # Unsure if necessary 169 | 170 | def start_scan(self): 171 | Command(send_expr='SN', read_expr=':A').execute(self) 172 | 173 | def close(self): 174 | """ 175 | Turn instrument off when shutting down 176 | :return: None 177 | """ 178 | pass 179 | 180 | zero_button = 0 181 | home_button = 1 182 | at_button = 2 183 | joystick_button = 3 184 | 185 | def set_button_enable(self, zero=1, home=1, at=1, joystick=1): 186 | # Bit 0: "Zero" Button 187 | # Bit 1: "Home" Button 188 | # Bit 2: "@" Button 189 | # Bit 3: Joystick Button 190 | Command(send_expr=f'BE Z=1111{zero}{home}{at}{joystick}', read_expr=':A').execute(self) 191 | 192 | def mm_to_encoder(self, mm): 193 | return round(mm * 1e4) 194 | 195 | def encoder_to_mm(self, enc): 196 | return enc * 1e-4 197 | 198 | def aa_query(self): 199 | read_expr = f':A\sX=({fpr})\s+Y=({fpr})\s\\r\\n' 200 | return Command(send_expr=f'AA X? Y? Z?', read_expr=read_expr).execute(self) 201 | 202 | def aa_set(self, val: int = 85, y: bool = False): 203 | return Command(send_expr=f'AA Y={val}' if y else f'AA X={val}', read_expr=':A').execute(self) 204 | 205 | def aa(self, y: bool = False): 206 | return Command(send_expr=f'AA Y' if y else f'AA X', read_expr=':A').execute(self) 207 | 208 | def az(self, y: bool = False): 209 | return Command(send_expr=f'AZ Y' if y else f'AZ X', read_expr=':A').execute(self) 210 | 211 | def move_panel(self, xlim: Tuple[float, float] = (-1, 1), ylim: Tuple[float, float] = (-6.18, 0.2), 212 | dx: float = 0.001, dy: float = 0.001) -> pn.Pane: 213 | """ 214 | 215 | Args: 216 | xlim: limits to the :math:`x`-position in mm 217 | ylim: limits to the :math:`y`-position in mm 218 | dx: increments for the :math:`x`-slider 219 | dy: increments for the :math:`y`-slider 220 | 221 | Returns: 222 | Move panel consisting of :math:`x`-slider and :math:`y`-slider and a Sync stage position button. 223 | 224 | """ 225 | init_x, init_y = self.where() 226 | 227 | x = pn.widgets.FloatSlider(start=xlim[0], end=xlim[1], step=dx, 228 | value=init_x, name='X Position', format='1[.]000') 229 | y = pn.widgets.FloatSlider(start=ylim[0], end=ylim[1], step=dy, 230 | value=init_y, name='Y Position', format='1[.]000') 231 | sync = pn.widgets.Button(name='Sync Stage Position') 232 | 233 | def move_x(*events): 234 | for event in events: 235 | if event.name == 'value': 236 | self.move(x=event.new) 237 | 238 | def move_y(*events): 239 | for event in events: 240 | if event.name == 'value': 241 | self.move(y=event.new) 242 | 243 | def sync_(*events): 244 | new_x, new_y = self.where() 245 | x.value = new_x 246 | y.value = new_y 247 | 248 | x.param.watch(move_x, 'value') 249 | y.param.watch(move_y, 'value') 250 | sync.on_click(sync_) 251 | 252 | return pn.Column(x, y, sync) 253 | 254 | 255 | class Errors(object): 256 | PI_NO_ERROR = 0 257 | PI_DEVICE_NOT_FOUND = 1 258 | PI_OBJECT_NOT_FOUND = 2 259 | PI_CANNOT_CREATE_OBJECT = 3 260 | PI_INVALID_DEVICE_HANDLE = 4 261 | PI_READ_TIMEOUT = 5 262 | PI_READ_THREAD_ABANDONED = 6 263 | PI_READ_FAILED = 7 264 | PI_INVALID_PARAMETER = 8 265 | PI_WRITE_FAILED = 9 266 | 267 | 268 | class Command(object): 269 | 270 | def __init__(self, send_expr, read_expr=':A', group=None, timeout=0.0): 271 | self.send_expr = send_expr 272 | self.read_expr = read_expr 273 | self.group = group 274 | self.timeout = timeout 275 | self.last_sent = '' 276 | 277 | def execute(self, ser, **kwargs): 278 | self.write(ser, **kwargs) 279 | return self.verify(ser) 280 | 281 | def write(self, ser, **kwargs): 282 | formatted = self.send_expr.format(**kwargs) # Format the message to send 283 | ser.write(formatted) 284 | self.last_sent = formatted 285 | 286 | def verify(self, ser): 287 | msg, is_match = ser.read_until(expr=self.read_expr, group_num=self.group, timeout=self.timeout) 288 | if not is_match: 289 | logger.warning(f'Command {self.last_sent} not validated. The following message was retrieved: {msg}') 290 | return msg 291 | -------------------------------------------------------------------------------- /phox/instrumentation/camera.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | 3 | # add types 4 | from collections import Callable 5 | 6 | import numpy as np 7 | import holoviews as hv 8 | from holoviews import opts 9 | from holoviews.streams import Pipe 10 | from numpy.ctypeslib import ndpointer 11 | from threading import Thread, Lock 12 | import logging 13 | 14 | from tornado.ioloop import PeriodicCallback 15 | 16 | logger = logging.getLogger() 17 | import time 18 | import panel as pn 19 | from tornado import gen 20 | 21 | from typing import Optional, List, Tuple 22 | 23 | lusb = ctypes.CDLL('/usr/local/lib/libusb-1.0.so', mode=ctypes.RTLD_GLOBAL) 24 | xen = ctypes.CDLL('libxeneth.so') 25 | 26 | # C Enumerations 27 | 28 | # Used for conversion to string 29 | errcodes = {0: 'I_OK', 30 | 1: 'I_DIRTY', 31 | 10000: 'E_BUG', 32 | 10001: 'E_NOINIT', 33 | 10002: 'E_LOGICLOADFAILED', 34 | 10003: 'E_INTERFACE_ERROR', 35 | 10004: 'E_OUT_OF_RANGE', 36 | 10005: 'E_NOT_SUPPORTED', 37 | 10006: 'E_NOT_FOUND', 38 | 10007: 'E_FILTER_DONE', 39 | 10008: 'E_NO_FRAME', 40 | 10009: 'E_SAVE_ERROR', 41 | 10010: 'E_MISMATCHED', 42 | 10011: 'E_BUSY', 43 | 10012: 'E_INVALID_HANDLE', 44 | 10013: 'E_TIMEOUT', 45 | 10014: 'E_FRAMEGRABBER', 46 | 10015: 'E_NO_CONVERSION', 47 | 10016: 'E_FILTER_SKIP_FRAME', 48 | 10017: 'E_WRONG_VERSION', 49 | 10018: 'E_PACKET_ERROR', 50 | 10019: 'E_WRONG_FORMAT', 51 | 10020: 'E_WRONG_SIZE', 52 | 10021: 'E_CAPSTOP', 53 | 10022: 'E_OUT_OF_MEMORY', 54 | 10023: 'E_RFU'} # The last one is uncertain 55 | 56 | # C functions 57 | 58 | # XCHANDLE XC_OpenCamera (const char * pCameraName = "cam://default", 59 | # XStatus pCallBack = 0, void * pUser = 0); 60 | open_camera = xen.XC_OpenCamera 61 | open_camera.restype = ctypes.c_uint # XCHANDLE 62 | # open_camera.argtypes = (ctypes.c_char_p,) 63 | open_camera.argtypes = (ctypes.c_char_p, ctypes.c_void_p, ctypes.c_void_p) 64 | 65 | error_to_string = xen.XC_ErrorToString 66 | error_to_string.restype = ctypes.c_int32 67 | error_to_string.argtypes = (ctypes.c_int32, ctypes.c_char_p, ctypes.c_int32) 68 | 69 | is_initialised = xen.XC_IsInitialised 70 | is_initialised.restype = ctypes.c_int32 71 | is_initialised.argtypes = (ctypes.c_int32,) 72 | 73 | start_capture = xen.XC_StartCapture 74 | start_capture.restype = ctypes.c_ulong # ErrCode 75 | start_capture.argtypes = (ctypes.c_int32,) 76 | 77 | is_capturing = xen.XC_IsCapturing 78 | is_capturing.restype = ctypes.c_bool 79 | is_capturing.argtypes = (ctypes.c_int32,) 80 | 81 | get_frame_size = xen.XC_GetFrameSize 82 | get_frame_size.restype = ctypes.c_ulong 83 | get_frame_size.argtypes = (ctypes.c_int32,) # Handle 84 | 85 | get_frame_type = xen.XC_GetFrameType 86 | get_frame_type.restype = ctypes.c_ulong # Returns enum 87 | get_frame_type.argtypes = (ctypes.c_int32,) # Handle 88 | 89 | get_frame_width = xen.XC_GetWidth 90 | get_frame_width.restype = ctypes.c_ulong 91 | get_frame_width.argtypes = (ctypes.c_int32,) # Handle 92 | 93 | get_frame_height = xen.XC_GetHeight 94 | get_frame_height.restype = ctypes.c_ulong 95 | get_frame_height.argtypes = (ctypes.c_int32,) # Handle 96 | 97 | get_frame = xen.XC_GetFrame 98 | get_frame.restype = ctypes.c_ulong # ErrCode 99 | get_frame.argtypes = (ctypes.c_int32, ctypes.c_ulong, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_uint) 100 | 101 | stop_capture = xen.XC_StopCapture 102 | stop_capture.restype = ctypes.c_ulong # ErrCode 103 | stop_capture.argtypes = (ctypes.c_int32,) 104 | 105 | close_camera = xen.XC_CloseCamera 106 | # Returns void 107 | close_camera.argtypes = (ctypes.c_int32,) # Handle 108 | 109 | # Calibration 110 | load_calibration = xen.XC_LoadCalibration 111 | load_calibration.restype = ctypes.c_ulong # ErrCode 112 | # load_calibration.argtypes = (ctypes.c_int32, ctypes.c_char_p, ctypes.c_ulong) 113 | 114 | # ColourProfile 115 | load_colour_profile = xen.XC_LoadColourProfile 116 | load_colour_profile.restype = ctypes.c_ulong 117 | load_colour_profile.argtypes = (ctypes.c_char_p,) 118 | 119 | # Settings 120 | load_settings = xen.XC_LoadSettings 121 | load_settings.restype = ctypes.c_ulong 122 | load_settings.argtypes = (ctypes.c_char_p, ctypes.c_ulong) 123 | 124 | # FileAccessCorrectionFile 125 | set_property_value = xen.XC_SetPropertyValue 126 | set_property_value.restype = ctypes.c_ulong # ErrCode 127 | # set_property_value.argtypes = (ctypes.c_int32, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p) 128 | 129 | # Set property value 130 | set_property_value_f = xen.XC_SetPropertyValueF 131 | set_property_value_f.restype = ctypes.c_ulong # ErrCode 132 | # set_property_value.argtypes = (ctypes.c_int32, ctypes.c_char_p, ctypes.c_double, ctypes.c_char_p) 133 | 134 | 135 | class XCamera: 136 | def __init__(self, name: str = 'cam://0', integration_time: int = 1000, 137 | spots: Optional[List[Tuple[int, int, int, int]]] = None, flip_spots: bool = False): 138 | """Xenics Camera (XCamera) 139 | 140 | Args: 141 | name: camera URL 142 | integration_time: Default integration time for the camera 143 | spots: list of spots to track during frame loop 144 | """ 145 | self.handle = open_camera(name.encode('UTF-8'), 0, 0) 146 | self.shape = (get_frame_height(self.handle), get_frame_width(self.handle)) 147 | self._current_frame = None 148 | self.calibrate = False 149 | self.thread = None 150 | self.started_frame_loop = False 151 | self.started = False 152 | self.frame_lock = Lock() 153 | self.background_reference = None 154 | self.set_integration_time(integration_time) 155 | self.integration_time = integration_time * 1e-6 156 | self.livestream_pipe = Pipe(data=[]) 157 | self.spots_indicator_pipe = Pipe(data=spots) 158 | self.spots = [] if spots is None else spots 159 | self.spot_powers = [] 160 | self.flip_spots = flip_spots 161 | self.livestream_on = False 162 | self.power_det_on = False 163 | self.num_frames = 0 164 | 165 | def start(self) -> int: 166 | self.started = True 167 | return errcodes[start_capture(self.handle)] 168 | 169 | def stop(self) -> int: 170 | self.started = False 171 | return errcodes[stop_capture(self.handle)] 172 | 173 | def start_frame_loop(self, on_frame: Optional[Callable] = None): 174 | if self.started_frame_loop: 175 | logger.warning('Cannot start a frame loop that has already been started. ' 176 | 'Use end_frame_loop to stop current frame loop.') 177 | return 178 | self.started_frame_loop = True 179 | self.thread = Thread(target=self._frame_loop, args=(on_frame,)) 180 | self.thread.start() 181 | 182 | def _frame_loop(self, on_frame: Optional[Callable] = None): 183 | while self.started_frame_loop: 184 | frame = self._frame() 185 | if self.spots is not None: 186 | self.spot_powers = np.asarray([_get_grating_spot(frame, (s[0], s[1]), (s[2], s[3]))[0] 187 | for s in self.spots]) 188 | if self.flip_spots: 189 | self.spot_powers = self.spot_powers[::-1] 190 | if on_frame is not None: 191 | on_frame(frame) 192 | with self.frame_lock: 193 | self._current_frame = frame.copy() 194 | self.num_frames += 1 195 | 196 | def _livestream_loop(self, on_frame: Optional[Callable] = None): 197 | while self.started_frame_loop: 198 | frame = self._frame() 199 | if self.spots is not None: 200 | self.spot_powers = np.asarray([_get_grating_spot(frame, (s[0], s[1]), (s[2], s[3]))[0] 201 | for s in self.spots]) 202 | if on_frame is not None: 203 | on_frame(frame) 204 | with self.frame_lock: 205 | self._current_frame = frame.copy() 206 | 207 | def stop_frame_loop(self): 208 | self.started_frame_loop = False 209 | self.thread.join() 210 | 211 | def frame(self, wait_time: float = 0) -> np.ndarray: 212 | if self.started_frame_loop: 213 | time.sleep(wait_time) 214 | with self.frame_lock: 215 | frame = self._current_frame.copy() 216 | return frame 217 | else: 218 | return self._frame() 219 | 220 | def _frame(self) -> np.ndarray: 221 | if not self.started: 222 | raise RuntimeError('Camera must be started to capture a frame.') 223 | frame = np.zeros(shape=self.shape, dtype=np.uint16) 224 | error = get_frame(self.handle, get_frame_type(self.handle), 1, 225 | frame.ctypes.data_as(ndpointer(np.uint16)), frame.nbytes) 226 | if error != 0: 227 | raise RuntimeError(f'Camera Error: {errcodes[error]}') 228 | 229 | return frame if self.background_reference is None else frame.astype(np.float) - self.background_reference.astype(np.float) 230 | 231 | def set_integration_time(self, integration_time: int): 232 | self.integration_time = integration_time 233 | return errcodes[set_property_value(self.handle, 'IntegrationTime'.encode('UTF-8'), 234 | str(integration_time).encode('UTF-8'), 0)] 235 | 236 | def load_calibration(self, filepath: str): 237 | self.calibrate = True 238 | return errcodes[load_calibration(self.handle, filepath.encode('UTF-8'), 1)] 239 | 240 | def __exit__(self): 241 | if self.started_frame_loop: 242 | self.stop_frame_loop() 243 | self.stop() 244 | 245 | def livestream_panel(self, cmap='hot'): 246 | """ 247 | 248 | Args: 249 | cmap: colormap for the livestream 250 | 251 | Returns: 252 | A video livestream for the camera 253 | 254 | """ 255 | 256 | # livestream 257 | bounded_img = lambda data: hv.Image(data, bounds=(0, 0, 640, 512)) 258 | dmap = hv.DynamicMap(bounded_img, streams=[self.livestream_pipe]).opts( 259 | width=640, height=512, show_grid=True, colorbar=True, cmap=cmap, 260 | shared_axes=False).redim.range(z=(0, 2 ** 15)) 261 | livestream_toggle = pn.widgets.Toggle(name='Livestream', value=False) 262 | capture_button = pn.widgets.Button(name='Capture') 263 | self.livestream_pipe.send(np.fliplr(self.frame().astype(np.float))) 264 | 265 | @gen.coroutine 266 | def update_img(): 267 | self.livestream_pipe.send(np.fliplr(self.frame().astype(np.float))) 268 | 269 | cb = PeriodicCallback(update_img, 100) 270 | 271 | def toggle_livestream(*events): 272 | for event in events: 273 | if event.name == 'value': 274 | self.livestream_on = bool(event.new) 275 | if self.livestream_on: 276 | cb.start() 277 | else: 278 | cb.stop() 279 | 280 | def capture_frame(*events): 281 | for event in events: 282 | if event.name == 'value': 283 | self.livestream_pipe.send(np.fliplr(self.frame().astype(np.float))) 284 | 285 | livestream_toggle.param.watch(toggle_livestream, 'value') 286 | capture_button.param.watch(capture_frame, 'value') 287 | 288 | spot_indicator_fn = lambda data: hv.Polygons([{('x', 'y'): hv.Box(640 - s[1], 512 - s[0], (s[3], s[2])).array()} 289 | for s in data]).opts(line_color='blue', fill_color=None, line_width=2) 290 | 291 | spot_indicator_fn = lambda data: hv.Polygons([{('x', 'y'): hv.Box(640 - s[1], 512 - s[0], (s[3], s[2])).array()} 292 | for s in data]).opts(line_color='blue', fill_color=None, line_width=2) 293 | 294 | spots_dmap = hv.DynamicMap(spot_indicator_fn, streams=[self.spots_indicator_pipe]) 295 | 296 | return pn.Column((dmap * spots_dmap).opts(shared_axes=False), 297 | pn.Row(livestream_toggle, capture_button)) 298 | 299 | def _get_grating_spot(img: np.ndarray, center: Tuple[int, int], window_dim: Tuple[int, int]) -> Tuple[np.ndarray, np.ndarray]: 300 | window = img[center[0] - window_dim[0] // 2:center[0] - window_dim[0] // 2 + window_dim[0], 301 | center[1] - window_dim[1] // 2:center[1] - window_dim[1] // 2 + window_dim[1]] 302 | power = np.sum(window) 303 | return power, window 304 | -------------------------------------------------------------------------------- /phox/apps/viz.py: -------------------------------------------------------------------------------- 1 | import matplotlib as mpl 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from collections import namedtuple 5 | 6 | # comment out the below two lines if you have trouble getting the plots to work 7 | 8 | import warnings 9 | 10 | warnings.filterwarnings('ignore') 11 | 12 | from sklearn.datasets import make_circles, make_moons, make_blobs, make_gaussian_quantiles 13 | from sklearn.model_selection import train_test_split 14 | from tensorflow.keras.utils import to_categorical 15 | from matplotlib.patches import Polygon 16 | from matplotlib.collections import PatchCollection 17 | from dphox.demo import mesh 18 | from dphox import Pattern 19 | from scipy.special import softmax 20 | from simphox.circuit import triangular 21 | from ..typing import Optional 22 | 23 | path_array, ps_array = mesh.demo_polys() 24 | 25 | Dataset = namedtuple('Dataset', ['X', 'y']) 26 | 27 | 28 | def add_bias(x, p=9, n=4): 29 | abs_sq_term = np.sum(x ** 2, axis=1)[:, np.newaxis] 30 | abs_sq_term[abs_sq_term > p] = p # avoid nan in case it exists 31 | normalized_bias_term = np.sqrt(p - abs_sq_term) 32 | return np.hstack([x, normalized_bias_term / np.sqrt(n - 2) * np.ones((x.shape[0], n - 2), dtype=np.complex64)]) 33 | 34 | 35 | def get_planar_dataset_with_circular_bias(dataset_name, test_size=.2): 36 | if dataset_name == 'moons': 37 | X, y = make_moons(noise=0.2, random_state=0, n_samples=500) 38 | elif dataset_name == 'circle': 39 | X, y = make_circles(noise=0.2, factor=0.5, random_state=1, n_samples=250) 40 | elif dataset_name == 'blobs': 41 | X, y = make_blobs(random_state=5, n_features=2, 42 | centers=[(-0.4, -0.4), (0.25, 0.3)], 43 | cluster_std=0.5, n_samples=250) 44 | elif dataset_name == 'ring': 45 | X, y = make_gaussian_quantiles(n_features=2, n_classes=3, n_samples=500, cov=0.4) 46 | y[y == 2] = 0 47 | y = to_categorical(y) 48 | 49 | X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=42) 50 | 51 | X_train_f = add_bias(X_train).astype(np.complex64) 52 | X_test_f = add_bias(X_test).astype(np.complex64) 53 | y_train_f = y_train.astype(np.complex64) 54 | y_test_f = y_test.astype(np.complex64) 55 | 56 | return Dataset(X, y), Dataset(X_train_f, y_train_f), Dataset(X_test_f, y_test_f) 57 | 58 | 59 | def cmap_map(function, cmap): 60 | """ Applies function (which should operate on vectors of shape 3: [r, g, b]), on colormap cmap. 61 | This routine will break any discontinuous points in a colormap. 62 | """ 63 | cdict = cmap._segmentdata 64 | step_dict = {} 65 | # Firt get the list of points where the segments start or end 66 | for key in ('red', 'green', 'blue'): 67 | step_dict[key] = list(map(lambda x: x[0], cdict[key])) 68 | step_list = sum(step_dict.values(), []) 69 | step_list = np.array(list(set(step_list))) 70 | # Then compute the LUT, and apply the function to the LUT 71 | reduced_cmap = lambda step: np.array(cmap(step)[0:3]) 72 | old_LUT = np.array(list(map(reduced_cmap, step_list))) 73 | new_LUT = np.array(list(map(function, old_LUT))) 74 | # Now try to make a minimal segment definition of the new LUT 75 | cdict = {} 76 | for i, key in enumerate(['red', 'green', 'blue']): 77 | this_cdict = {} 78 | for j, step in enumerate(step_list): 79 | if step in step_dict[key]: 80 | this_cdict[step] = new_LUT[j, i] 81 | elif new_LUT[j, i] != old_LUT[j, i]: 82 | this_cdict[step] = new_LUT[j, i] 83 | colorvector = list(map(lambda x: x + (x[1],), this_cdict.items())) 84 | colorvector.sort() 85 | cdict[key] = colorvector 86 | 87 | return mpl.colors.LinearSegmentedColormap('colormap', cdict, 1024) 88 | 89 | 90 | light_rdbu = cmap_map(lambda x: 0.5 * x + 0.5, plt.cm.RdBu) 91 | dark_bwr = cmap_map(lambda x: 0.75 * x, plt.cm.bwr) 92 | 93 | 94 | def plot_planar_boundary(plt, dataset, model, ax=None, grid_points=50, limit=2.5): 95 | if ax is None: 96 | ax = plt.axes() 97 | x_min, y_min = -limit, -limit 98 | x_max, y_max = limit, limit 99 | xx, yy = np.meshgrid(np.linspace(x_min, x_max, grid_points), np.linspace(x_min, x_max, grid_points)) 100 | 101 | # Predict the function value for the whole grid 102 | inputs = [] 103 | for x, y in zip(xx.flatten(), yy.flatten()): 104 | inputs.append([x, y]) 105 | inputs = add_bias(np.asarray(inputs, dtype=np.complex64)) 106 | 107 | Y_hat = model.predict(inputs) 108 | Y_hat = [yhat[0] for yhat in Y_hat] 109 | Z = np.array(Y_hat) 110 | Z = Z.reshape(xx.shape) 111 | 112 | # Plot the contour and training examples 113 | plot_handle = ax.contourf(xx, yy, Z, 50, cmap=light_rdbu, linewidths=0) 114 | plt.colorbar(ticks=[0, 0.2, 0.4, 0.6, 0.8, 1], mappable=plot_handle, ax=ax) 115 | points_x = dataset.X.T[0, :] 116 | points_y = dataset.X.T[1, :] 117 | labels = np.array([0 if yi[0] > yi[1] else 1 for yi in np.abs(dataset.y)]).flatten() 118 | 119 | ax.set_ylabel(r'$x_2$', fontsize=16) 120 | ax.set_xlabel(r'$x_1$', fontsize=16) 121 | 122 | ax.scatter(points_x, points_y, c=labels, edgecolors='black', linewidths=0.1, s=10, cmap=dark_bwr) 123 | 124 | 125 | def plot_planar_boundary_from_dataset(plt, dataset, params_list, iteration, ax=None): 126 | if ax is None: 127 | ax = plt.axes() 128 | 129 | xx, yy, Z = get_onn_contour_data(params_list, iteration) 130 | 131 | # Plot the contour and training examples 132 | levels = np.linspace(0, 1, 50) 133 | plot_handle = ax.contourf(xx, yy, Z, 50, cmap=light_rdbu, linewidths=0, levels=levels) 134 | points_x = dataset.X.T[0, :] 135 | points_y = dataset.X.T[1, :] 136 | labels = np.array([0 if yi[0] > yi[1] else 1 for yi in np.abs(dataset.y)]).flatten() 137 | 138 | ax.set_ylabel(r'$x_2$', fontsize=16) 139 | ax.set_xlabel(r'$x_1$', fontsize=16) 140 | 141 | ax.scatter(points_x, points_y, c=labels, edgecolors='black', linewidths=0.1, s=10, cmap=dark_bwr) 142 | return plot_handle 143 | 144 | 145 | def get_onn_contour_data(params_list, iteration, grid_points=50, limit=2.5): 146 | x_min, y_min = -limit, -limit 147 | x_max, y_max = limit, limit 148 | xx, yy = np.meshgrid(np.linspace(x_min, x_max, grid_points), np.linspace(x_min, x_max, grid_points)) 149 | 150 | onn_layers = {'layer1': triangular(4), 151 | 'layer2': triangular(4), 152 | 'layer3': triangular(4)} 153 | for layer in (1, 2, 3): 154 | onn_layers[f'layer{layer}'].params = ( 155 | np.mod(params_list[iteration][f'layer{layer}']['theta'], 2 * np.pi), 156 | np.mod(params_list[iteration][f'layer{layer}']['phi'], 2 * np.pi), 157 | np.mod(params_list[iteration][f'layer{layer}']['gamma'], 2 * np.pi) 158 | ) 159 | 160 | # Predict the function value for the whole grid 161 | inputs = [] 162 | for x, y in zip(xx.flatten(), yy.flatten()): 163 | inputs.append([x, y]) 164 | inputs = add_bias(np.asarray(inputs, dtype=np.complex64)) 165 | 166 | def predict(vin): 167 | out = vin 168 | for layer in (1, 2, 3): 169 | out = np.abs((onn_layers[f'layer{layer}'].matrix() @ out.T).T) 170 | out = softmax(np.asarray((np.sum(out[:, :2] ** 2, axis=1), np.sum(out[:, 2:] ** 2, axis=1))), axis=0) 171 | return out 172 | 173 | Y_hat = predict(inputs).T 174 | Y_hat = [yhat[0] for yhat in Y_hat] 175 | Z = np.array(Y_hat) 176 | Z = Z.reshape(xx.shape) 177 | 178 | return xx, yy, Z 179 | 180 | 181 | def plot_amf420_powers_mesh_only(ax, power_data, comparison_data=None, comparison_shift=1, cmap='hot'): 182 | start_polys = [0, 0, 2, 4] 183 | normed_powers = power_data / np.sum(power_data[6]) 184 | if comparison_data is not None: 185 | normed_comparison_powers = comparison_data / np.sum(comparison_data[6]) 186 | 187 | mask = np.ones((11, 4)) 188 | 189 | for i, poly in enumerate(start_polys): 190 | mask[:poly, i] = 0 191 | if poly > 0: 192 | mask[-poly:, i] = 0 193 | mask[poly, i] = 2 194 | mask[-poly - 1, i] = 3 195 | 196 | locs = np.array(np.where(mask == 1)).T 197 | left_locs = np.array(np.where(mask == 2)).T 198 | right_locs = np.array(np.where(mask == 3)).T 199 | 200 | possible_paths = path_array[::-1][:4, 4:15] 201 | 202 | multipolys = [possible_paths[r[1], r[0]] for r in locs] 203 | multipolys += [possible_paths[r[1], r[0]][2:] for r in left_locs] 204 | multipolys += [possible_paths[r[1], r[0]][:-1] for r in right_locs] 205 | waveguides = [Polygon(poly.T) for multipoly in multipolys for poly in multipoly] 206 | mesh = Pattern([poly for multipoly in multipolys for poly in multipoly]) 207 | if comparison_data is not None: 208 | shift = (comparison_shift + 1) * mesh.size[1] 209 | waveguides += [Polygon(poly.T + np.hstack((np.zeros_like(poly.T[:, :1]), np.ones_like(poly.T[:, 1:]) * shift))) 210 | for multipoly in multipolys for poly in multipoly] 211 | 212 | powers = np.hstack([[normed_powers[r[0], r[1]]] * len(mp) 213 | for r, mp in zip(np.vstack((locs, left_locs, right_locs)), multipolys)]) 214 | if comparison_data is not None: 215 | powers = np.hstack((powers, np.hstack([[normed_comparison_powers[r[0], r[1]]] * len(mp) 216 | for r, mp in 217 | zip(np.vstack((locs, left_locs, right_locs)), multipolys)]))) 218 | 219 | wg_patches = PatchCollection(waveguides, cmap=cmap, lw=1, edgecolor='black') 220 | wg_patches.set_array(np.zeros_like(powers)) 221 | p_patches = PatchCollection(waveguides, cmap=cmap, lw=0.5) 222 | p_patches.set_array(powers) 223 | p_patches.set_edgecolor(mpl.cm.hot(powers)) 224 | ax.add_collection(wg_patches) 225 | ax.add_collection(p_patches) 226 | b = mesh.bounds 227 | ax.set_xlim(b[0] - 2, b[2] + 2) 228 | ax.set_ylim(b[1] - 2, b[3] * (1 + (comparison_data is not None)) + 20) 229 | ax.set_aspect('equal') 230 | p_patches.set_clim(0, 1) 231 | ax.text(mesh.center[0], 32, 'Measured', ha='center', va='center') 232 | ax.text(mesh.center[0], 64, 'Predicted', ha='center', va='center') 233 | return p_patches 234 | 235 | 236 | def plot_amf420_backprop_iteration(fig, experiment: dict, iteration: int, final_iteration: Optional[int] = np.inf): 237 | max_iter = min(len(experiment['experiment']), final_iteration) 238 | subfigs = fig.subfigures(2, 1, height_ratios=[1.75, 1]) 239 | axs = subfigs[0].subplots(4, 3) 240 | for layer in (1, 2, 3): 241 | for i in range(3): 242 | title = ('Forward', 'Backward', 'Sum')[i] 243 | power_type = ('forward', 'backward', 'sum')[i] 244 | ax = axs[i, layer - 1] 245 | data = experiment['experiment'][iteration] 246 | im = plot_amf420_powers_mesh_only(ax, data['meas'][f'{power_type}_{layer}'].squeeze()[:11, :4], 247 | np.abs(data['pred'][f'{power_type}_{layer}'].squeeze()[::2, :4]) ** 2) 248 | ax.set_title(f'{title}{layer}') 249 | ax.axis('off') 250 | plt.colorbar(im, ax=ax) 251 | measured_gradients = experiment['experiment'][iteration]['meas']['gradients'][f'layer{layer}'] 252 | predicted_gradients = experiment['experiment'][iteration]['pred']['gradients'][0][f'layer{layer}'] 253 | ghat = np.hstack((measured_gradients['theta'][0], measured_gradients['phi'][0])) 254 | g = np.hstack((predicted_gradients['theta'], predicted_gradients['phi'])) 255 | index = np.arange(len(g)) 256 | bar_width = 0.45 257 | axs[-1, layer - 1].bar(index, ghat, bar_width, label=r"Measured") 258 | axs[-1, layer - 1].bar(index + bar_width, g, bar_width, label=r"Predicted") 259 | axs[-1, layer - 1].legend(bbox_to_anchor=(1, -0.01), fontsize=8) 260 | axs[-1, layer - 1].set_xticks([]) 261 | axs[-1, layer - 1].set_yticks([]) 262 | axs[-1, layer - 1].set_title(rf'Gradient{layer}: $\|g\| = {np.linalg.norm(g):3f}$') 263 | 264 | # cbar_ax = subfigs[0].add_axes([1, 0.25, 0.02, 0.7]) 265 | # fig.colorbar(im, cax=cbar_ax, label='Waveguide power') 266 | dataset = Dataset(experiment['X_train'], experiment['y_train']) 267 | axs = subfigs[1].subplots(1, 2) 268 | axs[0].plot([experiment['experiment'][i]['train_loss'] for i in range(max_iter)], 269 | label='train', color='black', linestyle='dotted') 270 | axs[0].plot([experiment['experiment'][i]['test_loss'] for i in range(max_iter)], 271 | label='test', color='black') 272 | axs[0].scatter(iteration, experiment['experiment'][iteration]['train_loss'], color='black') 273 | axs[0].scatter(iteration, experiment['experiment'][iteration]['test_loss'], color='black') 274 | axs[0].legend() 275 | axs[0].set_xlabel('Iteration') 276 | axs[0].set_ylabel('Cost function $\mathcal{L}$') 277 | plot_handle = plot_planar_boundary_from_dataset(plt, dataset, experiment['params_list'], iteration, ax=axs[1]) 278 | plt.colorbar(ticks=[0, 0.2, 0.4, 0.6, 0.8, 1], mappable=plot_handle, ax=axs[1]) 279 | 280 | 281 | def plot_amf420_powers(ax, power_data, ps_data, cmap='hot', ps_cmap='Greens'): 282 | ps_array_reshaped = np.reshape(ps_array, (6, 19, 4, 2)) 283 | normed_powers = power_data / np.sum(power_data, axis=1)[:, np.newaxis] 284 | normed_powers = normed_powers[:, ::-1] 285 | waveguides = [Polygon(poly.T) for multipoly in path_array.flatten() for poly in multipoly] 286 | phase_shifts = [Polygon(ps_array_reshaped[5 - ps_loc[1], ps_loc[0]]) for ps_loc in ps_data] 287 | ps_vals = [ps_data[ps_loc] for ps_loc in ps_data] 288 | powers = np.hstack([[normed_powers[c, r]] * len(path_array[r, c]) for r in range(6) for c in range(19)]) 289 | ps_patches = PatchCollection(phase_shifts, cmap=ps_cmap, lw=1, clim=(0, 2 * np.pi)) 290 | ps_patches.set_array(ps_vals) 291 | ps_patches.set_edgecolor(mpl.cm.Greens(np.array(ps_vals) / (2 * np.pi))) 292 | wg_patches = PatchCollection(waveguides, cmap=cmap, lw=0.75, edgecolor='black') 293 | wg_patches.set_array(np.zeros_like(powers)) 294 | p_patches = PatchCollection(waveguides, cmap=cmap, lw=0.25) 295 | p_patches.set_array(powers) 296 | p_patches.set_edgecolor(mpl.cm.hot(powers)) 297 | ax.add_collection(ps_patches) 298 | ax.add_collection(wg_patches) 299 | ax.add_collection(p_patches) 300 | b = mesh.bounds 301 | ax.set_xlim(b[0] - 2, b[2] + 2) 302 | ax.set_ylim(b[1] - 2, b[3] + 2) 303 | ax.set_aspect('equal') 304 | -------------------------------------------------------------------------------- /phox/apps/onn.py: -------------------------------------------------------------------------------- 1 | from .viz import Dataset, add_bias, light_rdbu, dark_bwr 2 | from ..experiment.amf420mesh import AMF420Mesh 3 | import jax 4 | import jax.numpy as jnp 5 | from jax import grad 6 | import optax 7 | import haiku as hk 8 | 9 | from simphox.circuit import triangular 10 | 11 | import numpy as np 12 | from keras import Model 13 | from scipy.special import softmax 14 | 15 | import time 16 | import pickle 17 | import copy 18 | 19 | 20 | PHI_LOCS = [(0, 0), (2, 1), (4, 2), (4, 0), (6, 1), (8, 0)] 21 | THETA_LOCS = [(1, 0), (3, 1), (5, 2), (5, 0), (7, 1), (9, 0)] 22 | 23 | 24 | def normalize(x): 25 | return x / np.linalg.norm(x) 26 | 27 | 28 | def extract_gradients_from_powers(forward_p, backward_p, sum_p, scaling=None): 29 | scaling = np.ones(sum_p.shape[1]) if scaling is None else scaling 30 | gradients = (sum_p - forward_p - backward_p) / 2 31 | return { 32 | 'theta': np.array([gradients[tl[0], :, tl[1]] for tl in THETA_LOCS]).T * scaling[:, np.newaxis], 33 | 'phi': np.array([gradients[pl[0], :, pl[1]] for pl in PHI_LOCS]).T * scaling[:, np.newaxis] 34 | } 35 | 36 | 37 | def extract_gradients_from_fields(forward, backward): 38 | gradients = -(backward * forward).imag 39 | return { 40 | 'theta': np.array([gradients[tl[0], :, tl[1]] for tl in THETA_LOCS]).T, 41 | 'phi': np.array([gradients[pl[0], :, pl[1]] for pl in PHI_LOCS]).T 42 | } 43 | 44 | 45 | def extract_gradients_from_sweep(sweep, scaling=None): 46 | scaling = np.ones(sweep.shape[1]) if scaling is None else scaling 47 | gradients = (sweep[:, :, 0] - np.mean(sweep, axis=2)) / 2 48 | return { 49 | 'theta': np.array([gradients[tl[0], :, tl[1]] for tl in THETA_LOCS]).T * scaling[:, np.newaxis], 50 | 'phi': np.array([gradients[pl[0], :, pl[1]] for pl in PHI_LOCS]).T * scaling[:, np.newaxis] 51 | } 52 | 53 | 54 | def extract_gradients_from_toggle(toggle, scaling=None): 55 | scaling = np.ones(toggle.shape[1]) if scaling is None else scaling 56 | gradients = (np.mean(np.abs(toggle - np.mean(toggle, axis=2)[..., np.newaxis]), axis=2)) / 2 57 | return { 58 | 'theta': np.array([gradients[tl[0], :, tl[1]] for tl in THETA_LOCS]).T * scaling[:, np.newaxis], 59 | 'phi': np.array([gradients[pl[0], :, pl[1]] for pl in PHI_LOCS]).T * scaling[:, np.newaxis] 60 | } 61 | 62 | def gradient_analog_update_test(chip: AMF420Mesh, sigma: float, u: np.ndarray, include_sweep=False, include_toggle=True, include_meas=True): 63 | """An exhaustive test for the analog update scheme in our backprop paper. 64 | 65 | Args: 66 | chip: The AMF420 chip to test 67 | sigma: The phase error in the unitary 68 | u: The unitary to test 69 | 70 | Returns: 71 | A dictionary containing the predicted and measured values at every stage of the process. 72 | 73 | """ 74 | good_mesh = triangular(u) 75 | 76 | bad_mesh = copy.deepcopy(good_mesh) 77 | bad_mesh.params = bad_mesh.phases(sigma) 78 | uhat = bad_mesh.matrix() 79 | 80 | matrix_fn = bad_mesh.matrix_fn(use_jax=True) 81 | 82 | def loss(u_, n): 83 | return lambda params: 1 - jnp.abs((matrix_fn(params) @ u_[n].conj())[n]) ** 2 84 | 85 | gradient = [grad(loss(u, i))(bad_mesh.params) for i in range(4)] 86 | 87 | if include_meas: 88 | meas = {} 89 | 90 | chip.set_transparent() 91 | chip.set_unitary_phases(bad_mesh.thetas, bad_mesh.phis) 92 | 93 | meas['forward'] = chip.matrix_prop(u.conj())[:, :, :4] 94 | chip.set_output_transparent() 95 | meas['forward_coherent'] = chip.coherent_batch(u.conj(), wait_time=0.1, coherent_4_alpha=1) * np.exp(1j * bad_mesh.gammas) 96 | meas['forward_output'] = np.sqrt(np.abs(meas['forward'][-1])) 97 | 98 | chip.toggle_propagation_direction() 99 | chip.set_transparent() 100 | chip.set_unitary_phases(bad_mesh.thetas, bad_mesh.phis) 101 | 102 | # note that here we assume we measure the correct phase 103 | # (we do not measure it for this test, as we find it introduces an order-of-mag more error) 104 | # a full backpropagation demo is still provided in the main text for the 2D classification problem 105 | phases = np.angle(np.diag(uhat @ u.T.conj())) 106 | 107 | meas['backward'] = chip.matrix_prop(np.eye(4, dtype=np.complex128))[:, :, :4] 108 | meas['backward_output'] = np.sqrt(np.abs(meas['backward'][0])) 109 | meas['backward_phase_corr'] = adjoint_signal = meas['backward_output'] * np.exp(1j * np.angle(uhat)) 110 | 111 | chip.toggle_propagation_direction() 112 | chip.set_transparent() 113 | gammas = chip.set_unitary_phases(bad_mesh.thetas, bad_mesh.phis) 114 | 115 | meas['sum_input'] = u.conj() + 1j * adjoint_signal.conj() * np.exp(1j * phases[:, np.newaxis]) 116 | meas['sum'] = chip.matrix_prop(meas['sum_input'])[:, :, :4] 117 | 118 | meas['gradients'] = extract_gradients_from_powers(meas['forward'], meas['backward'], meas['sum'], 119 | 2 * np.diag(meas['forward_output'])) 120 | meas['diff_input'] = u.conj() - 1j * adjoint_signal.conj() * np.exp(1j * phases[:, np.newaxis]) 121 | meas['diff'] = chip.matrix_prop(meas['diff_input'])[:, :, :4] 122 | meas['gradients_analog'] = extract_gradients_from_powers(meas['diff'], np.zeros_like(meas['diff']), meas['sum'], 123 | np.diag(meas['forward_output'])) 124 | 125 | if include_sweep: 126 | meas['sweep'] = chip.matrix_prop(np.vstack([ 127 | u.conj() + 1j * adjoint_signal.conj() * np.exp(1j * (phases[:, np.newaxis] + p)) 128 | for p in np.linspace(0, 2 * np.pi, 100) 129 | ])) 130 | 131 | meas['sweep'] = np.stack( 132 | [meas['sweep'][:, ::4, :], 133 | meas['sweep'][:, 1::4, :], 134 | meas['sweep'][:, 2::4, :], 135 | meas['sweep'][:, 3::4, :]], axis=1 136 | ) 137 | 138 | meas['gradients_sweep'] = extract_gradients_from_sweep(meas['sweep'], 2 * np.diag(meas['forward_output'])) 139 | if include_toggle: 140 | meas['toggle'] = chip.matrix_prop(np.vstack([ 141 | u.conj() + 1j * adjoint_signal.conj() * np.exp(1j * (phases[:, np.newaxis] + p)) 142 | for p in np.hstack([np.zeros(10), np.ones(10) * np.pi] * 5) 143 | ])) 144 | 145 | meas['toggle_interleave'] = np.stack( 146 | [meas['toggle'][:, ::4, :], 147 | meas['toggle'][:, 1::4, :], 148 | meas['toggle'][:, 2::4, :], 149 | meas['toggle'][:, 3::4, :]], axis=1 150 | ) 151 | 152 | pred = {} 153 | pred['forward'] = bad_mesh.propagate(u.T.conj()) 154 | pred['forward_output'] = pred['forward'][-1] 155 | phases = np.angle(np.diag(uhat @ u.T.conj())) 156 | pred['backward'] = bad_mesh.propagate(np.eye(4, dtype=np.complex128), back=True)[::-1] 157 | pred['backward_output'] = adjoint_signal = pred['backward'][0] 158 | pred['sum_input'] = u.T.conj() + 1j * adjoint_signal.conj() * np.exp(1j * phases[np.newaxis, :]) 159 | pred['sum'] = bad_mesh.propagate(pred['sum_input']) 160 | for t in ('forward', 'backward', 'sum'): 161 | pred[t] = pred[t].transpose(0, 2, 1) 162 | for t in ('forward_output', 'backward_output', 'sum_input'): 163 | pred[t] = pred[t].T 164 | pred['gradients'] = extract_gradients_from_powers(np.abs(pred['forward'][::2]) ** 2, 165 | np.abs(pred['backward'][::2]) ** 2, 166 | np.abs(pred['sum'][::2]) ** 2, 167 | 2 * np.diag(np.abs(pred['forward_output']))) 168 | pred['jax_gradients'] = {'theta': np.vstack([theta for theta, _, _ in gradient]), 169 | 'phi': np.vstack([phi for _, phi, _ in gradient])} 170 | 171 | return { 172 | 'sigma': sigma, 173 | 'uhat': uhat, 174 | 'u': u, 175 | 'meas': meas if include_meas else None, 176 | 'pred': pred 177 | } 178 | 179 | 180 | class Tri(hk.Module): 181 | def __init__(self, n, activation=None, name=None): 182 | super().__init__(name=name) 183 | self.output_size = self.n = n 184 | self._network = triangular(n) 185 | self._network.gammas = np.zeros_like(self._network.gammas) # useful hack for this demo 186 | self.matrix = self._network.matrix_fn(use_jax=True) 187 | self.activation = activation if activation is not None else (lambda x: x) 188 | 189 | def __call__(self, x): 190 | theta = hk.get_parameter("theta", 191 | shape=self._network.thetas.shape, 192 | init=hk.initializers.Constant(self._network.thetas)) 193 | phi = hk.get_parameter("phi", 194 | shape=self._network.phis.shape, 195 | init=hk.initializers.Constant(self._network.phis)) 196 | gamma = hk.get_parameter("gamma", 197 | shape=self._network.gammas.shape, 198 | init=hk.initializers.Constant(self._network.gammas)) 199 | return self.activation((self.matrix((theta, phi, gamma)) @ x.T).T) 200 | 201 | 202 | def optical_softmax(x): 203 | y = jnp.abs(x) ** 2 204 | return jnp.vstack((jnp.sum(y[:, :2], axis=1), 205 | jnp.sum(y[:, 2:], axis=1))).T 206 | 207 | 208 | def softmax_cross_entropy(logits, labels): 209 | return -jnp.sum(jax.nn.log_softmax(logits) * labels, axis=-1) 210 | 211 | 212 | def optical_softmax_cross_entropy(labels): 213 | return lambda x: jnp.mean(softmax_cross_entropy(optical_softmax(x), labels)).astype(jnp.float64) 214 | 215 | 216 | def onn(x, y): 217 | logits = hk.Sequential([ 218 | Tri(4, activation=jnp.abs, name='layer1'), 219 | Tri(4, activation=jnp.abs, name='layer2'), 220 | Tri(4, activation=jnp.abs, name='layer3') 221 | ])(x) 222 | return optical_softmax_cross_entropy(y)(logits) 223 | 224 | 225 | def onn_accuracy(x, y): 226 | logits = hk.Sequential([ 227 | Tri(4, activation=jnp.abs, name='layer1'), 228 | Tri(4, activation=jnp.abs, name='layer2'), 229 | Tri(4, activation=jnp.abs, name='layer3') 230 | ])(x) 231 | yhat = optical_softmax(logits) 232 | label = yhat.T[1] > yhat.T[0] 233 | 234 | return jnp.sum(jnp.abs(y.T[0] - label)) / y.T[0].size 235 | 236 | onn_t = hk.without_apply_rng(hk.transform(onn)) 237 | 238 | onn_a = hk.without_apply_rng(hk.transform(onn_accuracy)) 239 | 240 | 241 | @jax.jit 242 | def loss(params, X, y): 243 | return onn_t.apply(params, X, y) 244 | 245 | 246 | def get_update_fn(opt): 247 | @jax.jit 248 | def update(params: hk.Params, opt_state: optax.OptState, X, y): 249 | """Learning rule (stochastic gradient descent).""" 250 | grads = jax.grad(loss)(params, X, y) 251 | updates, opt_state = opt.update(grads, opt_state) 252 | new_params = optax.apply_updates(params, updates) 253 | return new_params, opt_state 254 | return update 255 | 256 | 257 | def get_gradient_predictions_onn(onn_layers, X, y, idx=0): 258 | pred = dict() 259 | pred['input_1'] = X[idx] 260 | for layer in (1, 2, 3): 261 | pred[f'forward_{layer}'] = onn_layers[f'layer{layer}'].propagate(pred[f'input_{layer}']) 262 | pred[f'input_{layer + 1}'] = np.abs(pred[f'forward_{layer}'][-1]) + 0j 263 | cost_fn = optical_softmax_cross_entropy(y[idx]) 264 | pred[f'adjoint_4'] = np.array(jax.grad(cost_fn)(jnp.array(pred[f'input_4'])[np.newaxis, :])).squeeze() 265 | for layer in (3, 2, 1): 266 | pred[f'error_{layer}'] = pred[f'adjoint_{layer + 1}'].real * np.exp( 267 | -1j * np.angle(pred[f'forward_{layer}'][-1])) # abs value nonlinearity 268 | pred[f'backward_{layer}'] = onn_layers[f'layer{layer}'].propagate(pred[f'error_{layer}'], back=True)[::-1] 269 | pred[f'adjoint_{layer}'] = pred[f'backward_{layer}'][0] 270 | for layer in (1, 2, 3): 271 | pred[f'sum_{layer}'] = onn_layers[f'layer{layer}'].propagate( 272 | pred[f'input_{layer}'] - 1j * pred[f'adjoint_{layer}'].conj()) 273 | 274 | pred['gradients'] = { 275 | f'layer{layer}': extract_gradients_from_fields(pred[f'forward_{layer}'][::2], 276 | pred[f'backward_{layer}'][::2]) 277 | for layer in (1, 2, 3) 278 | } 279 | return pred 280 | 281 | 282 | class BackpropAccuracyTest: 283 | """This class tests a specific 3-layer photonic neural network for 2D classification against digital simulations. 284 | 285 | """ 286 | def __init__(self, chip: AMF420Mesh, params_list, X, y, idx_list, iteration, wait_time: float = 0.05): 287 | self.X = X 288 | self.y = y 289 | self.chip = chip 290 | self.onn_layers = {} 291 | self.meas = {} 292 | self.idx_list = idx_list 293 | self.wait_time = wait_time 294 | self.onn_layers = {'layer1': triangular(4), 295 | 'layer2': triangular(4), 296 | 'layer3': triangular(4)} 297 | for layer in (1, 2, 3): 298 | self.onn_layers[f'layer{layer}'].params = ( 299 | np.mod(params_list[iteration][f'layer{layer}']['theta'], 2 * np.pi), 300 | np.mod(params_list[iteration][f'layer{layer}']['phi'], 2 * np.pi), 301 | np.mod(params_list[iteration][f'layer{layer}']['gamma'], 2 * np.pi) 302 | ) 303 | self.pred = get_gradient_predictions_onn(self.onn_layers, X, y, idx_list[0]) 304 | pred_list = [get_gradient_predictions_onn(self.onn_layers, X, y, i) for i in idx_list] 305 | self.pred = {key: np.array([p[key] for p in pred_list]) for key in self.pred} 306 | for key in self.pred: 307 | if self.pred[key].ndim == 3: 308 | self.pred[key] = self.pred[key].transpose((1, 0, 2)) 309 | 310 | def _sum_measure(self): 311 | for layer in (1, 2, 3): 312 | sum_input = self.meas[f'input_{layer}'] - 1j * self.meas[f'adjoint_{layer}'].conj() 313 | self.chip.set_unitary_phases(self.onn_layers[f'layer{layer}'].thetas, self.onn_layers[f'layer{layer}'].phis) 314 | self.meas[f'sum_{layer}'] = self.chip.matrix_prop(sum_input) 315 | 316 | def _forward_measure(self, phase_corr: bool = False): 317 | self.chip.set_transparent() 318 | self.meas['input_1'] = np.array([self.X[idx] for idx in self.idx_list]) 319 | for layer in (1, 2, 3): 320 | self.chip.set_unitary_phases(self.onn_layers[f'layer{layer}'].thetas, self.onn_layers[f'layer{layer}'].phis) 321 | self.meas[f'forward_{layer}'] = self.chip.matrix_prop(self.meas[f'input_{layer}']) 322 | self.meas[f'input_{layer + 1}'] = np.sqrt(np.abs(self.meas[f'forward_{layer}'][-1][:, :4])) 323 | if phase_corr: 324 | self.meas[f'forward_out_{layer}'] = self.pred[f'forward_{layer}'][-1] 325 | else: 326 | self.meas[f'forward_out_{layer}'] = self.chip.coherent_batch(self.meas[f'input_{layer}']) 327 | self.chip.set_output_transparent() 328 | 329 | def _backward_measure(self, phase_corr: bool = False): 330 | cost_fn = [optical_softmax_cross_entropy(self.y[idx]) for idx in self.idx_list] 331 | self.meas['adjoint_4'] = np.array([jax.grad(cost_fn[i])(self.meas[f'input_4'][i:i + 1, :]).squeeze() 332 | for i in range(len(self.idx_list))]) 333 | self.chip.set_transparent() 334 | self.chip.toggle_propagation_direction() 335 | for layer in (3, 2, 1): 336 | forward_phasors = np.exp(-1j * np.angle(self.meas[f'forward_out_{layer}'])) 337 | self.meas[f'error_{layer}'] = self.meas[f'adjoint_{layer + 1}'].real * forward_phasors 338 | self.chip.set_unitary_phases(self.onn_layers[f'layer{layer}'].thetas, self.onn_layers[f'layer{layer}'].phis) 339 | self.meas[f'backward_{layer}'] = self.chip.matrix_prop(self.meas[f'error_{layer}']) 340 | if phase_corr: 341 | self.meas[f'backward_out_{layer}'] = self.pred[f'backward_{layer}'][0] 342 | else: 343 | self.meas[f'backward_out_{layer}'] = self.chip.coherent_batch(self.meas[f'error_{layer}']) 344 | backward_phasors = np.exp(1j * np.angle(self.meas[f'backward_out_{layer}'])) 345 | self.meas[f'adjoint_{layer}'] = np.sqrt(np.abs(self.meas[f'backward_{layer}'][0][:, :4])) * backward_phasors 346 | self.chip.set_output_transparent() 347 | self.chip.toggle_propagation_direction() 348 | 349 | def run(self, phase_corr: bool = False): 350 | self.chip.reset_control() 351 | self._forward_measure(phase_corr) 352 | self._backward_measure(phase_corr) 353 | self._sum_measure() 354 | self.chip.reset_control() 355 | self.meas['gradients'] = {f'layer{layer}': extract_gradients_from_powers( 356 | self.meas[f'forward_{layer}'][:11, :, :4], 357 | self.meas[f'backward_{layer}'][:11, :, :4], 358 | self.meas[f'sum_{layer}'][:11, :, :4] 359 | ) for layer in (1, 2, 3)} 360 | 361 | 362 | #TODO(sunil): fix a lot of boilerplate code. 363 | class ONN2D: 364 | def __init__(self, dataset: Dataset, dataset_test: Dataset, 365 | n_layers: int, 366 | y_sim: np.ndarray = None, y_sim_test: np.ndarray = None, 367 | y_onn: np.ndarray = None, y_onn_test: np.ndarray = None): 368 | self.n_layers = n_layers 369 | self.unitaries = [] 370 | self.dataset = dataset 371 | self.dataset_test = dataset_test 372 | self.y_sim = [] if y_sim is None else np.asarray(y_sim) 373 | self.y_sim_test = [] if y_sim_test is None else np.asarray(y_sim_test) 374 | self.y_onn = [] if y_onn is None else y_onn 375 | self.y_onn_test = [] if y_onn_test is None else y_onn_test 376 | 377 | def set_model(self, model: Model): 378 | self.unitaries = [model.layers[i].matrix.conj().T for i in range(self.n_layers)] 379 | 380 | def onn(self, input_vector: np.ndarray, mesh: AMF420Mesh, meas_delay: float = 0.2, factor: float = 3): 381 | outputs = input_vector 382 | for u in self.unitaries: 383 | mesh.set_input(outputs) 384 | mesh.set_unitary(u) 385 | time.sleep(meas_delay) 386 | outputs = np.sqrt(np.abs(mesh.fractional_right[:4])) 387 | return softmax(factor ** 2 * np.asarray((np.sum(outputs[:2] ** 2), np.sum(outputs[2:] ** 2)))) 388 | 389 | def classify_train(self, mesh, model: Model, pbar=None, meas_time: float = 0.2): 390 | self.y_sim = [] 391 | self.y_onn = [] 392 | iterator = pbar(range(len(self.dataset.X))) if pbar else range(len(self.dataset.X)) 393 | for i in iterator: 394 | self.y_sim.append(model(self.dataset.X[i])) 395 | self.y_onn.append(self.onn(self.dataset.X[i], mesh, meas_time)) 396 | self.y_sim = np.asarray(self.y_sim)[:, 0, :] 397 | self.y_onn = np.asarray(self.y_onn) 398 | 399 | def classify_test(self, mesh, model: Model, pbar=None, meas_time: float = 0.2): 400 | self.y_sim_test = [] 401 | self.y_onn_test = [] 402 | iterator = pbar(range(len(self.dataset_test.X))) if pbar else range(len(self.dataset_test.X)) 403 | for i in iterator: 404 | self.y_sim_test.append(model(self.dataset_test.X[i])) 405 | self.y_onn_test.append(self.onn(self.dataset_test.X[i], mesh, meas_time)) 406 | self.y_sim_test = np.asarray(self.y_sim_test)[:, 0, :] 407 | self.y_onn_test = np.asarray(self.y_onn_test) 408 | 409 | def save(self, filename: str): 410 | with open(filename, 'wb') as f: 411 | pickle.dump(self.__dict__, f) 412 | 413 | def plot(self, plt, model: Model, ax=None, grid_points=50, sim: bool = False): 414 | if ax is None: 415 | ax = plt.axes() 416 | x_min, y_min = -2.5, -2.5 417 | x_max, y_max = 2.5, 2.5 418 | xx, yy = np.meshgrid(np.linspace(x_min, x_max, grid_points), np.linspace(x_min, x_max, grid_points)) 419 | 420 | # Predict the function value for the whole grid 421 | inputs = [] 422 | for x, y in zip(xx.flatten(), yy.flatten()): 423 | inputs.append([x, y]) 424 | inputs = add_bias(np.asarray(inputs, dtype=np.complex64)) 425 | 426 | Y_hat = model.predict(inputs) 427 | Y_hat = [yhat[0] for yhat in Y_hat] 428 | Z = np.array(Y_hat) 429 | Z = Z.reshape(xx.shape) 430 | 431 | # Plot the contour and training examples 432 | plot_handle = ax.contourf(xx, yy, Z, 50, cmap=light_rdbu, linewidths=0) 433 | plt.colorbar(ticks=[0, 0.2, 0.4, 0.6, 0.8, 1], mappable=plot_handle, ax=ax) 434 | plot_labels(ax, dataset=self.dataset, ys=self.y_sim if sim else self.y_onn) 435 | plot_labels(ax, dataset=self.dataset_test, ys=self.y_sim_test if sim else self.y_onn_test) 436 | ax.set_ylabel(r'$x_2$', fontsize=16) 437 | ax.set_xlabel(r'$x_1$', fontsize=16) 438 | 439 | def test_accuracy(self): 440 | return accuracy(self.dataset_test.y, self.y_sim_test, self.y_onn_test) 441 | 442 | def train_accuracy(self): 443 | return accuracy(self.dataset.y, self.y_sim, self.y_onn) 444 | 445 | 446 | def accuracy(actual, predicted, experimental): 447 | y = np.asarray([y[0] > y[1] for y in predicted], dtype=np.int32) 448 | y_sim = np.asarray([y[0] > y[1] for y in experimental], dtype=np.int32) 449 | y_act = np.asarray([y[0] > y[1] for y in actual], dtype=np.int32) 450 | return np.sum(np.abs(y - y_act)), np.sum(np.abs(y_sim - y_act)) 451 | 452 | 453 | def plot_labels(ax, dataset: Dataset, ys: np.ndarray): 454 | points_x = dataset.X.T[0, :] 455 | points_y = dataset.X.T[1, :] 456 | labels = np.array( 457 | [0 if yi[0] > yi[1] else 1 for yi in np.abs(ys)]).flatten() 458 | ax.scatter(points_x, points_y, c=labels, edgecolors='black', linewidths=0.1, s=10, cmap=dark_bwr, alpha=1) 459 | 460 | -------------------------------------------------------------------------------- /phox/experiment/amf420mesh.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pickle 3 | import time 4 | from typing import Callable, Dict, Optional, Tuple, Union 5 | 6 | import holoviews as hv 7 | import numpy as np 8 | import panel as pn 9 | from dphox.demo import mesh, mzi 10 | from holoviews import opts 11 | from holoviews.streams import Pipe 12 | from scipy.linalg import svd, dft 13 | from scipy.stats import unitary_group 14 | from simphox.circuit import ForwardMesh, triangular, unbalanced_tree 15 | from simphox.utils import random_unitary, random_vector 16 | from tornado import gen 17 | from tornado.ioloop import PeriodicCallback 18 | 19 | from .activephotonicsimager import _get_grating_spot, ActivePhotonicsImager 20 | from ..instrumentation import XCamera 21 | from ..model.phase import PhaseCalibration 22 | 23 | path_array, ps_array = mesh.demo_polys() 24 | logger = logging.getLogger() 25 | logger.setLevel(logging.WARN) 26 | 27 | PS_LAYER = 'heater' 28 | RIGHT_LAYER = 16 29 | LEFT_LAYER = 0 30 | 31 | 32 | AMF420MESH_CONFIG = { 33 | "network": {"theta_left": [(1, 1), (3, 2), (5, 3), (7, 4)], "phi_left": [(2, 1), (4, 2), (6, 3), (8, 4)], 34 | "theta_right": [(17, 1), (15, 2), (13, 3), (11, 4)], "phi_right": [(16, 1), (14, 2), (12, 2), (10, 4)], 35 | "theta_mesh": [(5, 0), (7, 1), (9, 2), (9, 0), (11, 1), (13, 0)], 36 | "phi_mesh": [(4, 0), (6, 1), (8, 2), (8, 0), (10, 1), (12, 0)], "theta_ref": (9, 5)}, 37 | "thetas": [{"grid_loc": (1, 1), "spot_loc": (0, 0), "voltage_channel": 5, "meta_ps": []}, 38 | {"grid_loc": (5, 0), "spot_loc": (4, 0), "voltage_channel": 17, "meta_ps": []}, 39 | {"grid_loc": (9, 0), "spot_loc": (8, 0), "voltage_channel": 39, "meta_ps": []}, 40 | {"grid_loc": (13, 0), "spot_loc": (12, 0), "voltage_channel": 48, "meta_ps": []}, 41 | {"grid_loc": (17, 1), "spot_loc": (16, 0), "voltage_channel": 59, "meta_ps": []}, 42 | {"grid_loc": (3, 2), "spot_loc": (2, 1), "voltage_channel": 7, "meta_ps": []}, 43 | {"grid_loc": (7, 1), "spot_loc": (6, 1), "voltage_channel": 26, "meta_ps": []}, 44 | {"grid_loc": (11, 1), "spot_loc": (10, 1), "voltage_channel": 40, "meta_ps": []}, 45 | {"grid_loc": (15, 2), "spot_loc": (14, 1), "voltage_channel": 55, "meta_ps": []}, 46 | {"grid_loc": (5, 3), "spot_loc": (4, 2), "voltage_channel": 15, "meta_ps": []}, 47 | {"grid_loc": (9, 2), "spot_loc": (8, 2), "voltage_channel": 32, "meta_ps": []}, 48 | {"grid_loc": (13, 3), "spot_loc": (12, 2), "voltage_channel": 46, "meta_ps": []}, 49 | {"grid_loc": (7, 4), "spot_loc": (6, 3), "voltage_channel": 25, "meta_ps": []}, 50 | {"grid_loc": (11, 4), "spot_loc": (10, 3), "voltage_channel": 38, "meta_ps": []}, 51 | {"grid_loc": (9, 5), "spot_loc": (8, 4), "voltage_channel": 28, "meta_ps": []}], 52 | "phis": [{"grid_loc": (2, 1), "spot_loc": (4, 0), "voltage_channel": 4, "meta_ps": [(1, 1), (5, 0)]}, 53 | {"grid_loc": (4, 0), "spot_loc": (4, 0), "voltage_channel": 13, "meta_ps": [(1, 1), (5, 0)]}, 54 | {"grid_loc": (6, 1), "spot_loc": (8, 0), "voltage_channel": 20, "meta_ps": [(5, 0), (9, 0)]}, 55 | {"grid_loc": (8, 0), "spot_loc": (8, 0), "voltage_channel": 27, "meta_ps": [(5, 0), (9, 0)]}, 56 | {"grid_loc": (10, 1), "spot_loc": (12, 0), "voltage_channel": 37, "meta_ps": [(9, 0), (13, 0)]}, 57 | {"grid_loc": (12, 0), "spot_loc": (12, 0), "voltage_channel": 44, "meta_ps": [(9, 0), (13, 0)]}, 58 | {"grid_loc": (16, 1), "spot_loc": (16, 0), "voltage_channel": 56, "meta_ps": [(13, 0), (17, 1)]}, 59 | {"grid_loc": (4, 2), "spot_loc": (6, 1), "voltage_channel": 16, "meta_ps": [(3, 2), (7, 1)]}, 60 | {"grid_loc": (8, 2), "spot_loc": (10, 1), "voltage_channel": 35, "meta_ps": [(7, 1), (11, 1)]}, 61 | {"grid_loc": (12, 2), "spot_loc": (14, 1), "voltage_channel": 47, "meta_ps": [(11, 1), (15, 2)]}, 62 | {"grid_loc": (14, 2), "spot_loc": (14, 1), "voltage_channel": 51, "meta_ps": [(11, 1), (15, 2)]}, 63 | {"grid_loc": (6, 3), "spot_loc": (8, 2), "voltage_channel": 21, "meta_ps": [(5, 3), (9, 2)]}, 64 | {"grid_loc": (8, 4), "spot_loc": (10, 3), "voltage_channel": 24, "meta_ps": [(7, 4), (11, 4)]}, 65 | {"grid_loc": (10, 4), "spot_loc": (10, 3), "voltage_channel": 34, "meta_ps": [(7, 4), (11, 4)]}], 66 | "phis_parallel": [ 67 | {"grid_loc": (4, 0), "spot_loc": (4, 0), "voltage_channel": 13, "meta_ps": [(5, 0)]}, 68 | {"grid_loc": (6, 1), "spot_loc": (6, 1), "voltage_channel": 20, "meta_ps": [(7, 1)]}, 69 | {"grid_loc": (8, 0), "spot_loc": (8, 0), "voltage_channel": 27, "meta_ps": [(9, 0)]}, 70 | {"grid_loc": (8, 2), "spot_loc": (8, 2), "voltage_channel": 35, "meta_ps": [(9, 2)]}, 71 | {"grid_loc": (10, 1), "spot_loc": (10, 1), "voltage_channel": 37, "meta_ps": [(11, 1)]}, 72 | {"grid_loc": (10, 4), "spot_loc": (10, 3), "voltage_channel": 34, "meta_ps": [(11, 4)]}, 73 | {"grid_loc": (12, 0), "spot_loc": (12, 0), "voltage_channel": 44, "meta_ps": [(13, 0)]}, 74 | {"grid_loc": (12, 2), "spot_loc": (12, 2), "voltage_channel": 47, "meta_ps": [(13, 3)]}, 75 | {"grid_loc": (14, 2), "spot_loc": (14, 1), "voltage_channel": 51, "meta_ps": [(15, 2)]}, 76 | {"grid_loc": (16, 1), "spot_loc": (16, 0), "voltage_channel": 56, "meta_ps": [(17, 1)]}], 77 | } 78 | 79 | 80 | class AMF420Mesh(ActivePhotonicsImager): 81 | def __init__(self, interlayer_xy: Tuple[float, float], spot_rowcol: Tuple[int, int], interspot_xy: Tuple[int, int], 82 | ps_calibration: Dict, window_dim: Tuple[int, int] = (15, 10), pd_calibration: Optional[np.ndarray] = None, 83 | backward_shift: float = 0.033, home: Tuple[float, float] = (0, 0), stage_port: str = '/dev/ttyUSB1', 84 | laser_port: str = '/dev/ttyUSB0', lmm_port: str = None, 85 | camera_calibration_filepath: Optional[str] = None, integration_time: int = 20000, 86 | plim: Tuple[float, float] = (0.05, 4.25), vmax: float = 5, mesh_2: bool = False, broken_ps = None): 87 | """This class is meant to test our first triangular mesh fabricated in AMF. 88 | 89 | These chips are 6x6 and contain thermal phase shifters placed strategically to enable arbitrary 4x4 90 | coherent matrix multiply operations. 91 | 92 | Args: 93 | interlayer_xy: Interlayer xy stage travel. 94 | spot_rowcol: Row, col center of the spot in the frame coordinates (numpy array) 95 | interspot_xy: Distance between the various spots on the camera (affected by whether objective is 5x or 10x) 96 | window_dim: Dimension of the spot window used. 97 | backward_shift: Backward shift for spot imaging in millimeters when sending light backward (33 um for this chip) 98 | home: Home location to use for the stage controller. 99 | stage_port: Stage serial port (Stage controller for taking images at various parts of the mesh.). 100 | laser_port: Laser serial port (Agilent laser). 101 | lmm_port: Laser multimeter port for serial control of the laser multimeter (if useful, otherwise use None to ignore). 102 | camera_calibration_filepath: 103 | integration_time: Integration time for the IR camera 104 | plim: Phase shift voltage limit for calibration and nulling sweeps. 105 | vmax: Maximum voltage limit for any phase shift setting. 106 | mesh_2: Use the second mesh by incrementing the voltage channel by 64 107 | broken_ps: Specify the broken phase shifters 108 | """ 109 | self.network = AMF420MESH_CONFIG['network'] 110 | self.thetas = [PhaseShifter(**ps_dict, mesh=self, 111 | calibration=PhaseCalibration(**ps_calibration[tuple(ps_dict['grid_loc'])]) 112 | ) for ps_dict in AMF420MESH_CONFIG['thetas']] 113 | self.thetas: Dict[Tuple[int, int], PhaseShifter] = {ps.grid_loc: ps for ps in self.thetas} 114 | self.phis = [PhaseShifter(**ps_dict, mesh=self, 115 | calibration=PhaseCalibration(**ps_calibration[tuple(ps_dict['grid_loc'])]) 116 | ) for ps_dict in AMF420MESH_CONFIG['phis']] 117 | self.phis: Dict[Tuple[int, int], PhaseShifter] = {ps.grid_loc: ps for ps in self.phis} 118 | self.mesh_2 = mesh_2 119 | if mesh_2: 120 | for _, ps in self.thetas.items(): 121 | ps.voltage_channel += 64 122 | for _, ps in self.phis.items(): 123 | ps.voltage_channel += 64 124 | self.ps: Dict[Tuple[int, int], PhaseShifter] = {**self.thetas, **self.phis} 125 | self.interlayer_xy = interlayer_xy 126 | self.spot_rowcol = s = spot_rowcol 127 | self.interspot_xy = ixy = interspot_xy 128 | self.pd_calibration = pd_calibration 129 | self.pd_params = [np.polyfit(pd_calibration[i, i], np.linspace(0, 1, 100), 5) for i in range(4)] if pd_calibration is not None else None 130 | 131 | self.window_dim = window_dim 132 | self.spots = [(int(j * ixy[0] + s[0]), int(i * ixy[1] + s[1]), window_dim[0], window_dim[1]) 133 | for j in range(6) for i in range(3)] 134 | self.camera = XCamera(integration_time=integration_time, spots=self.spots) 135 | self.integration_time = integration_time 136 | self.backward = False 137 | self.backward_shift = backward_shift 138 | super().__init__(home, stage_port, laser_port, lmm_port, camera_calibration_filepath, 139 | integration_time, plim, vmax) 140 | 141 | self.reset_control() 142 | self.camera.start_frame_loop() 143 | self.go_home() 144 | self.stage.wait_until_stopped() 145 | self.power_pipe = Pipe() 146 | self.ps_pipe = Pipe() 147 | self.spot_pipe = Pipe(data=[(i, 0) for i in range(6)]) 148 | time.sleep(0.1) 149 | self.layer = (0, False) 150 | 151 | """ 152 | self.broken_ps = broken_ps 153 | ## remove the controls in theta/phi list 154 | if self.broken_ps is not None: 155 | for kk in self.thetas.keys(): 156 | if kk in self.broken_ps: 157 | del self.thetas[kk] 158 | for kk in self.phis.keys(): 159 | if kk in self.broken_ps: 160 | del self.phis[kk] 161 | """ 162 | 163 | 164 | def to_layer(self, layer: int, wait_time: float = 0.0): 165 | if self.layer != (layer, self.backward): 166 | self.layer = layer, self.backward 167 | backward_shift = self.backward * self.backward_shift 168 | self.stage.move(x=self.home[0] + self.interlayer_xy[0] * layer + backward_shift * self.mesh_2, 169 | y=self.home[1] + self.interlayer_xy[1] * layer + backward_shift * (1 - self.mesh_2)) 170 | self.stage.wait_until_stopped() 171 | time.sleep(wait_time) 172 | 173 | def mesh_img(self, n: int = 6, wait_time: float = 0.5, window_dim: Tuple[int, int] = None): 174 | """ 175 | 176 | Args: 177 | n: Number of inputs to the mesh 178 | wait_time: Wait time after the stage stops moving for things to settle 179 | window_size: Window size for the spots (use window_dim by default) 180 | 181 | Returns: 182 | 183 | """ 184 | powers = [] 185 | spots = [] 186 | window_dim = self.window_dim if window_dim is None else window_dim 187 | s, ixy = self.spot_rowcol, self.interspot_xy 188 | for m in range(n + 1): 189 | self.to_layer(3 * m if m < n else 3 * n - 2) 190 | time.sleep(wait_time) 191 | img = self.camera.frame() 192 | if m < n: 193 | powers.append( 194 | np.hstack([np.vstack([_get_grating_spot(img, center=(j * ixy[0] + s[0], i * ixy[1] + s[1]), 195 | window_dim=window_dim)[0] 196 | for j in range(n)]) for i in range(3)])) 197 | spots.append(np.hstack( 198 | [np.vstack([_get_grating_spot(img, center=(j * ixy[0] + s[0], i * ixy[1] + s[1]), 199 | window_dim=window_dim)[1] / np.sum(powers[-1][:, i]) 200 | for j in range(n)]) for i in range(3)])) 201 | else: 202 | powers.append(np.vstack([_get_grating_spot(img, center=(j * ixy[0] + s[0], s[1]), 203 | window_dim=window_dim)[0] for j in range(n)])) 204 | spots.append(np.vstack([_get_grating_spot(img, center=(j * ixy[0] + s[0], s[1]), 205 | window_dim=window_dim)[1] / np.sum(powers[-1]) 206 | for j in range(n)])) 207 | direction = 1 if self.mesh_2 else 1 208 | return np.fliplr(np.hstack(powers[::-1]))[::direction], np.fliplr(np.hstack(spots[::-1]))[::direction] 209 | 210 | def sweep(self, channel: int, layer: int, vlim: Tuple[float, float], 211 | wait_time: float = 0.0, n_samples: int = 1001, move: bool = True, 212 | pbar: Optional[Callable] = None) -> Tuple[np.ndarray, np.ndarray]: 213 | """ 214 | 215 | Args: 216 | channel: voltage channel to sweep 217 | layer: layer to move the stage 218 | vlim: Voltage limit for the sweep 219 | wait_time: Wait time between setting the temperature and taking the image 220 | n_samples: Number of samples 221 | move: whether to move to the appropriate layer 222 | pbar: progress bar (optional) to track the progress of the sweep 223 | 224 | Returns: 225 | 226 | """ 227 | if move: 228 | self.to_layer(layer) 229 | vs = np.sqrt(np.linspace(vlim[0] ** 2, vlim[1] ** 2, n_samples)) 230 | iterator = pbar(vs) if pbar is not None else vs 231 | powers = [] 232 | for v in iterator: 233 | self.control.write_chan(channel, v) 234 | time.sleep(wait_time) 235 | powers.append(self.camera.spot_powers) 236 | return vs, np.asarray(powers).T 237 | 238 | def reset_control(self, vmin: float = 2): 239 | for device in self.control.system.devices: 240 | device.reset_device() 241 | for ps in self.ps: 242 | self.control.write_chan(self.ps[ps].voltage_channel, vmin) 243 | 244 | def propagation_toggle_panel(self, chan: int = 96): 245 | def toggle(*events): 246 | self.toggle_propagation_direction(chan) 247 | 248 | button = pn.widgets.Button(name='Switch Propagation Direction') 249 | button.on_click(toggle) 250 | return button 251 | 252 | def toggle_propagation_direction(self, chan: int = 96): 253 | self.backward = not self.backward 254 | self.control.ttl_toggle(chan) 255 | 256 | def led_panel(self, chan: int = 97): 257 | return self.control.continuous_slider(chan, name='LED Voltage', vlim=(0, 3)) 258 | 259 | def home_panel(self): 260 | home_button = pn.widgets.Button(name='Home') 261 | 262 | def go_home(*events): 263 | self.go_home() 264 | 265 | home_button.on_click(go_home) 266 | return home_button 267 | 268 | @property 269 | def output_layer(self): 270 | return LEFT_LAYER if self.backward else RIGHT_LAYER 271 | 272 | def input_panel(self): 273 | def transparent_bar(*events): 274 | self.set_transparent() 275 | 276 | def transparent_cross(*events): 277 | self.set_transparent(bar=False) 278 | 279 | def reset(*events): 280 | self.reset_control() 281 | 282 | def alternating(*events): 283 | self.set_input(np.asarray((1, 0, 1, 0, 1))) 284 | 285 | def uniform(*events): 286 | self.set_input(np.asarray((1, 1, 1, 1, 1))) 287 | 288 | def random(*events): 289 | self.set_rand_unitary() 290 | 291 | def basis(i: int): 292 | def f(*events): 293 | self.set_input(np.asarray(np.eye(5)[i])) 294 | 295 | return f 296 | 297 | bar_button = pn.widgets.Button(name='Transparent (Bar)') 298 | bar_button.on_click(transparent_bar) 299 | cross_button = pn.widgets.Button(name='Transparent (Cross)') 300 | cross_button.on_click(transparent_cross) 301 | alternating_button = pn.widgets.Button(name='Alternate In (1-0-1-0-1)') 302 | alternating_button.on_click(alternating) 303 | uniform_button = pn.widgets.Button(name='Uniform In (1-1-1-1-1)') 304 | uniform_button.on_click(uniform) 305 | random_button = pn.widgets.Button(name='Random Unitary') 306 | random_button.on_click(random) 307 | button_list = [pn.widgets.Button(name=f'{i}', width=15) for i in range(5)] 308 | for i, button in enumerate(button_list): 309 | button.on_click(basis(i)) 310 | buttons = pn.Row(*button_list) 311 | reset_button = pn.widgets.Button(name='Zero Voltages') 312 | reset_button.on_click(reset) 313 | return pn.Column(reset_button, bar_button, cross_button, alternating_button, uniform_button, buttons) 314 | 315 | def set_unitary(self, u: Union[np.ndarray, ForwardMesh]): 316 | network = triangular(u) if isinstance(u, np.ndarray) else u 317 | thetas, phis, gammas = network.params 318 | self.set_unitary_phases(np.mod(thetas, 2 * np.pi), np.mod(phis, 2 * np.pi)) 319 | return gammas 320 | 321 | def uhash(self, x: np.ndarray, us: np.ndarray, uc: Optional[np.ndarray] = None): 322 | targets = [] 323 | preds = [] 324 | uc = np.eye(4) if uc is None else uc 325 | for i in range(16): 326 | target = [] 327 | pred = [] 328 | for j in range(16): 329 | v = uc @ np.asarray(x[4 * j:4 * (j + 1)]) 330 | u = us[i, j] @ uc.conj().T 331 | self.set_unitary(u.conj().T) 332 | self.set_input(np.hstack((v, 0))) 333 | time.sleep(0.1) 334 | target.append(np.abs(self.fractional_right[:4])) 335 | pred.append(np.abs(us[i, j] @ v) ** 2) 336 | targets.append(np.hstack(target)) 337 | preds.append(np.hstack(pred)) 338 | 339 | def set_rand_unitary(self): 340 | alphas = np.asarray([1, 2, 1, 3, 2, 1]) 341 | thetas = 2 * np.arccos(np.power(np.random.rand(len(alphas)), 1 / (2 * alphas))) 342 | phis = np.random.rand(len(alphas)) * 2 * np.pi 343 | for ps_loc, phase in zip(self.network['theta_mesh'], thetas): 344 | self.ps[tuple(ps_loc)].phase = phase 345 | for ps_loc, phase in zip(self.network['phi_mesh'], phis): 346 | self.ps[tuple(ps_loc)].phase = phase 347 | 348 | def to_output(self, wait_time: float = 0.2): 349 | """Move stage to the output of the network. 350 | 351 | Args: 352 | wait_time: Wait time after moving to the output (wait extra time for the stage to adjust). 353 | 354 | Returns: 355 | 356 | """ 357 | self.to_layer(LEFT_LAYER if self.backward else RIGHT_LAYER, wait_time=wait_time) 358 | 359 | def u_fidelity(self, u: np.ndarray): 360 | vals = [] 361 | for i in range(4): 362 | self.set_input((np.hstack((u.T[i], 0)))) 363 | time.sleep(0.1) 364 | vals.append(self.fractional_right[i]) 365 | return np.asarray(vals) 366 | 367 | def matvec_comparison(self, u: np.ndarray, v: np.ndarray, wait_time: float = 0.1): 368 | v = v / np.linalg.norm(v) 369 | self.set_input(np.hstack((v, 0))) 370 | self.set_unitary(u.conj().T) 371 | time.sleep(wait_time) 372 | res = self.fractional_right[:4] 373 | actual = np.abs(u @ v) ** 2 374 | return res, actual 375 | 376 | def haar_fidelities(self, n: int = 1000, pbar: Optional[Callable] = None): 377 | self.set_transparent() 378 | self.to_output() 379 | iterator = pbar(range(n)) if pbar is not None else range(n) 380 | return np.asarray([self.u_fidelity(random_unitary(4)) for _ in iterator]) 381 | 382 | def matvec_comparisons(self, n: int = 100, wait_time: float = 0.1, pbar: Optional[Callable] = None): 383 | self.set_transparent() 384 | self.to_output() 385 | iterator = pbar(range(n)) if pbar is not None else range(n) 386 | return np.asarray([self.matvec_comparison(random_unitary(4), 387 | random_vector(4), wait_time) for _ in iterator]) 388 | 389 | def set_unitary_sc(self, u: np.ndarray, show_mesh: bool = False): 390 | t, p = self.network['theta_mesh'], self.network['phi_mesh'] 391 | theta_mesh_list, phi_mesh_list = [[t[0], t[1], t[2]], [t[3], t[4]], t[5:]], \ 392 | [[p[0], p[1], p[2]], [p[3], p[4]], p[5:]] 393 | 394 | self.set_unitary(u) 395 | # only need to shine the first three vectors 396 | for i, vector in enumerate(u.T[:3]): 397 | self.set_input(np.hstack((vector, 0))) 398 | for theta, phi in zip(theta_mesh_list[i], phi_mesh_list[i]): 399 | theta, phi = tuple(theta), tuple(phi) 400 | self.ps[phi].opt_spot(spot=(phi[0], phi[1] + 1), wait_time=0.01, maximize=True) 401 | self.ps[theta].opt_spot(spot=(phi[0], phi[1] + 1), wait_time=0.01, maximize=True) 402 | if show_mesh: 403 | self.update_mesh_image() 404 | 405 | def mesh_panel(self, power_cmap: str = 'hot', ps_cmap: str = 'greens'): 406 | polys = [p.T for multipoly in path_array.flatten() for p in multipoly] 407 | waveguides = hv.Polygons(polys).opts(data_aspect=1, frame_height=100, 408 | ylim=(-10, mesh.size[1] + 10), xlim=(0, mesh.size[0]), 409 | color='black', line_width=2) 410 | phase_shift_polys = [p for p in ps_array] 411 | labels = np.fliplr(np.fliplr(np.mgrid[0:6, 0:19]).reshape((2, -1)).T) 412 | centroids = [np.mean(poly, axis=0) for poly in ps_array] 413 | 414 | text = hv.Overlay([hv.Text(centroid[0], centroid[1] + mzi.interport_distance / 2, 415 | f'{label[0]},{label[1]}', fontsize=7) 416 | for label, centroid in zip(list(labels), centroids) if tuple(label) in self.ps]) 417 | 418 | power_polys = lambda data: hv.Polygons( 419 | [{('x', 'y'): poly, 'power': z} for poly, z in zip(polys, data)], vdims='power' 420 | ) 421 | ps_polys = lambda data: hv.Polygons( 422 | [{('x', 'y'): poly, 'phase_shift': z} for poly, z in zip(phase_shift_polys, data)], vdims='phase_shift' 423 | ) 424 | powers = hv.DynamicMap(power_polys, streams=[self.power_pipe]).opts( 425 | data_aspect=1, frame_height=200, ylim=(-10, mesh.size[1] + 10), 426 | xlim=(0, mesh.size[0]), line_color='none', cmap=power_cmap, shared_axes=False 427 | ) 428 | ps = hv.DynamicMap(ps_polys, streams=[self.ps_pipe]).opts( 429 | data_aspect=1, frame_height=200, ylim=(-10, mesh.size[1] + 10), 430 | xlim=(0, mesh.size[0]), line_color='none', cmap=ps_cmap, shared_axes=False, clim=(0, 2 * np.pi) 431 | ) 432 | self.power_pipe.send(np.full(len(polys), np.nan)) 433 | self.ps_pipe.send(np.full(len(phase_shift_polys), np.nan)) 434 | 435 | def mesh_image(*events): 436 | self.update_mesh_image() 437 | 438 | image_button = pn.widgets.Button(name='Mesh Image') 439 | image_button.on_click(mesh_image) 440 | 441 | def read_output(*events): 442 | self.self_config() 443 | self.update_mesh_image() 444 | 445 | def set_dft(*events): 446 | self.set_transparent() 447 | self.set_dft() 448 | 449 | def dft_basis(i: int): 450 | def f(*events): 451 | self.set_dft_input(i) 452 | 453 | return f 454 | 455 | button_list = [pn.widgets.Button(name=f'dft{i}', width=25) for i in range(4)] 456 | for i, button in enumerate(button_list): 457 | button.on_click(dft_basis(i)) 458 | dft_buttons = pn.Row(*button_list) 459 | 460 | dft_button = pn.widgets.Button(name='Set 4-point DFT') 461 | dft_button.on_click(set_dft) 462 | 463 | read_button = pn.widgets.Button(name='Self-Configure (Read) Output') 464 | read_button.on_click(read_output) 465 | 466 | return pn.Column(ps * waveguides * powers * text, pn.Row(image_button, read_button, dft_button, dft_buttons)) 467 | 468 | def update_mesh_image(self): 469 | powers, spots = self.mesh_img(6) 470 | powers[powers <= 0] = 0 471 | powers = np.flipud(np.sqrt(powers / np.max(powers))) 472 | self.power_pipe.send([p for p, multipoly in zip(powers.flatten(), path_array.flatten()) 473 | for _ in multipoly]) 474 | data = np.zeros((6, 19)) 475 | for loc in self.ps: 476 | data[loc[1], loc[0]] = self.ps[loc].phase 477 | self.ps_pipe.send(np.flipud(data).flatten()) 478 | 479 | def set_transparent(self, bar: bool = True, theta_only: bool = False): 480 | """ 481 | 482 | Args: 483 | bar: 484 | theta_only: 485 | 486 | Returns: 487 | 488 | """ 489 | for ps in self.thetas if theta_only else {**self.thetas, **self.phis}: 490 | self.ps[ps].phase = np.pi if bar else 0 491 | 492 | def calibrate_thetas(self, pbar: Optional[Callable] = None, n_samples=20000, wait_time: float = 0): 493 | """Row-wise calibration of the :math:`\\theta` phase shifters 494 | 495 | Args: 496 | pbar: Progress bar to keep track of each calibration 497 | 498 | Returns: 499 | Voltages used for the calibration and the resulting powers 500 | 501 | """ 502 | self.reset_control() 503 | idx = 0 504 | iterator = self.thetas.values() if pbar is None else pbar(self.thetas.values()) 505 | for ps in iterator: 506 | print(ps.grid_loc) 507 | if ps.spot_loc[1] > idx: 508 | input_ps = tuple(self.network['theta_left'][idx]) 509 | self.ps[input_ps].phase = 0 510 | idx += 1 511 | ps.calibrate(pbar, n_samples=n_samples, wait_time=wait_time) 512 | ps.phase = np.pi 513 | 514 | def calibrate_phis(self, pbar: Optional[Callable] = None, n_samples=20000, wait_time: float = 0): 515 | self.reset_control() 516 | # since the thetas are calibrated 517 | self.set_transparent(theta_only=True) 518 | idx = 0 519 | iterator = self.phis.values() if pbar is None else pbar(self.phis.values()) 520 | for ps in iterator: 521 | print(ps.grid_loc) 522 | if ps.spot_loc[1] > idx: 523 | input_ps = tuple(self.network['theta_left'][idx]) 524 | self.ps[input_ps].phase = 0 525 | idx += 1 526 | ps.calibrate(pbar, n_samples=n_samples, wait_time=wait_time) 527 | ps.phase = 0 528 | 529 | def calibrate_phis_parallel(self, pbar: Optional[Callable] = None, n_samples=20000, wait_time: float = 0): 530 | self.reset_control() 531 | # since the thetas are calibrated 532 | self.set_transparent() 533 | self.set_input(np.array((1, 1, 1, 1, 1))) 534 | iterator = AMF420MESH_CONFIG["phis_parallel"] if pbar is None else pbar(AMF420MESH_CONFIG["phis_parallel"]) 535 | grid_locs = [ps['grid_loc'] for ps in AMF420MESH_CONFIG["phis_parallel"]] 536 | for ps_cfg in iterator: 537 | ps = self.ps[ps_cfg["grid_loc"]] 538 | if ps.grid_loc in grid_locs: 539 | print(ps.grid_loc) 540 | ps.spot_loc = ps_cfg["spot_loc"] 541 | ps.meta_ps = ps_cfg["meta_ps"] 542 | ps.calibrate(pbar, n_samples=n_samples, wait_time=wait_time) 543 | ps.phase = np.pi 544 | 545 | def set_input(self, vector: np.ndarray, add_normalization: bool = False, theta_only: bool = False, 546 | backward: bool = False, override_backward: bool = False): 547 | n = 4 548 | if vector.size == 4: 549 | vector = np.hstack((vector, 0)) 550 | if add_normalization: 551 | vector = vector / np.sqrt(np.sum(np.abs(vector))) * np.sqrt(n / (n + 1)) 552 | vector = np.append(vector, np.sqrt(1 / (n + 1))) 553 | 554 | mesh = unbalanced_tree(vector[::-1].astype(np.complex128)) 555 | thetas, phis, gammas = mesh.params 556 | thetas, phis = np.mod(thetas[::-1], 2 * np.pi), np.mod(phis[::-1], 2 * np.pi) 557 | 558 | backward = backward or (self.backward and not override_backward) 559 | 560 | if backward: 561 | # hack that fixes an unfortunate circuit design error. 562 | phis[1] = np.mod(phis[1] + phis[2], 2 * np.pi) 563 | phis[2] = np.mod(-phis[2], 2 * np.pi) 564 | 565 | for i in range(n): 566 | phase = {'theta': thetas[i], 'phi': phis[i]} 567 | for var in ('theta',) if theta_only else ('theta', 'phi'): 568 | key = f'{var}_right' if backward else f'{var}_left' 569 | self.set_phase(self.network[key][i], phase[var]) 570 | self.set_phase(self.network['theta_ref'], np.pi) 571 | return gammas[-1] 572 | 573 | def set_output_transparent(self): 574 | theta_locs = self.network["theta_left"] if self.backward else self.network["theta_right"] 575 | phi_locs = self.network["phi_left"] if self.backward else self.network["phi_right"] 576 | for theta_loc in theta_locs: 577 | self.ps[theta_loc].phase = np.pi 578 | for phi_loc in phi_locs: 579 | self.ps[phi_loc].phase = np.pi 580 | 581 | def self_config(self, wait_time: float = 0.02): 582 | """Self configure the mesh via nullification output port :code:`idx` by phase measurement (PM). 583 | 584 | This method quickly self configures the mesh such that all the light comes out of the top port. 585 | This is achieved through phase measurement with active phase sensors, which is the fastest such approach 586 | on this chip. 587 | 588 | Args: 589 | wait_time: wait time for the phase measurements 590 | 591 | """ 592 | 593 | theta_locs, phi_locs = self.output_locs 594 | 595 | self.set_output_transparent() 596 | self.to_output() 597 | time.sleep(wait_time) 598 | idxs = np.arange(4) 599 | meas_power = self.fractional_left[:5] 600 | thetas = np.mod(-unbalanced_tree(np.sqrt(np.abs(meas_power).astype(np.complex128))[::-1]).thetas, 2 * np.pi) 601 | for idx, theta_loc, phi_loc in zip(idxs[::-1], theta_locs[::-1], phi_locs[::-1]): 602 | # perform phase measurement 603 | self.ps[theta_loc].phase = np.pi / 2 604 | power = [] 605 | for i in range(2): 606 | self.ps[phi_loc].phase = i * np.pi / 2 607 | time.sleep(wait_time) 608 | power.append(self.fractional_left if self.backward else self.fractional_right) 609 | power = np.asarray(power) 610 | time.sleep(wait_time) 611 | phi = np.arctan2((power[1, idx + 1] - power[1, idx]), 612 | (power[0, idx + 1] - power[0, idx])) 613 | 614 | # perform the nulling operation 615 | self.ps[phi_loc].phase = np.mod(phi, 2 * np.pi) 616 | self.ps[theta_loc].phase = np.mod(thetas[-idx - 1], 2 * np.pi) 617 | 618 | @property 619 | def output_locs(self): 620 | theta_locs = self.network["theta_left"] if self.backward else self.network["theta_right"] 621 | phi_locs = self.network["phi_left"] if self.backward else self.network["phi_right"] 622 | return theta_locs, phi_locs 623 | 624 | @property 625 | def output_from_analyzer(self): 626 | """Extract the output of this chip from the current phases of the analyzer. 627 | 628 | Note: 629 | It is important you run the `self_config` method before running this. 630 | 631 | Args: 632 | coherent_4_alpha: If coherent detection is desired, use an extra dimension. 633 | 634 | Returns: 635 | 636 | """ 637 | theta_locs, phi_locs = self.output_locs 638 | thetas = np.array([self.ps[t].phase for t in theta_locs]) 639 | phis = np.array([self.ps[p].phase for p in phi_locs]) 640 | 641 | if not self.backward: 642 | # hack that fixes an unfortunate circuit design error. 643 | phis[1] = np.mod(phis[1] + phis[2], 2 * np.pi) 644 | phis[2] = np.mod(-phis[2], 2 * np.pi) 645 | 646 | mesh = unbalanced_tree(5) 647 | _, _, gammas = mesh.params 648 | mesh.params = thetas[::-1], phis[::-1], np.zeros_like(gammas) 649 | 650 | return mesh.matrix()[-1][::-1].conj() 651 | 652 | def coherent_batch(self, vs: np.ndarray, wait_time: float = 0.02, coherent_4_alpha: float = 1): 653 | outputs = [] 654 | for v in vs: 655 | self.set_input(np.hstack((v / np.linalg.norm(v), coherent_4_alpha))) 656 | self.self_config(wait_time) 657 | y = self.output_from_analyzer 658 | if coherent_4_alpha != 0: 659 | y = y[:4] / -np.exp(1j * np.angle(y[-1])) 660 | y = y / np.linalg.norm(y) 661 | outputs.append(y * np.linalg.norm(v)) 662 | return np.array(outputs) 663 | 664 | def coherent_matmul(self, u: np.ndarray, v: np.ndarray, coherent_4_alpha: float = 1, wait_time: float = 0.05): 665 | """Coherent matrix multiplication :math:`U \\cdot \\boldsymbol{v}` including all phases. 666 | 667 | Note: 668 | Only compute the full coherent for :math:`N = 4`. The case for :math:`N = 5` misses a global phase lag 669 | though all differential phases can be measured. 670 | 671 | Args: 672 | u: unitary matrix to multiply (4 or 5 dimensional) 673 | v: vector to multiply 674 | coherent_4_alpha: coherent alpha coefficient, the amplitude of the reference channel defined as zero phase. 675 | wait_time: Wait time for the power/phase measurements 676 | 677 | Returns: 678 | A tuple of the measured and expected result. 679 | 680 | """ 681 | if not ((v.shape[0] == 4 or v.shape[0] == 5) and (u.shape == (4, 4) or u.shape == (5, 5))): 682 | raise AttributeError(f'Require v.shape == 4 or 5 and u.shape == (4, 4) or (5, 5) but got ' 683 | f'v.shape == {v.shape} and u.shape == {u.shape}') 684 | norm = np.linalg.norm(v) 685 | expected = u @ v 686 | v = v / norm 687 | 688 | if coherent_4_alpha != 0: 689 | if not (v.shape[0] == 4 and u.shape == (4, 4)): 690 | raise AttributeError(f'Require v.shape == 4 and u.shape == (4, 4) but got ' 691 | f'v.shape == {v.shape} and u.shape == {u.shape}') 692 | v = np.hstack((v, coherent_4_alpha)) 693 | v = v / np.linalg.norm(v) 694 | 695 | self.set_input(v) 696 | gammas = self.set_unitary(u) 697 | self.self_config(wait_time) 698 | y = self.output_from_analyzer 699 | 700 | if coherent_4_alpha != 0: 701 | y = y[:4] / -np.exp(1j * np.angle(y[-1])) 702 | y = y / np.linalg.norm(y) 703 | 704 | # the reference phases need to be dealt with since they are not implemented on-chip 705 | return y * np.exp(1j * gammas) * norm, expected 706 | 707 | def set_output(self, vector: np.ndarray): 708 | return self.set_input(vector, backward=not self.backward, override_backward=True) 709 | 710 | def set_phase(self, ps, phase): 711 | self.ps[tuple(ps)].phase = phase 712 | 713 | @property 714 | def phases(self): 715 | return {loc: self.ps[loc].phase for loc in self.ps} 716 | 717 | def calibrate_panel(self, vlim: Tuple[float, float] = (0.5, 4.5)): 718 | vs = np.sqrt(np.linspace(vlim[0] ** 2, vlim[1] ** 2, 20000)) 719 | ps_dropdown = pn.widgets.Select( 720 | name="Phase Shifter", options=[f"{ps[0]}, {ps[1]}" for ps in self.ps], value="1, 1" 721 | ) 722 | calibrated_values = pn.Row( 723 | hv.Overlay([hv.Curve((ps.v2p(vs) / np.pi * 2, vs)) for _, ps in self.ps.items()]).opts( 724 | xlabel='Phase (θ)', ylabel='Voltage (V)').opts(shared_axes=False, title='Calibration Curves', 725 | xformatter='%fπ'), 726 | hv.Overlay([hv.Curve((vs, ps.v2p(vs) / np.pi * 2)) for _, ps in self.ps.items()]).opts( 727 | ylabel='Phase (θ)', xlabel='Voltage (V)').opts(shared_axes=False, yformatter='%fπ') 728 | ) 729 | 730 | def to_layer(*events): 731 | ps_tuple = tuple([int(c) for c in ps_dropdown.value.split(', ')]) 732 | self.to_layer(self.ps[ps_tuple].grid_loc[0]) 733 | 734 | @pn.depends(ps_dropdown.param.value) 735 | def calibration_image(value): 736 | ps_tuple = tuple([int(c) for c in value.split(', ')]) 737 | p = self.ps[ps_tuple].calibration 738 | vs_cal = np.sqrt(np.linspace(vlim[0] ** 2, vlim[1] ** 2, len(p.upper_split_ratio))) 739 | if p is None: 740 | raise ValueError(f'Expected calibration field in phase shifter {ps_tuple} but got None.') 741 | return pn.Column( 742 | hv.Overlay([ 743 | hv.Curve((vs_cal ** 2, p.upper_split_ratio), label='upper split'), 744 | hv.Curve((vs_cal ** 2, p.lower_split_ratio), label='lower split'), 745 | hv.Curve((vs_cal ** 2, p.split_ratio_fit), label='lower split fit').opts( 746 | opts.Curve(line_dash='dashed')), 747 | hv.Curve((vs_cal ** 2, p.upper_out), label='upper out'), 748 | hv.Curve((vs_cal ** 2, p.lower_out), label='lower out'), 749 | hv.Curve((vs_cal ** 2, p.upper_arm), label='upper arm'), 750 | hv.Curve((vs_cal ** 2, p.lower_arm), label='lower arm'), 751 | hv.Curve((vs_cal ** 2, p.total_arm), label='total arm'), 752 | hv.Curve((vs_cal ** 2, p.total_out), label='total out') 753 | ]).opts(width=800, height=400, legend_position='right', shared_axes=False, 754 | title='MZI Inspection Curves', xlabel='Electrical Power (Vsqr)', ylabel='Recorded Values'), 755 | ) 756 | 757 | def abs_phase(phase: float): 758 | def f(*events): 759 | ps = tuple([int(c) for c in ps_dropdown.value.split(', ')]) 760 | self.set_phase(ps, phase) 761 | 762 | return f 763 | 764 | def invert(*events): 765 | ps = tuple([int(c) for c in ps_dropdown.value.split(', ')]) 766 | self.set_phase(ps, 2 * np.pi - self.ps[ps].phase) 767 | 768 | def rel_phase(phase_change: float): 769 | def f(*events): 770 | ps = tuple([int(c) for c in ps_dropdown.value.split(', ')]) 771 | self.set_phase(ps, np.mod(self.ps[ps].phase + phase_change, 2 * np.pi)) 772 | 773 | return f 774 | 775 | to_layer_button = pn.widgets.Button(name='To PS Layer') 776 | to_layer_button.on_click(to_layer) 777 | 778 | button_function_pairs = [ 779 | (pn.widgets.Button(name='0', width=40), abs_phase(0)), 780 | (pn.widgets.Button(name='π/2', width=40), abs_phase(np.pi / 2)), 781 | (pn.widgets.Button(name='π', width=40), abs_phase(np.pi)), 782 | (pn.widgets.Button(name='3π/2', width=40), abs_phase(3 * np.pi / 2)), 783 | (pn.widgets.Button(name='-θ', width=40), invert), 784 | (pn.widgets.Button(name='+π', width=40), rel_phase(np.pi)), 785 | (pn.widgets.Button(name='-π', width=40), rel_phase(-np.pi)), 786 | (pn.widgets.Button(name='+π/2', width=40), rel_phase(np.pi / 2)), 787 | (pn.widgets.Button(name='-π/2', width=40), rel_phase(-np.pi / 2)) 788 | ] 789 | 790 | for b, f in button_function_pairs: 791 | b.on_click(f) 792 | 793 | buttons = [b[0] for b in button_function_pairs] 794 | 795 | return pn.Column( 796 | ps_dropdown, 797 | pn.Row(*buttons), 798 | to_layer_button, 799 | calibration_image, 800 | calibrated_values, 801 | ) 802 | 803 | def hessian_test(self, delta: float = 0.1): 804 | u = unitary_group.rvs(4) 805 | self.set_unitary(u) 806 | self.set_input(np.array((*u.T[-1], 0))) 807 | phase_shift_locs = ((5, 0), (7, 1), (9, 2), (4, 0), (6, 1), (8, 2)) 808 | measurements = [] 809 | for i in phase_shift_locs: 810 | for j in phase_shift_locs: 811 | self.ps[i].phase += delta 812 | self.ps[j].phase += delta 813 | time.sleep(0.1) 814 | measurements.append(self.fractional_right) 815 | self.ps[j].phase -= 2 * delta 816 | time.sleep(0.1) 817 | measurements.append(self.fractional_right) 818 | 819 | def get_unitary_phases(self): 820 | ts = [self.ps[tuple(t)].phase for t in self.network['theta_mesh']] 821 | ps = [self.ps[tuple(p)].phase for p in self.network['phi_mesh']] 822 | return ts, ps 823 | 824 | def set_unitary_phases(self, ts, ps): 825 | for t, th in zip(self.network['theta_mesh'], ts): 826 | self.ps[tuple(t)].phase = np.mod(th, 2 * np.pi) 827 | for p, ph in zip(self.network['phi_mesh'], ps): 828 | self.ps[tuple(p)].phase = np.mod(ph, 2 * np.pi) 829 | 830 | def to_calibration_file(self, filename: str): 831 | with open(filename, 'wb') as f: 832 | pickle.dump({ps.grid_loc: ps.calibration.dict for loc, ps in self.ps.items()}, f) 833 | 834 | def calibrate_all(self, pbar: Optional[Callable] = None): 835 | self.calibrate_thetas(pbar) 836 | self.calibrate_phis(pbar) 837 | 838 | def power_panel(self, use_pd: bool = False): 839 | def power_bars(data): 840 | return hv.Bars(data, hv.Dimension('Port'), 'Fractional power').opts(ylim=(0, 1)) 841 | dmap = hv.DynamicMap(power_bars, streams=[self.spot_pipe]).opts(shared_axes=False) 842 | power_toggle = pn.widgets.Toggle(name='Power', value=False) 843 | lr_toggle = pn.widgets.Toggle(name='Left Spots', value=False) 844 | 845 | if not use_pd: 846 | @gen.coroutine 847 | def update_plot(): 848 | self.spot_pipe.send([(i, p) 849 | for i, p in enumerate(self.fractional_left 850 | if lr_toggle.value else self.fractional_right) 851 | ]) 852 | else: 853 | @gen.coroutine 854 | def update_plot(): 855 | self.spot_pipe.send([(i, p) for i, p in enumerate(self.output_pd)]) 856 | cb = PeriodicCallback(update_plot, 100) 857 | 858 | def change_power(*events): 859 | for event in events: 860 | if event.name == 'value': 861 | self.power_det_on = bool(event.new) 862 | if self.power_det_on: 863 | cb.start() 864 | else: 865 | cb.stop() 866 | 867 | power_toggle.param.watch(change_power, 'value') 868 | 869 | return pn.Column(dmap, power_toggle, lr_toggle) 870 | 871 | @property 872 | def fractional_right(self): 873 | return self.camera.spot_powers[::3] / np.sum(self.camera.spot_powers[::3]) 874 | 875 | @property 876 | def fractional_left(self): 877 | return self.camera.spot_powers[2::3] / np.sum(self.camera.spot_powers[2::3]) 878 | 879 | @property 880 | def fractional_center(self): 881 | return self.camera.spot_powers[1::3] / np.sum(self.camera.spot_powers[1::3]) 882 | 883 | @property 884 | def output_pd(self): 885 | raw = np.mean(self.control.meas_buffer, axis=1) 886 | fit = np.abs(np.array([np.polyval(self.pd_params[i], raw[i]) for i in range(4)])) 887 | return fit / np.sum(fit) 888 | 889 | def pd_calibrate(self, pbar=None): 890 | self.set_transparent() 891 | calibration_values = [] 892 | ps = np.linspace(0, 1, 100) 893 | for i in range(4): 894 | calibration_values_i = [] 895 | time.sleep(2) 896 | iterator = ps if pbar is None else pbar(ps) 897 | for p in iterator: 898 | self.set_input(np.sqrt(np.hstack((np.eye(4)[i] * p, 1 - p)))) 899 | time.sleep(0.1) 900 | calibration_values_i.append(np.mean(self.control.meas_buffer, axis=1)) 901 | calibration_values.append(calibration_values_i) 902 | self.pd_calibration = np.array(calibration_values).transpose((0, 2, 1)) 903 | self.pd_params = [np.polyfit(self.pd_calibration[i, i], np.linspace(0, 1, 100), 5) for i in range(4)] 904 | 905 | def livestream_panel(self, cmap: float = 'hot'): 906 | def change_tuple(name: str, index: int): 907 | def change_value(*events): 908 | for event in events: 909 | if event.name == 'value': 910 | if name == "spot_rowcol": 911 | src = list(self.spot_rowcol) 912 | src[index] = event.new 913 | self.spot_rowcol = tuple(src) 914 | if name == "window_dim": 915 | wd = list(self.window_dim) 916 | wd[index] = event.new 917 | self.window_dim = tuple(wd) 918 | if name == "interlayer_xy": 919 | inter_xy = list(self.interlayer_xy) 920 | inter_xy[index] = event.new 921 | self.interlayer_xy = tuple(inter_xy) 922 | if name == "interspot_xy": 923 | inters_xy = list(self.interspot_xy) 924 | inters_xy[index] = event.new 925 | self.interspot_xy = tuple(inters_xy) 926 | s = self.spot_rowcol 927 | ixy = self.interspot_xy 928 | self.spots = [(int(j * ixy[0] + s[0]), 929 | int(i * ixy[1] + s[1]), 930 | self.window_dim[0], self.window_dim[1]) 931 | for j in range(6) for i in range(3)] 932 | self.camera.spots = self.spots 933 | self.camera.spots_indicator_pipe.send(self.camera.spots) 934 | return change_value 935 | 936 | 937 | spot_row = pn.widgets.IntInput(name='Initial Spot Row', value=self.spot_rowcol[0], 938 | step=1, start=0, end=640) 939 | spot_row.param.watch(change_tuple("spot_rowcol", 0), 'value') 940 | spot_col = pn.widgets.IntInput(name='Initial Spot Col', value=self.spot_rowcol[1], 941 | step=1, start=0, end=512) 942 | spot_col.param.watch(change_tuple("spot_rowcol", 1), 'value') 943 | 944 | window_height = pn.widgets.IntInput(name='Initial Window Height', value=self.window_dim[0], 945 | step=1, start=0, end=50) 946 | window_height.param.watch(change_tuple("window_dim", 0), 'value') 947 | window_width = pn.widgets.IntInput(name='Initial Window Width', value=self.window_dim[1], 948 | step=1, start=15, end=50) 949 | window_width.param.watch(change_tuple("window_dim", 1), 'value') 950 | 951 | interlayer_x = pn.widgets.FloatInput(name='Initial Interlayer X {mm}', value=self.interlayer_xy[0], 952 | step=.0001, start=-.01, end=.01) 953 | interlayer_x.param.watch(change_tuple("interlayer_xy", 0), 'value') 954 | interlayer_y = pn.widgets.FloatInput(name='Initial Interlayer Y {mm}', value=self.interlayer_xy[1], 955 | step=.0001, start=-.5, end=.5) 956 | interlayer_y.param.watch(change_tuple("interlayer_xy", 1), 'value') 957 | 958 | interspot_y = pn.widgets.IntInput(name='Initial Interspot Y {mm}', value=self.interspot_xy[0], 959 | step=1, start=-100, end=100) 960 | interspot_y.param.watch(change_tuple("interspot_xy", 0), 'value') 961 | interspot_x = pn.widgets.IntInput(name='Initial Interspot X {mm}', value=self.interspot_xy[1], 962 | step=1, start=-400, end=400) 963 | interspot_x.param.watch(change_tuple("interspot_xy", 1), 'value') 964 | return pn.Row(self.camera.livestream_panel(cmap=cmap), pn.Column(spot_row, spot_col, window_width, window_height, interlayer_x, interlayer_y, interspot_x, interspot_y)) 965 | 966 | def livestream_panel(self, cmap: float = 'hot'): 967 | def change_tuple(name: str, index: int): 968 | def change_value(*events): 969 | for event in events: 970 | if event.name == 'value': 971 | if name == "spot_rowcol": 972 | src = list(self.spot_rowcol) 973 | src[index] = event.new 974 | self.spot_rowcol = tuple(src) 975 | if name == "window_dim": 976 | wd = list(self.window_dim) 977 | wd[index] = event.new 978 | self.window_dim = tuple(wd) 979 | if name == "interlayer_xy": 980 | inter_xy = list(self.interlayer_xy) 981 | inter_xy[index] = event.new 982 | self.interlayer_xy = tuple(inter_xy) 983 | if name == "interspot_xy": 984 | inters_xy = list(self.interspot_xy) 985 | inters_xy[index] = event.new 986 | self.interspot_xy = tuple(inters_xy) 987 | s = self.spot_rowcol 988 | ixy = self.interspot_xy 989 | self.spots = [(int(j * ixy[0] + s[0]), 990 | int(i * ixy[1] + s[1]), 991 | self.window_dim[0], self.window_dim[1]) 992 | for j in range(6) for i in range(3)] 993 | self.camera.spots = self.spots 994 | self.camera.spots_indicator_pipe.send(self.camera.spots) 995 | return change_value 996 | 997 | 998 | spot_row = pn.widgets.IntInput(name='Initial Spot Row', value=self.spot_rowcol[0], 999 | step=1, start=0, end=640) 1000 | spot_row.param.watch(change_tuple("spot_rowcol", 0), 'value') 1001 | spot_col = pn.widgets.IntInput(name='Initial Spot Col', value=self.spot_rowcol[1], 1002 | step=1, start=0, end=640) 1003 | spot_col.param.watch(change_tuple("spot_rowcol", 1), 'value') 1004 | 1005 | window_height = pn.widgets.IntInput(name='Initial Window Height', value=self.window_dim[0], 1006 | step=1, start=0, end=50) 1007 | window_height.param.watch(change_tuple("window_dim", 0), 'value') 1008 | window_width = pn.widgets.IntInput(name='Initial Window Width', value=self.window_dim[1], 1009 | step=1, start=15, end=50) 1010 | window_width.param.watch(change_tuple("window_dim", 1), 'value') 1011 | 1012 | interlayer_x = pn.widgets.FloatInput(name='Initial Interlayer X {mm}', value=self.interlayer_xy[0], 1013 | step=.0001, start=-.5, end=.5) 1014 | interlayer_x.param.watch(change_tuple("interlayer_xy", 0), 'value') 1015 | interlayer_y = pn.widgets.FloatInput(name='Initial Interlayer Y {mm}', value=self.interlayer_xy[1], 1016 | step=.0001, start=-.5, end=.5) 1017 | interlayer_y.param.watch(change_tuple("interlayer_xy", 1), 'value') 1018 | 1019 | interspot_y = pn.widgets.IntInput(name='Initial Interspot Y {mm}', value=self.interspot_xy[0], 1020 | step=1, start=-100, end=100) 1021 | interspot_y.param.watch(change_tuple("interspot_xy", 0), 'value') 1022 | interspot_x = pn.widgets.IntInput(name='Initial Interspot X {mm}', value=self.interspot_xy[1], 1023 | step=1, start=-400, end=400) 1024 | interspot_x.param.watch(change_tuple("interspot_xy", 1), 'value') 1025 | return pn.Row(self.camera.livestream_panel(cmap=cmap), pn.Column(spot_row, spot_col, window_width, window_height, interlayer_x, interlayer_y, interspot_x, interspot_y)) 1026 | 1027 | def default_panel(self): 1028 | mesh_panel = self.mesh_panel() 1029 | livestream_panel = self.livestream_panel(cmap='gray') 1030 | move_panel = self.stage.move_panel() 1031 | power_panel = self.laser.power_panel() 1032 | spot_panel = self.power_panel() # use_pd=(self.pd_calibration is not None) 1033 | wavelength_panel = self.laser.wavelength_panel() 1034 | led_panel = self.led_panel() 1035 | home_panel = self.home_panel() 1036 | propagation_toggle_panel = self.propagation_toggle_panel() 1037 | input_panel = self.input_panel() 1038 | return pn.Column( 1039 | pn.Pane(pn.Row(mesh_panel, input_panel), name='Mesh'), 1040 | pn.Row(pn.Tabs( 1041 | ('Live Interface', 1042 | pn.Column( 1043 | pn.Row(livestream_panel, 1044 | pn.Column(move_panel, home_panel, power_panel, 1045 | wavelength_panel, led_panel, propagation_toggle_panel) 1046 | ) 1047 | )), 1048 | ('Calibration Panel', self.calibrate_panel()) 1049 | ), spot_panel) 1050 | ) 1051 | 1052 | def svd_opow(self, q: np.ndarray, x: np.ndarray, p: int = 0, wait_time: float = 0.05, measure_signed: bool = True): 1053 | """ SVD calculation that uses photonic chip only for unitary matmuls for optical proof of work (oPoW). 1054 | The error is reduced since only powers from four waveguides need to be measured. 1055 | Signs of the first matmul are determined using coherent measurement and are usually correct. 1056 | 1057 | Args: 1058 | q: The complex matrix to multiply by :math:`\\boldsymbol{x}`. 1059 | x: The vector to multiply by :math:`Q`. 1060 | p: Cyclic shift for hardware-agnostic error correction. 1061 | wait_time: Wait time for the SVD amplitude measurements. 1062 | measure_signed: Use the sign rather than the measured phase for the SVD measurement 1063 | 1064 | Returns: 1065 | A tuple of the absolute value of the SVD result along with the expected result 1066 | 1067 | """ 1068 | u, d, v = svd(q) 1069 | if p > 0: 1070 | v = np.roll(v, p) 1071 | u = np.roll(u, p, axis=1) 1072 | d = np.roll(d, p) 1073 | self.set_unitary(v) 1074 | self.set_input(np.array(x)) 1075 | time.sleep(wait_time) 1076 | res = np.sqrt(np.abs(self.fractional_right[:4])) * np.linalg.norm(x) 1077 | if measure_signed: # use coherent measurement to measure signs 1078 | sign = np.sign(np.real(self.coherent_matmul(v, x, wait_time=wait_time)[0])) 1079 | else: 1080 | sign = np.sign(np.real(v @ x)) 1081 | res = res * d * sign 1082 | self.set_unitary(u) 1083 | self.set_input(res) 1084 | self.set_output_transparent() 1085 | time.sleep(wait_time) 1086 | return np.linalg.norm(res) * np.sqrt(np.abs(self.fractional_right[:4])), np.abs(q @ x) 1087 | 1088 | def svd(self, q: np.ndarray, x: np.ndarray, p: int = 0, wait_time: float = 0.05): 1089 | """ SVD calculation that uses photonic chip for amplitude AND phase measurements (less accurate). 1090 | 1091 | Args: 1092 | q: The complex matrix to multiply by :math:`\\boldsymbol{x}`. 1093 | x: The vector to multiply by :math:`Q`. 1094 | p: For hardware-agnostic error correction. 1095 | wait_time: Wait time for all measurements (amplitude and phase calculations). 1096 | 1097 | Returns: 1098 | 1099 | """ 1100 | u, d, v = svd(q) 1101 | if p > 0: 1102 | v = np.roll(v, p) 1103 | u = np.roll(u, p, axis=1) 1104 | d = np.roll(d, p) 1105 | y = self.coherent_matmul(v, x, wait_time=wait_time)[0] 1106 | y = np.abs(y) * d * np.sign(y) 1107 | r = self.coherent_matmul(u, y, wait_time=wait_time)[0] 1108 | return r, q @ x 1109 | 1110 | def matrix_prop(self, vs: np.ndarray, wait_time: float = 0.03, move_pause: float = 0.2): 1111 | prop_data = [] 1112 | for col in (4, 7, 10, 13, 16): 1113 | self.to_layer(col) 1114 | prop_col_data = [] 1115 | time.sleep(move_pause) 1116 | for v in vs: 1117 | self.set_input(v) 1118 | time.sleep(wait_time) 1119 | prop_col_data.append(np.stack([ 1120 | self.fractional_left, 1121 | self.fractional_center, 1122 | self.fractional_right 1123 | ]) * np.linalg.norm(v) ** 2) 1124 | prop_data.append(np.stack(prop_col_data)) 1125 | prop_data = np.hstack(prop_data) 1126 | return prop_data.transpose((1, 0, 2)) 1127 | 1128 | def set_dft(self): 1129 | self.set_unitary(dft(4)) 1130 | 1131 | def set_dft_input(self, i: int = 0): 1132 | self.set_input(dft(4)[i].conj()) 1133 | 1134 | 1135 | class PhaseShifter: 1136 | def __init__(self, grid_loc: Tuple[int, int], spot_loc: Tuple[int, int], 1137 | voltage_channel: int, mesh: AMF420Mesh, 1138 | meta_ps: Optional[Tuple[Tuple[int, int], Tuple[int, int]]] = None, 1139 | calibration: Optional[PhaseCalibration] = None): 1140 | self.grid_loc = tuple(grid_loc) 1141 | self.spot_loc = tuple(spot_loc) 1142 | self.voltage_channel = voltage_channel 1143 | self.meta_ps = [] if meta_ps is None else meta_ps 1144 | self.mesh = mesh 1145 | self.calibration = calibration 1146 | self._phase = np.pi 1147 | 1148 | def calibrate(self, pbar: Optional[Callable] = None, vlim: Tuple[float, float] = (1, 5), 1149 | p0: Tuple[float, ...] = (1, 0, -0.01, 0.3, 0, 0), n_samples: int = 20000, 1150 | wait_time: float = 0): 1151 | """Calibrate the phase shifter, setting calibration object to a new PhaseCalibration 1152 | 1153 | Args: 1154 | pbar: Progress bar to keep track of the calibration time 1155 | vlim: Voltage limits 1156 | p0: Fit function initial value 1157 | n_samples: Number of samples 1158 | 1159 | Returns: 1160 | 1161 | """ 1162 | 1163 | layer, idx = self.spot_loc 1164 | logger.info(f'Calibration of phase shifter {self.grid_loc} at spot {self.spot_loc}') 1165 | for t in self.meta_ps: 1166 | self.mesh.set_phase(t, np.pi / 2) 1167 | vs, powers = self.mesh.sweep(self.voltage_channel, wait_time=wait_time, 1168 | layer=layer, vlim=vlim, n_samples=n_samples, pbar=pbar) 1169 | for t in self.meta_ps: 1170 | self.mesh.set_phase(t, np.pi) 1171 | try: 1172 | self.calibration = PhaseCalibration(vs, powers, self.spot_loc, p0=p0) 1173 | except RuntimeError: 1174 | self.calibrate(pbar, vlim, p0, n_samples * 2, wait_time) 1175 | 1176 | def v2p(self, voltage: float): 1177 | """Voltage to phase conversion for a give phase shifter 1178 | 1179 | Args: 1180 | voltage: voltage to convert 1181 | 1182 | Returns: 1183 | Phase converted from voltage 1184 | 1185 | """ 1186 | return self.calibration.v2p(voltage) 1187 | 1188 | def p2v(self, phase: float): 1189 | """Phase to voltage conversion for a give phase shifter 1190 | 1191 | Args: 1192 | phase: phase to convert 1193 | 1194 | Returns: 1195 | Voltage converted from phase 1196 | 1197 | """ 1198 | return self.calibration.p2v(phase) 1199 | 1200 | @property 1201 | def dict(self): 1202 | return { 1203 | 'grid_loc': self.grid_loc, 1204 | 'spot_loc': self.spot_loc, 1205 | 'voltage_channel': self.voltage_channel, 1206 | 'meta_ps': self.meta_ps 1207 | } 1208 | 1209 | @property 1210 | def phase(self): 1211 | return self._phase 1212 | 1213 | @phase.setter 1214 | def phase(self, phase: float): 1215 | """Set the phase shifter in radians 1216 | 1217 | Args: 1218 | phase: phase shift in the range [0, 2 * pi) 1219 | 1220 | Returns: 1221 | 1222 | """ 1223 | self.mesh.control.write_chan(self.voltage_channel, self.p2v(np.mod(phase, 2 * np.pi))) 1224 | self._phase = phase 1225 | 1226 | def reset(self): 1227 | """Reset the phase voltage to 0V and set the phase shift to np.nan 1228 | 1229 | Args: 1230 | 1231 | Returns: 1232 | 1233 | """ 1234 | # offset in temperature to reduce instability during calibration 1235 | self.mesh.control.write_chan(self.voltage_channel, 2) 1236 | self._phase = np.nan # there might not be a phase defined here, so we just treat it as nan. 1237 | 1238 | def opt_spot(self, spot: Optional[Tuple[int, int]] = None, guess_phase: float = None, 1239 | wait_time: float = 0, n_samples: int = 100, 1240 | pbar: Optional[Callable] = None, move: bool = True, maximize: bool = False): 1241 | """Maximize the power at a spot by sweeping the phase shift voltage 1242 | 1243 | Args: 1244 | spot: Spot to minimize power 1245 | wait_time: Wait time between samples 1246 | n_samples: Number of samples 1247 | pbar: Progress bar handle 1248 | move: Whether to move the stage (to save time, set to false if the stage doesn't move) 1249 | maximize: Whether to maximize the power or minimize the power 1250 | 1251 | Returns: 1252 | 1253 | """ 1254 | layer, idx = self.spot_loc if spot is None else spot 1255 | if move: 1256 | self.mesh.to_layer(layer) 1257 | min_phase = 0 if guess_phase is None else np.maximum(0, guess_phase - 0.1) 1258 | max_phase = 0 if guess_phase is None else np.minimum(2 * np.pi, guess_phase + 0.1) 1259 | phases = np.linspace(0, 2 * np.pi, n_samples) if guess_phase is None else np.linspace(min_phase, max_phase) 1260 | iterator = pbar(phases) if pbar is not None else phases 1261 | self.phase = guess_phase 1262 | time.sleep(1) 1263 | powers = [] 1264 | for phase in iterator: 1265 | self.phase = phase 1266 | time.sleep(wait_time) 1267 | p = self.mesh.camera.spot_powers 1268 | powers.append(p[3 * idx] / (p[3 * idx] + p[3 * idx - 3])) 1269 | opt_ps = phases[np.argmax(powers) if maximize else np.argmin(powers)] 1270 | self.phase = opt_ps 1271 | return opt_ps, powers 1272 | --------------------------------------------------------------------------------