{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "506a3620-d09f-4de8-aad7-3ff889842f01",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T22:06:43.415546Z",
     "iopub.status.busy": "2022-05-23T22:06:43.414756Z",
     "iopub.status.idle": "2022-05-23T22:06:43.434380Z",
     "shell.execute_reply": "2022-05-23T22:06:43.433797Z",
     "shell.execute_reply.started": "2022-05-23T22:06:43.415488Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "import ipywidgets\n",
    "import batoid\n",
    "import numpy as np\n",
    "import ipyvolume as ipv\n",
    "from functools import lru_cache\n",
    "import matplotlib.pyplot as plt\n",
    "import galsim\n",
    "from IPython.display import display\n",
    "\n",
    "%matplotlib widget"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "149722ac-b76b-43f8-97aa-21aea2cbd19b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T22:06:43.581900Z",
     "iopub.status.busy": "2022-05-23T22:06:43.581227Z",
     "iopub.status.idle": "2022-05-23T22:06:43.602845Z",
     "shell.execute_reply": "2022-05-23T22:06:43.602455Z",
     "shell.execute_reply.started": "2022-05-23T22:06:43.581874Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "@lru_cache\n",
    "def get_constellations_xyz():\n",
    "    from astropy.io import fits\n",
    "    try:\n",
    "        xyz = fits.getdata(\"constellations.fits\")\n",
    "    except:    \n",
    "        from astroquery.simbad import Simbad\n",
    "\n",
    "        def ten(s):\n",
    "            ss = s.split()\n",
    "            h = float(ss[0])\n",
    "            sign = +1\n",
    "            if h < 0:\n",
    "                sign = -1\n",
    "                h *= -1\n",
    "            m = float(ss[1])\n",
    "            s = float(ss[2])\n",
    "            return sign * (h + m/60 + s/3600)        \n",
    "        \n",
    "        Simbad.add_votable_fields('typed_id')\n",
    "        HIPset = set()\n",
    "        with open(\"constellationship.fab\") as f:\n",
    "            lines = f.readlines()\n",
    "        for line in lines:\n",
    "            HIPset.update([int(s) for s in line.split()[2:]])\n",
    "        HIPlist = list(HIPset)\n",
    "        table = Simbad.query_objects(\n",
    "            [f\"HIP {s}\" for s in HIPlist]\n",
    "        )\n",
    "        table['HIPID'] = HIPlist\n",
    "\n",
    "        xs = []\n",
    "        ys = []\n",
    "        zs = []\n",
    "        for line in lines:\n",
    "            xs.append(np.nan)\n",
    "            ys.append(np.nan)\n",
    "            zs.append(np.nan)\n",
    "            prev_second = -1\n",
    "            endpoints = iter(line.split()[2:])\n",
    "            for first in endpoints:\n",
    "                second = next(endpoints)\n",
    "                first = int(first)\n",
    "                second = int(second)\n",
    "\n",
    "                secondrow = table[np.nonzero(table['HIPID'] == int(second))]\n",
    "                ra1 = np.deg2rad(15*ten(secondrow['RA'][0]))\n",
    "                dec1 = np.deg2rad(ten(secondrow['DEC'][0]))\n",
    "                x1 = np.cos(ra1)*np.cos(dec1)\n",
    "                y1 = np.sin(ra1)*np.cos(dec1)\n",
    "                z1 = np.sin(dec1)\n",
    "\n",
    "                if first == prev_second:\n",
    "                    # just append new second\n",
    "                    xs.append(x1)\n",
    "                    ys.append(y1)\n",
    "                    zs.append(z1)\n",
    "                else:                    \n",
    "                    firstrow = table[np.nonzero(table['HIPID'] == int(first))]\n",
    "                    ra0 = np.deg2rad(15*ten(firstrow['RA'][0]))\n",
    "                    dec0 = np.deg2rad(ten(firstrow['DEC'][0]))\n",
    "                    x0 = np.cos(ra0)*np.cos(dec0)\n",
    "                    y0 = np.sin(ra0)*np.cos(dec0)\n",
    "                    z0 = np.sin(dec0)\n",
    "                    xs.extend([np.nan, x0, x1])\n",
    "                    ys.extend([np.nan, y0, y1])\n",
    "                    zs.extend([np.nan, z0, z1])\n",
    "                prev_second = second\n",
    "        xyz = np.array([xs, ys, zs])\n",
    "        fits.writeto(\"constellations.fits\", xyz)\n",
    "    return xyz\n",
    "\n",
    "@lru_cache\n",
    "def get_stars_xyzs():    \n",
    "    #http://tdc-www.harvard.edu/catalogs/bsc5.dat.gz\n",
    "    from astropy.io import fits\n",
    "    table = fits.getdata(\"BSC5.fits\")\n",
    "    table = table[table['mag'] < 5.5]\n",
    "    x = np.cos(table['ra']) * np.cos(table['dec'])\n",
    "    y = np.sin(table['ra']) * np.cos(table['dec'])\n",
    "    z = np.sin(table['dec'])    \n",
    "    return x, y, z, 10**(3-0.2*table['mag'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "66e15eb4-2251-4b20-bb31-26ede81584de",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T22:06:43.750729Z",
     "iopub.status.busy": "2022-05-23T22:06:43.750445Z",
     "iopub.status.idle": "2022-05-23T22:06:43.759219Z",
     "shell.execute_reply": "2022-05-23T22:06:43.758705Z",
     "shell.execute_reply.started": "2022-05-23T22:06:43.750705Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "def get_zk(opd):\n",
    "    xs = np.linspace(-1, 1, opd.shape[0])\n",
    "    ys = np.linspace(-1, 1, opd.shape[1])\n",
    "    xs, ys = np.meshgrid(xs, ys)\n",
    "    w = ~opd.mask\n",
    "    basis = galsim.zernike.zernikeBasis(22, xs[w], ys[w], R_inner=0.61)\n",
    "    zk, *_ = np.linalg.lstsq(basis.T, opd[w], rcond=None)\n",
    "    return zk\n",
    "\n",
    "def sub_ptt(opd):\n",
    "    xs = np.linspace(-1, 1, opd.shape[0])\n",
    "    ys = np.linspace(-1, 1, opd.shape[1])\n",
    "    xs, ys = np.meshgrid(xs, ys)\n",
    "    zk = get_zk(opd)\n",
    "    opd -= galsim.zernike.Zernike(zk[:4], R_inner=0.61)(xs, ys)\n",
    "    return opd"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "d3da8603-1e0f-49ab-9347-705e9dca0b11",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T22:06:44.186478Z",
     "iopub.status.busy": "2022-05-23T22:06:44.186151Z",
     "iopub.status.idle": "2022-05-23T22:06:44.235022Z",
     "shell.execute_reply": "2022-05-23T22:06:44.234546Z",
     "shell.execute_reply.started": "2022-05-23T22:06:44.186452Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "ename": "SyntaxError",
     "evalue": "invalid syntax (3903807444.py, line 483)",
     "output_type": "error",
     "traceback": [
      "\u001b[0;36m  Input \u001b[0;32mIn [12]\u001b[0;36m\u001b[0m\n\u001b[0;31m    def update_cs(self, cs):\u001b[0m\n\u001b[0m    ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
     ]
    }
   ],
   "source": [
    "debug = ipywidgets.Output()\n",
    "\n",
    "class RubinCSApp:\n",
    "    def __init__(self, sky_dist=15000):\n",
    "        self.sky_dist = sky_dist\n",
    "        self.lat = -30.2446\n",
    "        self.fiducial_telescope = batoid.Optic.fromYaml(\"LSST_r.yaml\")\n",
    "        self.actual_telescope = self.fiducial_telescope # for now\n",
    "        \n",
    "        # widget variables\n",
    "        self.clip_horizon = False\n",
    "        self.lst = 0.0\n",
    "        self.rtp = 0.0\n",
    "        self.alt = 45.0\n",
    "        self.az = 45.0\n",
    "        self.thx = 0.0\n",
    "        self.thy = 0.0\n",
    "        self.show_CCS = False\n",
    "        self.show_OCS = False\n",
    "        self.show_ZCS = False\n",
    "        self.show_DVCS = False\n",
    "        self.show_EDCS = False\n",
    "\n",
    "        # Scatters\n",
    "        self.constellations = self._constellations_widget()\n",
    "        self.stars = self._stars_widget()\n",
    "        self.telescope = self._telescope_widget()\n",
    "        self.fp = self._fp_widget()\n",
    "        self.azimuth_ring = self._azimuth_ring_widget()\n",
    "        self.elevation_bearings = self._elevation_bearings_widget()\n",
    "        self.rays = self._rays_widget()\n",
    "        self.CCS = self._ccs_widget()  # 3-tuple\n",
    "        \n",
    "        # Matplotlib\n",
    "        self.mpl = self._mpl_widget()\n",
    "        \n",
    "        # Controls\n",
    "        self.alt_widget = ipywidgets.FloatText(value=45.0, step=5.0, description='alt (deg)')\n",
    "        self.az_widget = ipywidgets.FloatText(value=45.0, step=5.0, description='az (deg)')\n",
    "        self.rtp_widget = ipywidgets.FloatText(value=0.0, step=5.0, description='RTP (deg)')\n",
    "        self.thx_widget = ipywidgets.FloatText(value=0.0, step=0.25, description='Field x (deg)')\n",
    "        self.thy_widget = ipywidgets.FloatText(value=0.0, step=0.25, description='Field y (deg)')\n",
    "        self.lst_widget = ipywidgets.FloatText(value=0.0, step=0.01, description='LST (hr)')\n",
    "        self.horizon_widget = ipywidgets.Checkbox(value=self.clip_horizon, description='horizon')\n",
    "        self.CCS_widget = ipywidgets.Checkbox(value=self.show_CCS, description='CCS')\n",
    "        \n",
    "        # observe\n",
    "        self.alt_widget.observe(self.handle_alt, 'value')\n",
    "        self.az_widget.observe(self.handle_az, 'value')\n",
    "        self.rtp_widget.observe(self.handle_rtp, 'value')\n",
    "        self.thx_widget.observe(self.handle_thx, 'value')\n",
    "        self.thy_widget.observe(self.handle_thy, 'value')\n",
    "        self.lst_widget.observe(self.handle_lst, 'value')\n",
    "        self.horizon_widget.observe(self.handle_horizon, 'value')\n",
    "        self.CCS_widget.observer(self.handle_CCS, 'value')\n",
    "\n",
    "        self.update_constellations()\n",
    "        self.update_telescope()\n",
    "        self.update_fp()\n",
    "        self.update_elevation_bearings()\n",
    "        self.update_rays()\n",
    "        self.update_spot()\n",
    "        self.update_wf()\n",
    "        \n",
    "        self.scatters = [\n",
    "            self.constellations,\n",
    "            self.stars,\n",
    "            self.telescope,\n",
    "            self.fp,\n",
    "            self.azimuth_ring,\n",
    "            self.elevation_bearings,\n",
    "            self.rays,\n",
    "            *self.CCS\n",
    "        ]\n",
    "        \n",
    "        self.controls = [\n",
    "            self.alt_widget,\n",
    "            self.az_widget,\n",
    "            self.rtp_widget,\n",
    "            self.thx_widget, \n",
    "            self.thy_widget, \n",
    "            self.lst_widget, \n",
    "            self.horizon_widget,\n",
    "            self.CCS_widget\n",
    "        ]\n",
    "    \n",
    "    def _cs_xyz(self, coordSys, length=2):\n",
    "        p0 = coordSys.origin\n",
    "        px = p0 + coordSys.xhat * length\n",
    "        py = p0 + coordSys.yhat * length\n",
    "        pz = p0 + coordSys.zhat * length\n",
    "        return (\n",
    "            np.vstack([p0, px]).T,\n",
    "            np.vstack([p0, py]).T,\n",
    "            np.vstack([p0, pz]).T\n",
    "        )\n",
    "    \n",
    "    def _cs_widget(self, coordSys, length=2):\n",
    "        x_xyz, y_xyz, z_xys = self._cs_xyz(coordSys, length=length)        \n",
    "\n",
    "        xscat = ipv.Scatter(\n",
    "            x=x_xyz[0], y=x_xyz[1], z=x_xyz[2],\n",
    "            color=\"blue\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=1, \n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "        yscat = ipv.Scatter(\n",
    "            x=y_xyz[0], y=y_xyz[1], z=y_xyz[2],\n",
    "            color=\"red\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=1, \n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "        zscat = ipv.Scatter(\n",
    "            x=z_xyz[0], y=z_xyz[1], z=z_xyz[2],\n",
    "            color=\"white\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=1, \n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "        return xscat, yscat, zscat            \n",
    "    \n",
    "    def _ccs_widget(self):\n",
    "        return self._cs_widget(self.actual_telescope['Detector'].coordSys, 2)\n",
    "    \n",
    "    def _constellations_xyz(self, lst):\n",
    "        ctf = batoid.CoordTransform(\n",
    "            batoid.globalCoordSys,\n",
    "            batoid.CoordSys(\n",
    "                (0, 0, 0),\n",
    "                batoid.RotZ(np.deg2rad(lst*15)) @ batoid.RotY(-np.deg2rad(90-self.lat))\n",
    "            )\n",
    "        )\n",
    "        return ctf.applyForwardArray(*get_constellations_xyz())        \n",
    "    \n",
    "    def _constellations_widget(self):\n",
    "        x, y, z = self._constellations_xyz(0.0)\n",
    "        return ipv.Scatter(\n",
    "            x=x, y=y, z=z,\n",
    "            color=\"blue\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=1, \n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "    \n",
    "    def _stars_xyzs(self, lst):\n",
    "        ctf = batoid.CoordTransform(\n",
    "            batoid.globalCoordSys,\n",
    "            batoid.CoordSys(\n",
    "                (0, 0, 0),\n",
    "                batoid.RotZ(np.deg2rad(lst*15)) @ batoid.RotY(-np.deg2rad(90-self.lat))\n",
    "            )\n",
    "        )\n",
    "        x, y, z, s = get_stars_xyzs()\n",
    "        x, y, z = ctf.applyForwardArray(x, y, z)\n",
    "        return x, y, z, s\n",
    "    \n",
    "    def _stars_widget(self):\n",
    "        x, y, z, s = self._stars_xyzs(0.0)\n",
    "        x *= self.sky_dist\n",
    "        y *= self.sky_dist\n",
    "        z *= self.sky_dist\n",
    "        return ipv.Scatter(\n",
    "            x=x, y=y, z=z,\n",
    "            color=\"white\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=1, \n",
    "            size=s, \n",
    "            connected=False, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True,\n",
    "            geo='sphere'\n",
    "        )\n",
    "\n",
    "    def _telescope_xyz(self, alt, az, rtp):\n",
    "        telescope = self.fiducial_telescope\n",
    "        telescope = telescope.withGlobalShift([0, 0, 3.53])  # Height of M1 vertex above azimuth ring\n",
    "        telescope = telescope.withLocallyRotatedOptic(\"LSSTCamera\", batoid.RotZ(np.deg2rad(rtp)))\n",
    "        telescope = telescope.withLocalRotation(batoid.RotZ(np.deg2rad(90-az)))\n",
    "        telescope = telescope.withLocalRotation(batoid.RotX(np.deg2rad(90-alt)), rotOrigin=[0, 0, 5.425], coordSys=batoid.globalCoordSys)\n",
    "        self.actual_telescope = telescope\n",
    "        return self.actual_telescope.get3dmesh()\n",
    "    \n",
    "    def _telescope_widget(self):\n",
    "        x, y, z = self._telescope_xyz(self.alt, self.az, self.rtp)\n",
    "        return ipv.Scatter(\n",
    "            x=x, y=y, z=z,\n",
    "            color=\"white\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=1, \n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "    \n",
    "    def _azimuth_ring_widget(self):        \n",
    "        th = np.linspace(0, 2*np.pi, 100)\n",
    "        x_, y_ = np.cos(th), np.sin(th)\n",
    "        xs = []\n",
    "        ys = []\n",
    "        zs = []\n",
    "        for d in [-0.1, 0.1]:\n",
    "            for r in [4.5, 5.0]:\n",
    "                x = x_*r\n",
    "                y = y_*r\n",
    "                z = np.full_like(x, d)\n",
    "                xs.append(x)\n",
    "                ys.append(y)\n",
    "                zs.append(z)\n",
    "                xs.append([np.nan])\n",
    "                ys.append([np.nan])\n",
    "                zs.append([np.nan])\n",
    "        xs = np.hstack(xs)\n",
    "        ys = np.hstack(ys)\n",
    "        zs = np.hstack(zs)\n",
    "        return ipv.Scatter(\n",
    "            x=xs, y=ys, z=zs,\n",
    "            color=\"white\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=0,\n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )            \n",
    "            \n",
    "    def _elevation_bearings_xyz(self, az):\n",
    "        th = np.linspace(0, 2*np.pi, 100)\n",
    "        x_, y_ = np.cos(th), np.sin(th)\n",
    "        xs = []\n",
    "        ys = []\n",
    "        zs = []    \n",
    "        for d in [4.4, 4.5]:\n",
    "            for r in [1.5, 1.75]:\n",
    "                x = np.full_like(th, -d)\n",
    "                y = x_*r\n",
    "                z = y_*r            \n",
    "                z += 5.425  # height of elevation axis above azimuth ring\n",
    "                c = np.cos(np.deg2rad(90-az))\n",
    "                s = np.sin(np.deg2rad(90-az))\n",
    "                x, y = c*x - s*y, s*x + c*y\n",
    "                xs.append(x)\n",
    "                ys.append(y)\n",
    "                zs.append(z)\n",
    "                xs.append([np.nan])\n",
    "                ys.append([np.nan])\n",
    "                zs.append([np.nan])\n",
    "                xs.append(-x)\n",
    "                ys.append(-y)\n",
    "                zs.append(z)\n",
    "                xs.append([np.nan])\n",
    "                ys.append([np.nan])\n",
    "                zs.append([np.nan])\n",
    "        return np.hstack(xs), np.hstack(ys), np.hstack(zs)        \n",
    "        \n",
    "    def _elevation_bearings_widget(self):\n",
    "        x, y, z = self._elevation_bearings_xyz(45.0)\n",
    "        return ipv.Scatter(\n",
    "            x=x, y=y, z=z,\n",
    "            color=\"white\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=0,\n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "    \n",
    "    def _rays_xyz(self, thx, thy):\n",
    "        # Assume that self.actual_telescope is correct; \n",
    "        # so need to update telescope before rays when changing alt,az,rtp\n",
    "        rays = batoid.RayVector.asFan(\n",
    "            optic=self.actual_telescope,\n",
    "            wavelength=620e-9, \n",
    "            theta_x=np.deg2rad(thx), theta_y=np.deg2rad(thy), \n",
    "            nx=30, ny=30,\n",
    "            lx=8.359, ly=8.359\n",
    "        )\n",
    "        tf = self.actual_telescope.traceFull(rays)\n",
    "        xyz = []\n",
    "        for key, surface in tf.items():\n",
    "            if len(xyz) == 0:                \n",
    "                xyz.append(surface['in'].propagate(-self.sky_dist).toCoordSys(batoid.globalCoordSys).r)\n",
    "            out = surface['out'].toCoordSys(batoid.globalCoordSys)\n",
    "            r = out.r\n",
    "            r[out.vignetted] = np.nan\n",
    "            xyz.append(r)\n",
    "\n",
    "        xyz = np.array(xyz)\n",
    "        xyz = np.concatenate([xyz, np.full((1,)+xyz.shape[1:3], np.nan)])\n",
    "        xyz = np.array(xyz)\n",
    "        xyz = np.transpose(xyz, axes=[1,0,2])\n",
    "        xyz = np.vstack(xyz).T\n",
    "        return xyz\n",
    "    \n",
    "    def _rays_widget(self):\n",
    "        x, y, z = self._rays_xyz(0.0, 0.0)\n",
    "        return ipv.Scatter(\n",
    "            x=x, y=y, z=z,\n",
    "            color=\"yellow\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=0,\n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )        \n",
    "        \n",
    "    def _fp_xyz(self):\n",
    "        # Assume self.actual_telescope is up-to-date.\n",
    "        raft_det = 0.31527*2/5\n",
    "        raft_sky = self.sky_dist * np.deg2rad(0.7)\n",
    "        detector_ctf = batoid.CoordTransform(\n",
    "            self.actual_telescope['Detector'].coordSys, \n",
    "            batoid.globalCoordSys\n",
    "        )\n",
    "        # Sky CoordSys can be obtained from detector coordsys by \n",
    "        # rotating 180 degrees about the detector Z, and then \n",
    "        # placing the origin along Z out sky_dist meters.\n",
    "        sky_coordSys = self.actual_telescope['Detector'].coordSys\n",
    "        sky_coordSys = sky_coordSys.rotateLocal(batoid.RotZ(np.pi))\n",
    "        sky_coordSys.origin = sky_coordSys.zhat * self.sky_dist\n",
    "        sky_ctf = batoid.CoordTransform(\n",
    "            sky_coordSys,\n",
    "            batoid.globalCoordSys\n",
    "        )\n",
    "        \n",
    "        # Build in xy-plane in units of rafts to start\n",
    "        xs = []\n",
    "        ys = []\n",
    "        \n",
    "        # top hline\n",
    "        xs.extend([-1.5, 1.5, np.nan])\n",
    "        ys.extend([2.5, 2.5, np.nan])\n",
    "        # Middle hlines\n",
    "        for y in [1.5, 0.5, -0.5, -1.5]:\n",
    "            xs.extend([-2.5, 2.5, np.nan])\n",
    "            ys.extend([y, y, np.nan])\n",
    "        # bottom hline\n",
    "        xs.extend([-1.5, 1.5, np.nan])\n",
    "        ys.extend([-2.5, -2.5, np.nan])\n",
    "        # left vline\n",
    "        xs.extend([-2.5, -2.5, np.nan])\n",
    "        ys.extend([-1.5, 1.5, np.nan])\n",
    "        # middle vlines\n",
    "        for x in [1.5, 0.5, -0.5, -1.5]:\n",
    "            xs.extend([x, x, np.nan])\n",
    "            ys.extend([-2.5, 2.5, np.nan])\n",
    "        # right vline\n",
    "        xs.extend([2.5, 2.5, np.nan])\n",
    "        ys.extend([-1.5, 1.5, np.nan])\n",
    "        xs = np.array(xs)\n",
    "        ys = np.array(ys)\n",
    "        zs = np.zeros_like(xs)\n",
    "        \n",
    "        # Now do the projection on the sky.  It's technically a 180-degree rotation, \n",
    "        # but we'll ignore that for now since the pattern is rotationally symmetric.\n",
    "        xfp, yfp, zfp = detector_ctf.applyForwardArray(xs*raft_det, ys*raft_det, zs)\n",
    "        xsky, ysky, zsky = sky_ctf.applyForwardArray(xs*raft_sky, ys*raft_sky, zs)\n",
    "        \n",
    "        return (\n",
    "            np.hstack([xfp, [np.nan], xsky]),\n",
    "            np.hstack([yfp, [np.nan], ysky]),\n",
    "            np.hstack([zfp, [np.nan], zsky])\n",
    "        )\n",
    "    \n",
    "    def _fp_widget(self):\n",
    "        x, y, z = self._fp_xyz()\n",
    "        return ipv.Scatter(\n",
    "            x=x, y=y, z=z,\n",
    "            color=\"red\",\n",
    "            visible_lines=True, \n",
    "            color_selected=None, \n",
    "            size_selected=0,\n",
    "            size=0, \n",
    "            connected=True, \n",
    "            visible_markers=False,\n",
    "            cast_shadow=True,\n",
    "            receive_shadow=True\n",
    "        )\n",
    "    \n",
    "    def _mpl_widget(self):\n",
    "        fig, axes = plt.subplots(\n",
    "            nrows=3, ncols=1, \n",
    "            figsize=(2, 6.2), dpi=100,\n",
    "            facecolor='k'\n",
    "        )\n",
    "        fig.canvas.header_visible=False\n",
    "        self.spot_ax = axes[0]\n",
    "        self.spot_scatter = self.spot_ax.scatter([], [], s=1)\n",
    "        self.spot_ax.set_xlim(-1, 1)\n",
    "        self.spot_ax.set_ylim(-1, 1)\n",
    "\n",
    "        self.wf_ax = axes[1]\n",
    "        self.wf_imshow = self.wf_ax.imshow(\n",
    "            np.zeros((256, 256)),\n",
    "            vmin=-1.0, vmax=1.0,\n",
    "            cmap='seismic'\n",
    "        )\n",
    "\n",
    "        for ax in axes:\n",
    "            ax.set_aspect('equal')\n",
    "            ax.set_facecolor('k')\n",
    "        \n",
    "        out = ipywidgets.Output()\n",
    "        with out:\n",
    "            plt.show(fig)\n",
    "        return out\n",
    "\n",
    "    def handle_alt(self, change):\n",
    "        self.alt = change['new']\n",
    "        self.update_telescope()\n",
    "    \n",
    "    def handle_az(self, change):\n",
    "        self.az = change['new']\n",
    "        self.update_telescope()\n",
    "        self.update_elevation_bearings()\n",
    "\n",
    "    def handle_rtp(self, change):\n",
    "        self.rtp = change['new']\n",
    "        self.update_telescope()\n",
    "        self.update_spot()\n",
    "        self.update_wf()\n",
    "    \n",
    "    def handle_lst(self, change):\n",
    "        self.lst = change['new']\n",
    "        self.update_constellations()\n",
    "        self.update_stars()\n",
    "\n",
    "    def handle_thx(self, change):\n",
    "        self.thx = change['new']\n",
    "        self.update_rays()\n",
    "        self.update_spot()\n",
    "        self.update_wf()\n",
    "\n",
    "    def handle_thy(self, change):\n",
    "        self.thy = change['new']\n",
    "        self.update_rays()\n",
    "        self.update_spot()\n",
    "        self.update_wf()\n",
    "\n",
    "    def handle_horizon(self, change):\n",
    "        self.clip_horizon = not self.clip_horizon\n",
    "        self.update_constellations()\n",
    "        self.update_stars()\n",
    "        \n",
    "    def handle_ccs(self, change):\n",
    "        self.show_ccs = not self.show_ccs\n",
    "        self.update_cs(\n",
    "\n",
    "    def update_cs(self, cs):\n",
    "        cs_xyz = self._ccs_xyz(cs)\n",
    "        \n",
    "    def update_constellations(self):\n",
    "        x, y, z = self._constellations_xyz(self.lst)*self.sky_dist\n",
    "        if self.clip_horizon:\n",
    "            w = z<0\n",
    "            x[w] = np.nan\n",
    "            y[w] = np.nan\n",
    "            z[w] = np.nan\n",
    "        self.constellations.x = x\n",
    "        self.constellations.y = y\n",
    "        self.constellations.z = z\n",
    "        \n",
    "    def update_stars(self):\n",
    "        x, y, z, s = self._stars_xyzs(self.lst)\n",
    "        x *= self.sky_dist\n",
    "        y *= self.sky_dist\n",
    "        z *= self.sky_dist\n",
    "        if self.clip_horizon:\n",
    "            w = z<0\n",
    "            x[w] = np.nan\n",
    "            y[w] = np.nan\n",
    "            z[w] = np.nan\n",
    "        self.stars.x = x\n",
    "        self.stars.y = y\n",
    "        self.stars.z = z\n",
    "        \n",
    "    def update_telescope(self):\n",
    "        x, y, z = self._telescope_xyz(self.alt, self.az, self.rtp)\n",
    "        self.telescope.x = x\n",
    "        self.telescope.y = y\n",
    "        self.telescope.z = z\n",
    "        self.update_rays()\n",
    "        self.update_fp()\n",
    "        \n",
    "    def update_elevation_bearings(self):\n",
    "        x, y, z = self._elevation_bearings_xyz(self.az)\n",
    "        self.elevation_bearings.x = x\n",
    "        self.elevation_bearings.y = y\n",
    "        self.elevation_bearings.z = z\n",
    "        \n",
    "    def update_rays(self):\n",
    "        x, y, z = self._rays_xyz(self.thx, self.thy)\n",
    "        self.rays.x = x\n",
    "        self.rays.y = y        \n",
    "        self.rays.z = z\n",
    "        \n",
    "    def update_fp(self):\n",
    "        x, y, z = self._fp_xyz()\n",
    "        self.fp.x = x\n",
    "        self.fp.y = y\n",
    "        self.fp.z = z\n",
    "        \n",
    "    def update_spot(self):\n",
    "        perturbed = self.actual_telescope.withLocallyShiftedOptic(\"Detector\", (0, 0, -0.0015))  # intra-focal\n",
    "        nx = 64\n",
    "        rays = batoid.RayVector.asGrid(\n",
    "            optic=perturbed, wavelength=620e-9,\n",
    "            nx=nx, \n",
    "            theta_x=np.deg2rad(self.thx),\n",
    "            theta_y=np.deg2rad(self.thy)\n",
    "        )\n",
    "        perturbed.trace(rays)\n",
    "        cridx = (nx//2)*nx+nx//2 if (nx%2)==0 else (nx*nx-1)//2\n",
    "        point = rays[cridx].r[0]    \n",
    "        targetCoordSys = rays.coordSys.shiftLocal(point)\n",
    "        rays = rays.toCoordSys(targetCoordSys)\n",
    "        \n",
    "        visible = ~rays.vignetted\n",
    "        x = rays.x * 1e6  # microns\n",
    "        y = rays.y * 1e6  # microns\n",
    "        \n",
    "        xmax = 1.5*np.quantile(np.abs(np.array([x[visible], y[visible]])), 0.95)\n",
    "                    \n",
    "        self.spot_ax.set_xlim(xmax, -xmax)\n",
    "        self.spot_ax.set_ylim(-xmax, xmax)\n",
    "        \n",
    "        self.spot_scatter.set_alpha(visible.astype(float)*0.25)\n",
    "        self.spot_scatter.set_offsets(np.array([x, y]).T)\n",
    "        \n",
    "    def update_wf(self):\n",
    "        nx = 256\n",
    "        wf = batoid.wavefront(\n",
    "            self.fiducial_telescope,\n",
    "            np.deg2rad(self.thx),\n",
    "            np.deg2rad(self.thy),\n",
    "            620e-9,\n",
    "            nx=nx\n",
    "        )\n",
    "        arr = sub_ptt(wf.array)\n",
    "        self.wf_imshow.set_array(arr)\n",
    "        \n",
    "    def display(self):\n",
    "        ipvfig = ipv.Figure(width=800, height=600)\n",
    "        ipvfig.camera.far = 100000\n",
    "        ipvfig.camera.near = 0.01\n",
    "        ipvfig.style = ipv.styles.dark\n",
    "        ipvfig.style['box'] = dict(visible=False)\n",
    "        ipvfig.style['axes']['visible'] = False\n",
    "        ipvfig.xlim = (-10, 10)\n",
    "        ipvfig.ylim = (-10, 10)\n",
    "        ipvfig.zlim = (-10, 10)\n",
    "        ipvfig.animation = 100\n",
    "        ipvfig.scatters = self.scatters\n",
    "\n",
    "        display(ipywidgets.HBox([\n",
    "            self.mpl,\n",
    "            ipvfig,\n",
    "            ipywidgets.VBox(self.controls)\n",
    "        ]))  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "0dbda4a4-8031-4b88-aa83-33c9ef7926ba",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T21:17:15.281859Z",
     "iopub.status.busy": "2022-05-23T21:17:15.281250Z",
     "iopub.status.idle": "2022-05-23T21:17:15.293507Z",
     "shell.execute_reply": "2022-05-23T21:17:15.292638Z",
     "shell.execute_reply.started": "2022-05-23T21:17:15.281818Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "54cc0f9a32684b39a15b91ae943884c8",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "debug"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "93b447cd-2c47-4adf-8f50-a105f472b014",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T21:17:24.792768Z",
     "iopub.status.busy": "2022-05-23T21:17:24.791884Z",
     "iopub.status.idle": "2022-05-23T21:17:25.509996Z",
     "shell.execute_reply": "2022-05-23T21:17:25.509613Z",
     "shell.execute_reply.started": "2022-05-23T21:17:24.792729Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "de6ca98ba92f49898f3f7f0e3b55a48e",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(Output(), Figure(animation=100.0, box_center=[0.5, 0.5, 0.5], box_size=[1.0, 1.0, 1.0], camera=…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "app = RubinCSApp()\n",
    "app.display()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 132,
   "id": "40f958ed-1d70-4dbe-9b9f-9e85440d6d7b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2022-05-23T20:22:15.140771Z",
     "iopub.status.busy": "2022-05-23T20:22:15.140415Z",
     "iopub.status.idle": "2022-05-23T20:22:15.145954Z",
     "shell.execute_reply": "2022-05-23T20:22:15.144714Z",
     "shell.execute_reply.started": "2022-05-23T20:22:15.140717Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Todo\n",
    "#  - sun\n",
    "#  - moon\n",
    "#  - planets\n",
    "#  - CSSs\n",
    "#  - Correct Time\n",
    "#  - RA/dec, FIELD_ANGLE, FOCAL output\n",
    "#  - matplotlib images:\n",
    "#    - spot\n",
    "#    - focal plane\n",
    "#    • Axes labeled: EDCS, DVCS, EqCS, AACS\n",
    "#  - Telescope perturbations\n",
    "#    • AOS modes\n",
    "#    • Additional Zernike in entrance pupil\n",
    "#    • flexure/thermal/etc\n",
    "#    • apply LUT\n",
    "#  - Zernikes across FoV (might be slowish)\n",
    "#    • absolute\n",
    "#    • difference from nominal"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4763804a-da8a-412a-8434-4ed079db078e",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
