From 731c69a9f01540c545ff2b317947f60e4b34de64 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Fri, 23 Feb 2024 10:14:27 -0700 Subject: [PATCH 01/36] update status to make it more usable on Oxford IPS120 --- .../01-example_property_scans.ipynb | 72 ++++--------------- pyscan/drivers/oxfordips120.py | 2 +- 2 files changed, 14 insertions(+), 60 deletions(-) diff --git a/demo_notebooks/01-example_property_scans.ipynb b/demo_notebooks/01-example_property_scans.ipynb index 3d5cbab8..9e19ecbc 100644 --- a/demo_notebooks/01-example_property_scans.ipynb +++ b/demo_notebooks/01-example_property_scans.ipynb @@ -11,12 +11,6 @@ "cell_type": "code", "execution_count": 1, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:04.166557Z", - "iopub.status.busy": "2023-12-14T21:19:04.165721Z", - "iopub.status.idle": "2023-12-14T21:19:06.257252Z", - "shell.execute_reply": "2023-12-14T21:19:06.256870Z" - }, "tags": [] }, "outputs": [ @@ -27,6 +21,7 @@ "Could not load Keysight SD1\n", "Could not load Keysight SD1\n", "pylablib not found, AttocubeANC350 not loaded\n", + "Basler Camera software not found, BaserCamera not loaded\n", "Helios Camera not installed\n", "msl not installed, Thorlabs BSC203 driver not loaded\n", "seabreeze module not found, Ocean Optics not imported\n", @@ -48,12 +43,6 @@ "cell_type": "code", "execution_count": 2, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:06.260031Z", - "iopub.status.busy": "2023-12-14T21:19:06.259668Z", - "iopub.status.idle": "2023-12-14T21:19:06.263607Z", - "shell.execute_reply": "2023-12-14T21:19:06.263143Z" - }, "tags": [] }, "outputs": [], @@ -86,12 +75,6 @@ "cell_type": "code", "execution_count": 3, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:06.265505Z", - "iopub.status.busy": "2023-12-14T21:19:06.265339Z", - "iopub.status.idle": "2023-12-14T21:19:06.267711Z", - "shell.execute_reply": "2023-12-14T21:19:06.267311Z" - }, "tags": [] }, "outputs": [], @@ -108,12 +91,6 @@ "cell_type": "code", "execution_count": 4, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:06.269666Z", - "iopub.status.busy": "2023-12-14T21:19:06.269406Z", - "iopub.status.idle": "2023-12-14T21:19:06.275415Z", - "shell.execute_reply": "2023-12-14T21:19:06.275036Z" - }, "tags": [] }, "outputs": [], @@ -133,18 +110,12 @@ "cell_type": "code", "execution_count": 5, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:06.277440Z", - "iopub.status.busy": "2023-12-14T21:19:06.277286Z", - "iopub.status.idle": "2023-12-14T21:19:07.487927Z", - "shell.execute_reply": "2023-12-14T21:19:07.487470Z" - }, "tags": [] }, "outputs": [ { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj4AAAHFCAYAAADyj/PrAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAABRtUlEQVR4nO3dd3RUdeL+8fek94QaAgkJJbQAKSAKyAJSXEGkCAjBtu6ujSqCAlZsWFEUrKy4uxK6YAEEVEAQlTYJvfcSWkgnbeb+/vBLfhtpCSS5k8zzOifnMLfNM/cmuQ/3c2diMQzDQERERMQJuJgdQERERKS8qPiIiIiI01DxEREREaeh4iMiIiJOQ8VHREREnIaKj4iIiDgNFR8RERFxGio+IiIi4jRUfERERMRpqPiIlLNjx44xatQoOnbsSFBQEBaLhS+++OKGtrlp0yaGDh1KixYt8Pf3Jzg4mK5du/LTTz9ddvkDBw7Qr18/goKC8PPzo1u3bmzevLnIMidPnuTZZ5+lbdu2VK9enYCAAFq1asWnn36KzWa7ap7p06djsVjw8/O77PzNmzfTtWtX/Pz8CAoKol+/fhw4cKDIMnv27GHMmDG0atWKoKAgqlatSvv27Zk/f/4l2yvpPs3KyuL555+nUaNGeHp6Uq1aNTp37szevXsBiIiIwGKxXPPr4nP85z//YdCgQTRu3BgXFxciIiKuun+Ks58efPDByz5nkyZNLrutw4cP89BDD1G7dm08PT2pU6cOffv2ve799N1333H//ffTokUL3N3dsVgsxXpNIo7OzewAIs5m3759zJw5k5iYGHr06MGsWbNueJuzZs1i/fr1PPTQQ0RHR5OVlcXHH39Mly5d+Pe//839999fuOyZM2fo0KEDVapU4fPPP8fLy4tJkybRqVMnNmzYQOPGjYE/ytR//vMf7r//fp577jnc3d1ZunQpjz32GL/99huff/75ZbMcP36cMWPGULt2bdLS0i6Zv2vXLjp16kRMTAxz584lJyeH559/ng4dOpCYmEiNGjUAWL58OYsXL+a+++7jpptuoqCggDlz5jBgwAAmTpzI888/f137NDMzk86dO3PixAnGjRtHy5YtSUtLY926dWRnZwOwcOFCcnNzC9eZPn06//rXv/j+++8JDAwsnN6gQQMA/vvf/5KcnEybNm2w2+3k5+df85hdaz8BeHt7X1Jevb29L1lu27ZtdOrUifr16/P2228TGhrKyZMnWbZsWZHlSrKfFi5cyG+//UZsbCyenp5s2rTpmq9JpEIwRKRc2Wy2wn9v2LDBAIwZM2bc0DZPnTp1ybSCggKjZcuWRoMGDYpMHzt2rOHu7m4cOnSocFpaWppRvXp1Y+DAgYXTUlJSjLy8vEu2O3ToUAMwjhw5ctksd955p9GrVy/jgQceMHx9fS+ZP2DAAKN69epGWlpa4bRDhw4Z7u7uxlNPPVU47cyZM4bdbr9k/Z49exo+Pj5GTk5O4bSS7NORI0cavr6+xv79+y87/3JeeOEFAzDOnDlz2fn/+/w9e/Y0wsPDr7nNa+2nK03/M7vdbsTExBgxMTFF9sm1cl5rP/3vshePuUhloKEukVKQk5NDbGwsDRs2LPK/9+TkZGrVqkWnTp0Kh4dcXEr/x65mzZqXTHN1daVVq1YcPXq0yPSFCxdy2223ER4eXjgtICCAfv368e2331JQUABAlSpVcHd3v2S7bdq0Af4YNvmzL7/8ktWrV/Phhx9eNmdBQQHfffcdd999NwEBAYXTw8PD6dy5MwsXLiycVr169csOr7Rp04bs7GxSUlIKpxV3n2ZnZzN9+nQGDBhA/fr1i7VOcZT0mF5rP5XEzz//TGJiIqNGjcLT0/Oqy5YkZ1l8n4o4An1ni5QCLy8v5s6dy+nTp3nooYcAsNvtDBkyBMMwmDVrFq6uruWaqaCggDVr1hAVFVU47cKFC+zfv5+WLVtesnzLli25cOHCJffa/NlPP/2Em5sbjRo1KjL99OnTjBo1itdff53Q0NDLrrt//34uXLhwxefft28fOTk5V33+lStXUqNGjcuWvWvZtGkTWVlZREZG8thjj1GlShU8PDxo3bo1ixcvLvH2rkdx9tNFFy5coFatWri6uhIaGsqwYcOKFD74o/gA+Pv706NHD7y8vPDz8+POO+9k165dZfY6RCoq3eMjUkoiIyOZPn0699xzD1OmTCElJYVVq1bx/fffExISUu55XnzxRfbt28eiRYsKp50/fx7DMKhateoly1+cdu7cuStuc/ny5fz3v/9l5MiRVKtWrci8xx9/nMaNG/PYY49dcf2L277S8xuGwfnz56+4v6ZPn86qVauYMmXKdRXJ48ePA/DGG2/QokUL/vOf/+Di4sI777xDr169WLp0KbfffnuJt1sSxdlPANHR0URHR9O8eXMAVq9ezbvvvsuPP/7Ihg0bCm+Ivvia/va3vzFgwAAWL15ceGN6hw4d2LJliynffyKOSsVHpBQNHDiQVatWMXbsWGw2GxMmTKBbt27lnmP69Om8+uqrPPnkk/Tu3fuS+Vd7h86V5m3evJmBAwdyyy23MGnSpCLzFixYwLfffovVai3Wu3+u5/mXLl3K0KFD6d+/P8OHD7/mc1yO3W4HwMPDg6VLl+Lv7w9A586diYyM5OWXXy7T4lOS/fTEE08UedytWzdiY2Pp378/n332WeH8i6+pbdu2TJ8+vXD55s2bExsby7Rp03jllVdK+ZWIVFwa6hIpZQ899BD5+fm4ubkxYsSIcn/+GTNm8Mgjj/Dwww/z1ltvFZlXpUoVLBbLZa/qXBxCudzVGKvVSrdu3YiMjGTJkiVF7iXJzMxk6NChDB8+nNq1a5Oamkpqaip5eXkApKamkpWVBVB4lehKz2+xWAgKCrpk3rJly+jXrx/dunVj5syZ1/3W6ovP365du8LSA+Dj40PHjh0veUt/aSrJfrqSvn374uvry2+//VY47eJr+nNhi4mJISQkpExfk0hFpOIjUoqysrK47777aNSoEd7e3vzjH/8o1+efMWMG//jHP3jggQf4+OOPLykI3t7eNGzYkK1bt16y7tatW/H29r7kpl+r1UrXrl0JDw9n+fLlRd7ODXD27FlOnTrFO++8Q5UqVQq/Zs2aRVZWFlWqVGHIkCHAH2//9vb2vuLzN2zYEC8vryLTly1bRp8+fejYsSMLFizAw8PjuvYNcNl7iy4yDKNMb+gtyX66mj/nNPM1iVREGuoSKUWPPvooR44cYf369ezatYv+/fvz7rvvXjJsURa++OIL/vGPf3DvvfcWfjDe5fTt25f33nuPo0ePEhYWBkBGRgZfffUVd911F25u///XQmJiIl27diU0NJQVK1ZQpUqVS7ZXq1YtVq5cecn0119/ndWrV7N06VKqV68OgJubG7169eKrr77izTffLLzqcuTIEVauXHnJflq+fDl9+vTh1ltvZdGiRdd819K1hISE0LZtW3755RfS09ML31mWnZ3N6tWrueWWW25o+1dTkv10JfPnzyc7O7tIzjvuuAMfHx+WLl1aZP9t3ryZ5OTkMn1NIhWRio9IKZk+fTpffvklM2bMICoqiqioKIYNG8bTTz9N+/btC98GDhR++vDFd1Bt3Lix8GbV/v37Fy734osvMnHiRFauXEmnTp2u+Nzz5s3j73//OzExMTzyyCOsX7++yPyLH0IHMGbMGP773//Ss2dPXnrpJTw9PXn99dfJycnhxRdfLFxn9+7ddO3aFYBXX32VvXv3Fn6yMfxx9aZGjRp4eXldNtsXX3yBq6vrJfMmTpzITTfdxJ133sm4ceMKP8CwevXqPPnkk4XLrV27lj59+lCrVi0mTJhAYmJike00a9asyFvii7tP3377bTp37sztt9/O008/jcVi4Z133uHs2bO8/PLLV9zHV7Njxw527NgB/PERBtnZ2YV5mjVrRrNmzUq0nw4fPkx8fDyDBg2iYcOGWCwWVq9ezXvvvUdUVFSRK4lBQUG89NJLjBkzhgcffJDBgweTnJzMc889R926dXn88ceLPF9x99Phw4fZsGED8Me78f533YiICFq3bn1d+0rEdGZ+iJBIZbFlyxbD29vbeOCBB4pMz8nJMVq1amVEREQY58+fL5wOXPHrfz355JOGxWIxdu7cedXnf+CBB666zYMHDxZZft++fUafPn2MgIAAw8fHx+jSpYuxadOmIsvMmDHjqtu81ocuXu0D+DZu3Gh06dLF8PHxMQICAow+ffoY+/btK7LMxQ8NvNLXypUriyxf3H1qGIaxZs0ao2PHjoaPj4/h4+Nj3HbbbcYvv/xyxddyrQ8wvFrWF154ocT7KSUlxejbt68RERFheHt7Gx4eHkZkZKTx1FNPGampqZfdzmeffWY0b97c8PDwMKpVq2YMGTLEOHr06CXLFXc/Xe34//n7XKQisRiGYZRijxKRUtSmTRvCw8OZN2+e2VFERCoFFR8RB5Wenk6NGjVITEykadOmZscREakUVHxERETEaeh9jiIiIuI0TC0+P//8M7169aJ27dpYLJYiH60Pf3wGxYsvvkjt2rXx9vamU6dObN++3ZywIiIiUuGZWnyysrKIjo5m6tSpl53/5ptvMnnyZKZOncqGDRuoVasW3bp1IyMjo5yTioiISGXgMPf4WCwWFi5cSJ8+fYA/rvbUrl2bUaNG8fTTTwOQm5tLcHAwb7zxBo888oiJaUVERKQictgPMDx48CDJycl07969cJqnpycdO3Zk3bp1Vyw+ubm55ObmFj622+2kpKRQrVq16/77PiIi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", "text/plain": [ "
" ] @@ -161,12 +132,6 @@ "cell_type": "code", "execution_count": 6, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:07.489935Z", - "iopub.status.busy": "2023-12-14T21:19:07.489733Z", - "iopub.status.idle": "2023-12-14T21:19:07.495266Z", - "shell.execute_reply": "2023-12-14T21:19:07.494893Z" - }, "tags": [] }, "outputs": [], @@ -190,18 +155,12 @@ "cell_type": "code", "execution_count": 7, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:07.497338Z", - "iopub.status.busy": "2023-12-14T21:19:07.497003Z", - "iopub.status.idle": "2023-12-14T21:19:10.927831Z", - "shell.execute_reply": "2023-12-14T21:19:10.927275Z" - }, "tags": [] }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHFCAYAAAAe+pb9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA6qUlEQVR4nO3deXQUVd7G8aeTzgaYsATCFsK+icsQDALDoAhxgAFBEBRGQGAgbggIKuLIMipuoKCCOGyDIiICiu9EICIgi69ISBSFEWQxgIkhQRJASEhy3z849GtPAiad7nTS9f2c0+dM375163f7EvuZqupqmzHGCAAAwIL8vF0AAACAtxCEAACAZRGEAACAZRGEAACAZRGEAACAZRGEAACAZRGEAACAZRGEAACAZRGEAACAZRGEAC87fvy4xo0bpy5duqhq1aqy2WxaunRpqcZMTEzUgw8+qOuuu07XXHONIiIi1K1bN3322WdF9j98+LDuvPNOVa1aVVWqVFH37t21Z88epz6pqal66qmn1KFDB4WHhys0NFTR0dF66623lJ+f79Q3OTlZvXr1UoMGDRQSEqLq1aurQ4cOeueddwrte/v27Ro1apSio6MVFBQkm82mo0ePFup34MABTZw4UdHR0apataqqV6+uTp066YMPPijU19X39Pz582revLlsNptefvllR3vDhg1ls9l+93F5H8uWLdPdd9+tFi1ayM/PTw0bNvzdfUvSwoULZbPZVKVKFZffJ+nSWg0fPly1atVScHCwrr/+ei1atKjIvunp6Ro+fLjCw8NVqVIldejQQZs2bXLqc/To0avO+89//nOx5geURwQhwMt++OEHLV++XIGBgerZs6dbxlyxYoV27dqlESNG6KOPPtLChQsVFBSk2267TcuWLXPqe/LkSXXu3FkHDhzQ4sWL9f777+vChQu65ZZb9P333zv6JSYmatmyZY4xVq9erS5duuj+++/X3/72N6cxT58+rcjISD333HOKj4/XsmXL1LBhQ91777165plnnPpu2rRJn376qRo0aKCOHTtecU4bN27Uv//9b/Xv31+rVq3S8uXL1axZM911112aMWOGU19X39O///3vOnfuXKH2tWvX6osvvnA8Ro4cKUlav369U3uvXr0kSW+//ba+++47xcTEqEmTJsXa94kTJzRx4kTVrVu3yNeL+z5lZWXp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HpV85N+Up9fiwtk8SYfdOiZHhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGWViyA0b948NWrUSMHBwYqOjta2bduu2n/58uW64YYbVKlSJdWpU0f33XefMjMzy6haAADgK7wehFauXKlx48ZpypQpSkpKUufOndWjRw+lpKQU2X/79u0aOnSoRo4cqe+++06rVq3SV199pVGjRpVx5QAAoKLzehCaPXu2Ro4cqVGjRqlVq1Z69dVXFRkZqfnz5xfZ/3//93/VsGFDjR07Vo0aNdIf//hHjRkzRrt37y7jygEAQEXn1SCUm5urxMRExcbGOrXHxsZq586dRW7TsWNHHT9+XPHx8TLG6Oeff9YHH3ygXr16XXE/OTk5ys7OdnoAAAB4NQhlZGQoPz9fERERTu0RERFKS0srcpuOHTtq+fLlGjRokAIDA1W7dm1VrVpVr7322hX3M3PmTIWFhTkekZGRbp0HAAComLx+akySbDab03NjTKG2y/bt26exY8fq6aefVmJiotavX68jR44oLi7uiuNPnjxZWVlZjsexY8fcWj8AAKiY7N7ceXh4uPz9/Qsd/UlPTy90lOiymTNnqlOnTpo0aZIk6frrr1flypXVuXNnPfPMM6pTp06hbYKCghQUFOT+CQAAgArNq0eEAgMDFR0drYSEBKf2hIQEdezYschtfv31V/n5OZft7+8v6dKRJAAAgOLy+qmxCRMmaOHChVq8eLH279+v8ePHKyUlxXGqa/LkyRo6dKijf+/evbVmzRrNnz9fhw8f1o4dOzR27FjFxMSobt263poGAACogLx6akySBg0apMzMTM2YMUOpqalq06aN4uPjFRUVJUlKTU11uqfQ8OHDdebMGb3++ut69NFHVbVqVXXt2lUvvPCCt6YAAAAqKJux4Pmk7OxshYWFKWbtI7JX9sy1Q4H+eR4Zt6wF231kHj6wHsH+F71dglsE+uV7u4RSC/HP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gQMHZIxRy5Yt1aJFC0efW2+91W1FAgAAeEKpbqjYsmVLtWzZ0l21AAAAlCmXg9Dx48e1bt06paSkKDc31+m12bNnl7owAAAAT3MpCG3atEl9+vRRo0aN9P3336tNmzY6evSojDFq27atu2sEAADwCJculp48ebIeffRRffvttwoODtbq1at17NgxdenSRXfddZe7awQAAPAIl4LQ/v37NWzYMEmS3W7X+fPnVaVKFc2YMUMvvPCCWwsEAADwFJeCUOXKlZWTkyPp0o0TDx065HgtIyPDPZUBAAB4mEvXCN18883asWOHWrdurV69eunRRx/V3r17tWbNGt18883urhEAAMAjXApCs2fP1tmzZyVJ06ZN09mzZ7Vy5Uo1bdpUr7zyilsLBAAA8BSXglDjxo0d/7tSpUqaN2+e2woCAAAoKy5dI9S4cWNlZmYWaj99+rRTSAIAACjPXApCR48eVX5+fqH2nJwcnThxotRFAQAAlIUSnRpbt26d439v2LBBYWFhjuf5+fnatGmTGjZs6LbiAAAAPKlEQahv376SJJvN5riP0GUBAQFq2LAhvzgPAAAqjBIFoYKCAklSo0aN9NVXXyk8PNwjRQEAAJQFl741duTIEXfXAQAAUOaKHYTmzp1b7EHHjh3rUjEAAABlqdhBqLg3SrTZbAQhAABQIRQ7CHE6DAAA+BqX7iP0W8YYGWPcUQsAAECZculiaUlatmyZXnrpJR08eFCS1Lx5c02aNEn33nuv24rztLADfvIPLHUWLFKBy+9s+WJ8ZB4F/t6uoPR8Zi18YB4+sxY+8HchSRd9YD1y7L5xQMHTfxsFFy64fUyXf3T173//ux566CF16tRJxhjt2LFDcXFxysjI0Pjx491dJwAAgNu5FIRee+01zZ8/X0OHDnW03XHHHbr22ms1bdo0ghAAAKgQXDovlJqaqo4dOxZq79ixo1JTU0tdFAAAQFlwKQg1bdpU77//fqH2lStXqlmzZqUuCgAAoCy4dGps+vTpGjRokD7//HN16tRJNptN27dv16ZNm4oMSAAAAOWRS0eE+vfvry+//FLh4eH68MMPtWbNGoWHh2vXrl3q16+fu2sEAADwCJe/6BYdHa133nnHnbUAAACUKZeOCN16661atGiRsrKy3F0PAABAmXEpCF133XV66qmnVLt2bfXv318ffvihcnNz3V0bAACAR7kUhObOnasTJ07oo48+0jXXXKNhw4apdu3aGj16tLZu3eruGgEAADzC5d+X8PPzU2xsrJYuXaqff/5ZCxYs0K5du9S1a1d31gcAAOAxpf5VkLS0NL333nt655139M033+imm25yR10AAAAe59IRoezsbC1ZskTdu3dXZGSk5s+fr969e+vAgQP68ssv3V0jAACAR7h0RCgiIkLVqlXTwIED9dxzz3EUCAAAVEguBaGPPvpI3bp1k5/f1Q8o7dixQ+3atVNQUJBLxQEAAHiSS6fGYmNjfzcESVKPHj104sQJV3YBAADgcS5/a6w4jDGeHB4AAKBUPBqEAAAAyjOCEAAAsCyCEAAAsCyPBiGbzebJ4QEAAEqFi6UBAIBllfonNq7mzJkznhweAACgVEp8ROjrr7/WM888o3nz5ikjI8PptezsbI0YMcJtxQEAAHhSiYLQxo0bFRMTo/fee08vvPCCWrVqpc2bNzteP3/+vP71r3+5vUgAAABPKFEQmjZtmiZOnKhvv/1WR48e1WOPPaY+ffpo/fr1nqoPAADAY0p0jdB3332nt99+W9Klb4RNmjRJ9evX14ABA7RixQrFxMR4pEgAAABPKFEQCgoK0unTp53a7rnnHvn5+enuu+/WrFmz3FkbAACAR5UoCN14443avHmzoqOjndoHDRqkgoICDRs2zK3FAQAAeFKJgtD999+vzz//vMjX7rnnHknSW2+9VfqqAAAAykCJLpbu16+fXnnlFd13333atGlToRsm3nPPPU7fIgMAACjPXLqzdGZmpnr16qX69evr0UcfVXJyspvLAgAA8DyXgtC6deuUlpamqVOnKjExUdHR0WrdurWee+45HT16tMTjzZs3T40aNVJwcLCio6O1bdu2Ym23Y8cO2e123XjjjSXeJwAAgMu/NVa1alWNHj1aW7Zs0Y8//qj77rtPb7/9tpo2bVqicVauXKlx48ZpypQpSkpKUufOndWjRw+lpKRcdbusrCwNHTpUt912m6tTAAAAFlfqH129ePGidu/erS+//FJHjx5VREREibafPXu2Ro4cqVGjRqlVq1Z69dVXFRkZqfnz5191uzFjxmjw4MHq0KFDacoHAAAW5nIQ2rx5s/72t78pIiJCw4YN0zXXXKOPP/5Yx44dK/YYubm5SkxMVGxsrFN7bGysdu7cecXtlixZokOH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hAkTtHDhQi1evFj79+/X+PHjlZKSori4OEmXjuYMHTrU0T8uLk4//vijJkyYoP3792vx4sVatGiRJk6c6K0pAACACsrr1wgNGjRImZmZmjFjhlJTU9WmTRvFx8crKipKkpSamup0T6FGjRopPj5e48eP1xtvvKG6detq7ty56t+/v7emAAAAKiivByFJeuCBB/TAAw8U+drSpUsLtXXp0kV79uxxeX9BQUGaOnVqkafLULZYi/KDtSg/WIvyg7UoXzyxHl6/oSIAAIC3eP0aIQAAAG8hCAEAAMsiCAEAAMsiCAEAAMvyySA0b948NWrUSMHBwYqOjta2bduu2n/r1q2Kjo5WcHCwGjdurDfffLOMKrWGkqzHmjVr1L17d9WsWVOhoaHq0KGDNmzYUIbV+raS/m1ctmPHDtntdt14442eLdBCSroWOTk5mjJliqKiohQUFKQmTZpo8eLFZVStbyvpWixfvlw33HCDKlWqpDp16ui+++5TZmZmGVXruz7//HP17t1bdevWlc1m04cffvi727jl89v4mPfee88EBASYf/7zn2bfvn3mkUceMZUrVzY//vhjkf0PHz5sKlWqZB555BGzb98+889//tMEBASYDz74oIwr900lXY9HHnnEvPDCC2bXrl3mwIEDZvLkySYgIMDs2bOnjCv3PSVdi8tOnz5tGjdubGJjY80NN9xQNsX6OFfWok+fPqZ9+/YmISHBHDlyxHz55Zdmx44dZVi1byrpWmzbts34+fmZOXPmmMOHD5tt27aZa6+91vTt27eMK/c98fHxZsqUKWb16tVGklm7du1V+7vr89vnglBMTIyJi4tzamvZsqV54okniuz/2GOPmZYtWzq1jRkzxtx8880eq9FKSroeRWndurWZPn26u0uzHFfXYtCgQeapp54yU6dOJQi5SUnX4pNPPjFhYWEmMzOzLMqzlJKuxUsvvWQaN27s1DZ37lxTv359j9VoRcUJQu76/PapU2O5ublKTExUbGysU3tsbKx27txZ5DZffPFFof633367du/erYsXL3qsVitwZT3+W0FBgc6cOePWH9izIlfXYsmSJTp06JCmTp3q6RItw5W1WLdundq1a6cXX3xR9erVU/PmzTVx4kSdP3++LEr2Wa6sRceOHXX8+HHFx8fLGKOff/5ZH3zwgXr16lUWJeM33PX5XS7uLO0u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fiZNmqQNGzbo5ZdfVtOmTRUSEqIBAwYoNzf3qmP+tr2goEB16tTRli1bCvWrWrVqiWsCUDYIQgBKJSQkRHfeeaeWL1+uH374Qc2bN1d0dLTbxh88eLCWL1+u+vXry8/PT7169XK81rp1a23fvt2p/86dO9W8efMrHpEKCAgodLRo27ZtGj58uPr16yfp0jVDR48edbzesmVL5eXlKSkpyTG3H374welC7rZt2yotLU12u91xETaA8o9TYwBKbciQIfr3v/+txYsX669//avTa8nJyUpOTtbZs2d18uRJJScna9++fSUae8+ePXr22Wc1YMAABQcHO1579NFHtWnTJv3jH//QgQMH9K9//Uuvv/66Jk6ceMXxGjZsqE2bNiktLU2//PKLpEunudasWaPk5GR9/fXXGjx4sNM34Vq2bKlu3bpp9OjR2rVrl5KSkjR69GiFhIQ4jkh169ZNHTp0UN++fbVhwwYdPXpUO3fu1FNPPaXdu3cXe74Ayph3L1EC4Avy8vJMnTp1jCRz6NAhp9ckFXpERUWVaPybbrrJSDKfffZZodc++OAD07p1axMQEGAaNGhgXnrpJafX//ti6XXr1pmmTZsau93uqOPIkSPm1ltvNSEhISYyMtK8/vrrhS7u/umnn0yPHj1MUFCQiYqKMu+++66pVauWefPNNx19srOzzcMPP2zq1q1rAgICTGRkpBkyZIhJSUkp0XwBlB2bMS6cUAcAizt+/LgiIyP16aef6rbbbvN2OQBcRBACgGL47LPPdPbsWV133XVKTU3VY489phMnTujAgQMKCAjwdnkAXMTF0gC8pkqVKld87ZNPPlHnzp3LsJqru3jxop588kkdPnxY11xzjTp27Kjly5cTgoAKjiNCALzmhx9+uOJr9erVU0hISBlWA8CKCEIAAMCy+Po8AACwLIIQAACwLIIQAACwLIIQAACwLIIQAACwLIIQAACwLIIQAACwLIIQAACwrP8DQb3fo4W5rJIAAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHFCAYAAAAe+pb9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAAA6VklEQVR4nO3deXxU1f3/8fckkxVI2EOAGEB2UZAgSygiCPEHFATZLFRQpIiiCAhWxIJYW4oLKiq4sIlFRGUp1rCkyBKWKoREqWDZDWBiTJCENZDk/P7gwXw7JiyZzGSSua/n4zGPR+fMvWc+557gvHvunTs2Y4wRAACABfl5uwAAAABvIQgBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBXnb8+HGNGzdOnTt3VuXKlWWz2bRo0aIS9ZmUlKQxY8bo1ltvVaVKlRQREaFu3brpyy+/LHL7w4cP67777lPlypVVsWJFde/eXbt373baJi0tTc8995w6dOig6tWrKywsTDExMXrvvfeUn59/zXrmzZsnm82mihUrFvn67t271a1bN1WsWFGVK1fWfffdp8OHDztts3//fk2cOFExMTGqXLmyqlatqo4dO+qzzz4r1F9xj+nZs2c1depUNW7cWEFBQapWrZq6dOmiAwcOSJLq1asnm8123ceV91i8eLHuv/9+NWnSRH5+fqpXr941j8+NHKcHH3ywyPds2rRpkX398MMPGjFihGrXrq2goCDVqVNH/fr1c+k4HT169Jrj/n//7//d0PiAssju7QIAqzt48KCWLFmiVq1aqWfPnlq6dGmJ+1y6dKm+/vprjRgxQi1bttTZs2f1zjvv6O6779YHH3ygYcOGObb9+eef1alTJ1WpUkULFixQcHCwZsyYobvuuks7d+5UkyZNJF0OV4sXL9awYcP0pz/9SQEBAVqzZo0effRR/fvf/9aCBQuKrOXEiROaOHGiateurezs7EKvf//997rrrrvUqlUrffLJJ7pw4YKmTp2qTp06KSUlRTVq1JAkrV+/Xl988YUeeOAB3XHHHcrLy9OyZcs0cOBATZ8+XVOnTnXpmJ45c0ZdunTRjz/+qGeeeUa33XabsrOztX37dp07d06StHLlSuXm5jr2mTdvnubPn6+1a9cqPDzc0X7zzTdLkj788EOlp6erbdu2Kigo0KVLl647Z9c7TpIUEhJSKMyGhIQU2u4///mP7rrrLjVo0ECvvPKK6tatq7S0NK1bt85puxs9TpGRkdqxY0eh9lWrVmnmzJmFAhZQrhgAXpWfn+/43zt37jSSzMKFC0vU508//VSoLS8vz9x2223m5ptvdmqfNGmSCQgIMEePHnW0ZWdnm+rVq5tBgwY52k6ePGkuXrxYqN8xY8YYSSY1NbXIWn7729+a3r17m+HDh5sKFSoUen3gwIGmevXqJjs729F29OhRExAQYJ5++mlH288//2wKCgoK7d+rVy8TGhpqLly44GgrzjF98sknTYUKFcyhQ4eKfL0o06ZNM5LMzz//XOTr//v+vXr1MtHR0dft83rH6Wrtv1ZQUGBatWplWrVq5XRMrlenK397d911lwkNDXWaO6C84dQY4AEXLlzQ7bffroYNGzr9v/v09HTVqlVLd911l+N0kp+f+/8Z1qxZs1Cbv7+/YmJidOzYMaf2lStXqmvXroqOjna0hYWF6b777tPnn3+uvLw8SVKVKlUUEBBQqN+2bdtKunya5df+/ve/a/PmzZozZ06Rdebl5emf//yn+vfvr7CwMEd7dHS0unTpopUrVzraqlevLpvNVuT7nzt3TidPnnS03egxPXfunObNm6eBAweqQYMGN7TPjSjunF7vOBXHli1blJKSonHjxikoKOia25bkb+/QoUPavHmzBg0a5DR3QHlDEAI8IDg4WJ988okyMjI0YsQISVJBQYGGDh0qY4yWLl0qf3//Uq0pLy9PiYmJuuWWWxxt58+f16FDh3TbbbcV2v62227T+fPnC12r82tffvml7Ha7Gjdu7NSekZGhcePG6W9/+5vq1q1b5L6HDh3S+fPnr/r+Bw8e1IULF675/hs3blSNGjWKDH/Xk5SUpLNnz6pRo0Z69NFHVaVKFQUGBqpNmzb64osvit2fK27kOF1x/vx51apVS/7+/qpbt64ef/xxpwAoXQ5CklSpUiX17NlTwcHBqlixon7729/q+++/d1vdCxYskDFGI0eOdFufgDdwjRDgIY0aNdK8efM0ePBgvfHGGzp58qQ2bdqktWvXKjIystTref7553Xw4EGtWrXK0fbLL7/IGKOqVasW2v5KW1ZW1lX7XL9+vT788EM9+eSTqlatmtNrjz32mJo0aaJHH330qvtf6ftq72+M0S+//HLV4zVv3jxt2rRJb7zxhkvB8sSJE5KkmTNn6tZbb9XixYvl5+enV199Vb1799aaNWt0zz33FLvf4riR4yRJLVu2VMuWLdWiRQtJ0ubNm/Xaa69pw4YN2rlzp+MC6ytjeuihhzRw4EB98cUXjgvdO3XqpG+//bbEf3/5+fn64IMP1LRpU3Xs2LFEfQHeRhACPGjQoEHatGmTJk2apPz8fD377LPq3r17qdcxb948/eUvf9FTTz2le++9t9DrRZ1yut5ru3fv1qBBg9S+fXvNmDHD6bXly5fr888/V3Jy8jX7Lsn7r1mzRmPGjNGAAQP0xBNPXPc9ilJQUCBJCgwM1Jo1a1SpUiVJUpcuXdSoUSP9+c9/9mgQKs5xGj9+vNPz7t276/bbb9eAAQP0/vvvO16/MqYOHTpo3rx5ju1btGih22+/XW+//bZefPHFEtW9du1anThxQi+//HKJ+gHKAk6NAR42YsQIXbp0SXa7XWPHji3191+4cKEeeeQRjRo1qtAHV5UqVWSz2Ypc9blyyqWo1Zrk5GR1795djRo1Unx8vNO1KGfOnNGYMWP0xBNPqHbt2jp16pROnTqlixcvSpJOnTqls2fPSpJjFelq72+z2VS5cuVCr61bt0733XefunfvriVLltxQ2CrKlfePjY11hCBJCg0NVefOnQvdQsCdinOcrqZfv36qUKGC/v3vfzvarozp1wGuVatWioyMdMuY5s+fr4CAAKdvHwLlFUEI8KCzZ8/qgQceUOPGjRUSElLq11MsXLhQI0eO1PDhw/XOO+8UCgwhISFq2LCh9uzZU2jfPXv2KCQkpNBFxMnJyerWrZuio6O1fv16p6+PS1JmZqZ++uknvfrqq6pSpYrjsXTpUp09e1ZVqlTR0KFDJV3+unlISMhV379hw4YKDg52al+3bp369u2rzp07a/ny5QoMDHTp2Egq8tqkK4wxHrmQ/YriHKdr+XWdnh5TRkaG/vnPf6pPnz4uXZcFlDWcGgM8aPTo0UpNTdXXX3+t77//XgMGDNBrr71W6DSHJyxatEgjR47U73//e8eN+orSr18/vf766zp27JiioqIkSadPn9aKFSvUp08f2e3/95+JlJQUdevWTXXr1lVCQoKqVKlSqL9atWpp48aNhdr/9re/afPmzVqzZo2qV68uSbLb7erdu7dWrFihl156ybEqk5qaqo0bNxY6TuvXr1ffvn31m9/8RqtWrbrut6KuJzIyUh06dNC2bduUk5Pj+PbTuXPntHnzZrVv375E/V9LcY7T1Xz22Wc6d+6cU509evRQaGio1qxZ43T8du/erfT09BKPafHixbp06ZIefvjhEvUDlBle/Oo+4NPef//9Qvdlefzxx01AQID56quvnLb99NNPzaeffmpmzpxpJJkxY8Y42v7XlfvXbNy48Zrv/cknnxg/Pz/TunVrs23bNrNjxw6nx//eXyYjI8NERkaaW2+91axcudLEx8ebO++801SqVMns27fPsd33339vqlWrZqpWrWo+//zzQn1mZGRcs6ar3Qdn3759pmLFiubOO+808fHxZsWKFaZFixamdu3aTn0mJiaakJAQU69ePfPll18Wev9f38vmRo/ptm3bTGBgoGnfvr1ZuXKlWbVqlenUqZMJCAgw27dvL3Is17uP0Hfffed4r5iYGFOjRg3H8++++67Yx+no0aMmNjbWzJ4928THx5s1a9aYZ555xgQHB5tbbrnFnDlzxmn7V155xUgyw4cPN2vXrjWLFi0yUVFR5qabbjJZWVkuHacrmjZtaqKiopzuQQSUZwQhwAO+/fZbExISYoYPH+7UfuHCBRMTE2Pq1atnfvnlF0e7pKs+/tdTTz1lbDabU0ApyvDhw6/Z55EjR5y2P3jwoOnbt68JCwszoaGh5u677zZJSUlO2yxcuPCafV7vRnzXuiHgrl27zN13321CQ0NNWFiY6du3rzl48KDTNlfCx9Uevw6HN3pMjbkcsjp37mxCQ0NNaGio6dq1q9m2bdtVx3K9IHStWqdNm1bs43Ty5EnTr18/U69ePRMSEmICAwNNo0aNzNNPP21OnTpVZD/vv/++adGihQkMDDTVqlUzQ4cONceOHSu0XXGO07Zt24wkM3Xq1GuOAShPbMYY47blJQAe1bZtW0VHR+vTTz/1dikA4BMIQkA5kZOToxo1aiglJUXNmjXzdjkA4BMIQgAAwLL4+jwAALAsrwahLVu2qHfv3qpdu7ZsNpvTrf+vZvPmzYqJiVFwcLAaNGigd955x/OFAgAAn+TVIHT27Fm1bNlSb7311g1tf+TIEfXs2VOdOnVScnKynn32WY0dO1bLly/3cKUAAMAXlZlrhGw2m1auXKm+fftedZs//vGPWr16tfbt2+doGz16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rtQPP/ygxYsX31A/1atXl7+/f6HVn4yMjEKrRFdERkaqTp06Cg8Pd7Q1a9ZMxhgdP35cjRo1KrRPUFCQgoKCijFCAABgBS6tCE2YMEEPPvigDhw4oODg/7vYuEePHtqyZcsN9xMYGKiYmBglJCQ4tSckJCg2NrbIfTp27Kgff/xRZ86ccbTt379ffn5+qlu3bjFHAgAArMylILRz50498sgjhdrr1Klz1Wt7rmbChAmaN2+eFixYoH379mn8+PFKTU3V6NGjJV0+rTVs2DDH9kOGDFG1atX00EMPae/evdqyZYsmTZqkESNGKCQkxJXhAAAAi3Lp1FhwcHCR37z673//qxo1ahSrr8GDBysrK0svvPCC0tLS1KJFC8XHxys6OlqSlJaWptTUVMf2FStWVEJCgp544gm1adNG1apV06BBg/Tiiy+6MhQAAGBhLt1HaNSoUfr555/1ySefqGrVqvr222/l7++vvn376s4779Trr7/ugVLdh/sIFYOP3LuG+wiVHdxHqOzgPkJlB/cRujFl5j5Cr7zyin7++WfVrFlT58+fV+fOndWwYUNVqlRJf/nLX9xSGAAAgKe5dGosLCxMW7du1Zdffqndu3eroKBArVu3Vrdu3dxdHwAAgMeU6IaKXbt2VdeuXd1VCwAAQKlyKQjNnj27yHabzabg4GA1bNhQd955p/z9/UtUHAAAgCe5FIRee+01/fzzzzp37pyqVKkiY4xOnTql0NBQVaxYURkZGWrQoIE2btyoqKgod9cMAADgFi5dLP3Xv/5Vd9xxhw4cOKCsrCydPHlS+/fvV7t27fTGG28oNTVVtWrV0vjx491dLwAAgNu4tCL03HPPafny5br55psdbQ0bNtQrr7yi/v376/Dhw3rppZfUv39/txUKAADgbi6tCKWlpSkvL69Qe15enuPO0rVr19bp06dLVh0AAIAHuRSEunTpokceeUTJycmOtuTkZD366KOOb5Ht2bNH9evXd0+VAAAAHuBSEJo/f76qVq2qmJgYxy+7t2nTRlWrVtX8+fMlXf4pjFdffdWtxQIAALiTS9cI1apVSwkJCfr++++1f/9+GWPUtGlTNWnSxLFNly5d3FYkAACAJ5TohopNmzZV06ZN3VULAABAqXI5CB0/flyrV69WamqqLl686PTarFmzSlwYAACAp7kUhDZs2KA+ffqofv36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8GFt3bpVwcHBWrlypTIzM/XYY4/p5MmTXCdUAq7MRWxsrJYsWaLBgwfrwoULysvLU58+ffTmm2+WRsn4H+76/PapFaErbDab03NjTKG2621fVDtcU9z5uGLp0qV6/vnntWzZMtWsWdNT5VnKjc5Ffn6+hgwZounTp6tx48alVZ6lFOffRUFBgWw2m5YsWaK2bduqZ8+emjVrlhYtWsSqkBsUZy727t2rsWPHaurUqUpKStLatWt15MgRxw+Fo3S54/Pbp/5vXvXq1eXv718oyWdkZBRKjVfUqlWryO3tdruqVavmsVqtwJX5uGLZsmV6+OGH9emnn6pbt26eLNMSijsXp0+f1q5du5ScnKzHH39c0uUPY2OM7Ha71q9fr65du5ZK7b7GlX8XkZGRqlOnjsLDwx1tzZo1kzFGx48fV6NGjTxas69yZS5mzJihjh07atKkSZKk2267TRUqVFCnTp304osvchahFLnr89unVoQCAwMVExOjhIQEp/aEhATFxsYWuU+HDh0Kbb9+/Xq1adNGAQEBHqvVClyZD+nyStCDDz6ojz76iPPublLcuQgLC9OePXuUkpLieIwePVpNmjRRSkqK2rVrV1ql+xxX/l107NhRP/74o86cOeNo279/v/z8/FS3bl2P1uvLXJmLc+fOyc/P+aPT399f0v+tRqB0uO3zu1iXVpcDV74KOX/+fLN3714zbtw4U6FCBXP06FFjjDHPPPOMeeCBBxzbX/n63fjx483evXvN/Pnz+fq8GxV3Pj766CNjt9vN22+/bdLS0hyPU6dOeWsIPqO4c/FrfGvMfYo7F6dPnzZ169Y1AwYMMN99953ZvHmzadSokRk5cqS3huAzijsXCxcuNHa73cyZM8ccOnTIbN261bRp08a0bdvWW0PwGadPnzbJyckmOTnZSDKzZs0yycnJjlsZeOrz2+eCkDHGvP322yY6OtoEBgaa1q1bm82bNzteGz58uOncubPT9ps2bTK33367CQwMNPXq1TNz584t5Yp9W3Hmo3PnzkZSocfw4cNLv3AfVNx/G/+LIORexZ2Lffv2mW7dupmQkBBTt25dM2HCBHPu3LlSrto3FXcuZs+ebZo3b25CQkJMZGSkGTp0qDl+/HgpV+17Nm7ceM3//nvq89tmDGt5AADAmnzqGiEAAIDiIAgBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggB8Gn16tXT66+/7u0yAJRRBCEAHpOWlqYhQ4aoSZMm8vPz07hx47xdkmw2m1atWuXtMgCUEQQhAB6Tm5urGjVqaMqUKWrZsqW3ywGAQghCAFz27rvvqk6dOiooKHBq79Onj4YPH6569erpjTfe0LBhwxQeHl6svtetW6fg4GCdOnXKqX3s2LHq3Lmz4/ny5ct1yy23KCgoSPXq1dOrr7561T7r1asnSerXr59sNpvj+aFDh3TvvfcqIiJCFStW1B133KF//etfTvumpaWpV69eCgkJUf369fXRRx8VOu2WnZ2tUaNGqWbNmgoLC1PXrl31zTffFGvcAEoXQQiAywYOHKjMzExt3LjR0fbLL79o3bp1Gjp0aIn67tatmypXrqzly5c72vLz8/XJJ584+k5KStKgQYN0//33a8+ePXr++ef1pz/9SYsWLSqyz507d0qSFi5cqLS0NMfzM2fOqGfPnvrXv/6l5ORk3XPPPerdu7dSU1Md+w4bNkw//vijNm3apOXLl+u9995TRkaG43VjjHr16qX09HTFx8crKSlJrVu31t13362TJ0+W6FgA8KCS/losAGvr06ePGTFihOP5u+++a2rVqmXy8vKctuvcubN58skni9X32LFjTdeuXR3P161bZwIDA83JkyeNMcYMGTLEdO/e3WmfSZMmmebNmzueR0dHm9dee83xXJJZuXLldd+7efPm5s033zTGXP7ld0lm586djtcPHDhgJDn63rBhgwkLCzMXLlxw6ufmm28277777g2NF0DpY0UIQIkMHTpUy5cvV25uriRpyZIluv/+++Xv7++Wvjdt2qQff/zR0XfPnj1VpUoVSdK+ffvUsWNHp306duyoAwcOKD8//4bf5+zZs3r66afVvHlzVa5cWRUrVtT333/vWBH673//K7vdrtatWzv2adiwoaMO6fLq1JkzZ1StWjVVrFjR8Thy5IgOHTrk8jEA4Fl2bxcAoHzr3bu3CgoK9MUXX+iOO+5QYmKiZs2a5Za+27Ztq5tvvlkff/yxHn30Ua1cuVILFy50vG6Mkc1mc9rHGFPs95k0aZLWrVunV155RQ0bNlRISIgGDBigixcvXrPP/20vKChQZGSkNm3aVGi7ypUrF7smAKWDIASgREJCQnTfffdpyZIlOnjwoBo3bqyYmBi39T9kyBAtWbJEdevWlZ+fn3r16uV4rXnz5tq6davT9tu3b1fjxo2vuiIVEBBQaLUoMTFRDz74oPr16yfp8jVDR48edbzetGlT5eXlKTk52TG2gwcPOl3I3bp1a6Wnp8tutzsuwgZQ9nFqDECJDR06VF988YUWLFig3//+906vpaSkKCUlRWfOnNHPP/+slJQU7d27t1h97969W3/5y180YMAABQcHO1576qmntGHDBv35z3/W/v379cEHH+itt97SxIkTr9pfvXr1tGHDBqWnp+uXX36RdPk014oVK5SSkqJvvvlGQ4YMcfomXNOmTdWtWzeNGjVKX3/9tZKTkzVq1CiFhIQ4VqS6deumDh06qG/fvlq3bp2OHj2q7du367nnntOuXbtueLwASpl3L1EC4Avy8vJMZGSkkWQOHTrk9JqkQo/o6Ohi9X/HHXcYSebLL78s9Npnn31mmjdvbgICAsxNN91kXn75ZafXf32x9OrVq03Dhg2N3W531HHkyBHTpUsXExISYqKiosxbb71V6OLuH3/80fTo0cMEBQWZ6Oho89FHH5maNWuad955x7FNTk6OeeKJJ0zt2rVNQECAiYqKMkOHDjWpqanFGi+A0mMzxoUT6gBgccePH1dUVJT+9a9/6e677/Z2OQBcRBACgBvw5Zdf6syZM7r11luVlpamp59+WidOnND+/fsVEBDg7fIAuIiLpQF4TcWKFa/62po1a9SpU6dSrObaLl26pGeffVaHDx9WpUqVFBsbqyVLlhCCgHKOFSEAXnPw4MGrvlanTh2FhISUYjUArIggBAAALIuvzwMAAMsiCAEAAMsiCAEAAMsiCAEAAMsiCAEAAMsiCAEAAMsiCAEAAMsiCAEAAMv6/61q4jeMxuWDAAAAAElFTkSuQmCC", "text/plain": [ "
" ] @@ -218,12 +177,6 @@ "cell_type": "code", "execution_count": 8, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:10.929993Z", - "iopub.status.busy": "2023-12-14T21:19:10.929815Z", - "iopub.status.idle": "2023-12-14T21:19:10.935762Z", - "shell.execute_reply": "2023-12-14T21:19:10.935330Z" - }, "tags": [] }, "outputs": [], @@ -245,18 +198,12 @@ "cell_type": "code", "execution_count": 9, "metadata": { - "execution": { - "iopub.execute_input": "2023-12-14T21:19:10.937899Z", - "iopub.status.busy": "2023-12-14T21:19:10.937572Z", - "iopub.status.idle": "2023-12-14T21:19:14.317469Z", - "shell.execute_reply": "2023-12-14T21:19:14.317010Z" - }, "tags": [] }, "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHFCAYAAAAe+pb9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8qNh9FAAAACXBIWXMAAA9hAAAPYQGoP6dpAABAkklEQVR4nO3deXQUVf7+8aezhyVhT1jDvkTcCASBQQQh/oBhcQOFYWcAUZFVQVSWURkXEFBBHLZBAREBxRGBiOyobMENZIcAJkJAEkBISHJ/f3Dor20CJJ3udNL1fp3T59i3q259bt0O/VhVXW0zxhgBAABYkI+nCwAAAPAUghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghDgYSdPntTQoUPVokULlShRQjabTfPnz89Tn7t27dKTTz6p22+/XcWLF1dYWJhat26tr7/+Otvljxw5ooceekglSpRQsWLF1KZNG+3evdthmYSEBL3wwgtq0qSJypQpo5CQEEVFRen9999XRkaGw7J79uxR+/btVaVKFQUHB6tUqVJq0qSJPvzwwyzb3rJli/r376+oqCgFBgbKZrPp2LFjWZY7cOCARo4cqaioKJUoUUKlSpVSs2bN9Mknn2RZ1tl9evnyZdWuXVs2m01vvvmmvb1q1aqy2Wy3fFzfxoIFC/TYY4+pTp068vHxUdWqVW+5bUmaPXu2bDabihUr5vR+kq7NVe/evVWuXDkFBQXpjjvu0Jw5c7Isl9v99NVXX6lJkyYqUqSIypQpo969e+v06dM5GhtQUBGEAA87dOiQFi5cqICAALVr184lfS5evFjbt29X37599dlnn2n27NkKDAzU/fffrwULFjgse+bMGTVv3lwHDhzQ3Llz9fHHH+vKlSu67777tH//fvtyu3bt0oIFC+x9LFu2TC1atNATTzyhf/7znw59nj9/XpUrV9arr76qVatWacGCBapatap69Oihl19+2WHZdevW6auvvlKVKlXUtGnTG45p7dq1+uKLL/Twww9r6dKlWrhwoWrVqqVHH31UEydOdFjW2X364osv6tKlS1naV6xYoW+++cb+6NevnyRp9erVDu3t27eXJH3wwQf6+eefFR0drRo1auRo26dOndLIkSNVoUKFbF/P6X5KTk7W3/72N61bt06vv/66Pvvs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NHToUE+XIUn63//+p7vvvluZmZmeLgXIFwQhoJBISEhQt27dVKdOHfn4+BSID06bzaZPP/3U02XYpaam6qWXXtKLL76Y43X279+vli1bKiwsTEFBQapevbpeeOEFXb161Y2V3pwnA+ff//532Ww2LVq0yCPbB/IbQQgoJFJTU1W2bFmNHTtWd955p6fLKZCWLVumYsWKqXnz5jlex9/fXz179tTatWu1f/9+TZ06Vf/5z380btw4N1ZasPXp00dvv/22p8sA8gVBCCgAZs2apYoVK2Y5HdGxY0f16tVL0rWjBNOmTVPPnj0VGhqaq/7XrFmjoKAgnT9/3qF9yJAhatGihf35smXLdNtttykwMFBVq1bV5MmTb9hn1apVJUkPPvigbDab/fnhw4fVqVMnhYWFqVixYmrUqJG++uorh3UTEhLUvn17BQcHq1q1alq0aFGWoyDJyckaMGCAypUrp5CQELVq1Urff//9Tcf50UcfqWPHjrkad/Xq1dWnTx/deeedioiIUMeOHdW9e3dt3rz5ptuSrh1Nstls+uWXXxzap0yZoqpVq9p/jXzjxo2Kjo5WYGCgypcvr9GjRys9PT3bPu+77z4dP35cw4YNk81mk81mkySdPXtWjz/+uCpVqqQiRYro9ttv1+LFix3WvXDhgrp3766iRYuqfPnyeuutt7KcdktLS9Ozzz6rihUrqmjRomrcuLE2bNjg0E/Hjh21fft2HTly5Jb7ACjsCEJAAfDoo48qKSlJ69evt7f9/vvvWrNmjbp3757n/lu3bq0SJUpo2bJl9raMjAx9/PHH9v537dqlLl266LHHHtOPP/6o8ePH68UXX9T8+fOz7XPHjh2SpHnz5ikhIcH+/OLFi2rXrp2++uorxcXF6YEHHlCHDh0UHx9vX7dnz5769ddftWHDBi1btkzvv/++Tp8+bX/dGKP27dsrMTFRq1at0q5du9SgQQPdf//9Onfu3A3HuXnzZjVs2DBX4/6rQ4cOafXq1Q4B8Ubq1KmjqKgoLVy40KF90aJF6tatm2w2m06dOqV27dqpUaNG+v777zVz5kzNmTNHL7/8crZ9Ll++XJUqVdLEiROVkJCghIQESdKVK1cUFRWl//3vf/rpp580YMAA9ejRQ99995193eHDh2vr1q1auXKlYmNjtXnzZu3evduh/z59+mjr1q366KOP9MMPP+jRRx/V//t//08HDx60LxMREaFy5crlKAwChV4efywWgIt07NjR9O3b1/581qxZJjw83KSnp2dZtkWLFuaZZ57JVf9DhgwxrVq1sj9fs2aNCQgIMOfOnTPGGNOtWzfTpk0bh3VGjRplIiMj7c8jIiLMW2+9ZX8uyaxYseKW246MjDRvv/22Mebar6hLMjt27LC/fvDgQSPJ3ve6detMSEiIuXLlikM/NWrUMLNmzcp2G7///ruRZDZt2pSrcV/XpEkTExgYaCSZAQMGmIyMjFuOyxhjpkyZYqpXr25/vn//fiPJ/Pzzz8YYY55//nlTp04dk5mZaV/m3XffNcWKFbNv46/z+df9fCPt2rUzI0aMMMYYk5KSYvz9/c3SpUvtr58/f94UKVLE3vehQ4eMzWYzp06dcujn/vvvN2PGjHFou/vuu8348eNvvQOAQo4jQkAB0b17dy1btkypqamSpIULF+qxxx6Tr6+vy/rfsGGDfv31V3v/7dq1U8mSJSVJ+/btU7NmzRzWadasmQ4ePKiMjIwcb+fSpUt69tlnFRkZqRIlSqhYsWL65Zdf7EeE9u/fLz8/PzVo0MC+Ts2aNe11SNeOTl28eFGlS5dWsWLF7I+jR4/q8OHD2W738uXLkqSgoKBcjfu6JUuWaPfu3Vq0aJG++OILvfnmmzka72OPPabjx4/r22+/tfd/1113KTIyUtK1/dqkSRP7KS7p2n69ePGiTp48maNtSNeOZL3yyiu644477Ptl7dq19v165MgRXb16VdHR0fZ1QkNDVadOHfvz3bt3yxij2rVrO+zXjRs3ZtmvwcHB+uOPP3JcH1BY+Xm6AADXdOjQQZmZmfriiy/UqFEjbd68WVOmTHFZ/9HR0apRo4Y++ugjPfHEE1qxYoXmzZtnf90Y4/Bhfb0tt0aNGqU1a9bozTffVM2aNRUcHKxHHnlEaWlpN+3zz+2ZmZkqX758lmtXJKlEiRLZrl+6dGnZbDb9/vvvDu23Gvd1lStXliRFRkYqIyNDAwYM0IgRI24ZRMuXL6+WLVtq0aJFuueee7R48WINHDjQYVw32q9/bb+ZyZMn66233tLUqVN1++23q2jRoho6dGiW/XqzOczMzJSvr6927dqVZVzFihVzeH7u3DmVLVs2x/UBhRVBCCgggoOD9dBDD2nhwoU6dOiQateuraioKJduo1u3blq4cKEqVaokHx8ftW/f3v5aZGSktmzZ4rD8tm3bVLt27RuGAX9//yxHizZv3qzevXvrwQcflHTtmqFjx47ZX69bt67S09MVFxdnH9+hQ4ccLmhu0KCBEhMT5efnZ78I+1YCAgIUGRmpvXv3ZrmP0M3GnR1jjK5evZrjINi9e3c999xzevzxx3X48GE99thj9tciIyO1bNkyh0C0bds2FS9eXBUrVrzhWLLbr506ddI//vEPSddCzcGDB1WvXj1JUo0aNeTv76/t27fbQ11KSooOHjxov97p7rvvVkZGhk6fPn3Tb9ZduXJFhw8f1t13352j8QOFGafGgAKke/fu+uKLLzR37lz7B96f7dmzR3v27NHFixd15swZ7dmzR3v37s1V/7t379Yrr7yiRx55xOE00ogRI7Ru3Tr961//0oEDB/Tf//5X77zzjkaOHHnD/qpWrap169YpMTHRfiSmZs2aWr58ufbs2aPvv/9e3bp1c/g2XN26ddW6dWsNGDBA27dvV1xcnAYMGKDg4GB7UGjdurWaNGmizp07a82aNTp27Ji2bdumF154QTt37rxhPQ888ECWMHercS9cuFAff/yx9u3bpyNHjmjp0qUaM2aMunbtKj+/nP2/4kMPPaSUlBQ98cQTatmypUPAGTx4sE6cOKGnn35av/zyiz777DONGzdOw4cPl49P9v8EV61aVZs2bdKpU6eUlJRk36+xsbHatm2b9u3bp4EDByoxMdG+TvHixdWrVy+NGjVK69ev188//6y+ffvKx8fHvl9r166t7t27q2fPnlq+fLmOHj2qHTt26LXXXtOqVavsfX377bcKDAxUkyZNcjR+oFDzzKVJALKTnp5uypcvbySZw4cPZ3ldUpZHRERErrbRqFEjI8l8/fXXWV775JNPTGRkpPH39zdVqlQxb7zxhsPrf72Id+XKlaZmzZrGz8/PXsfRo0dNy5YtTXBwsKlcubJ55513slwM/Ouvv5q2bduawMBAExERYRYtWmTKlStn3nvvPfsyKSkp5umnnzYVKlQw/v7+pnLlyqZ79+4mPj7+hmPbt2+fCQ4ONufPn8/xuD/66CPToEEDU6xYMVO0aFETGRlpXn31VXP58uWb7cYsHn30USPJzJ07N8trGzZsMI0aNTIBAQEmPDzcPPfcc+bq1av21/+6f7755htzxx132C/eNsaYs2fPmk6dOplixYqZcuXKmRdeeMH07NnTdOrUyb5eSkqK6datmylSpIgJDw83U6ZMMdHR0Wb06NH2ZdLS0sxLL71kqlatavz9/U14eLh58MEHzQ8//GBfZsCAAWbgwIG5Gj9QWNmMceIiAABwoZMnT6py5cr66quvdP/99+epry5duujuu+/WmDFjXFRd4XXp0iVVrFhRkydPVr9+/XK0zpkzZ1S3bl3t3LlT1apVc3OFgOdxjRCAfPf111/r4sWLuv3225WQkKBnn31WVatW1b333pvnvt944w2tXLnSBVUWPnFxcfrll18UHR2t5ORkTZw4UZLUqVOnHPdx9OhRzZgxgxAEy+CIEOAl/vqtnz/78ssvc/WzE+62Zs0ajRgxQkeOHFHx4sXVtGlTTZ06VREREZ4uLYvbbrtNx48fz/a1WbNmueSGl64SFxen/v37a//+/QoICFBUVJSmTJmi22+/3dOlAQUWQQjwEocOHbrhaxUrVlRwcHA+VuM9jh8/fsMfYA0LC1Px4sXzuSIArkQQAgAAlsXX5wEAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGURhAAAgGX9f8NCFBY2AD1bAAAAAElFTkSuQmCC", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAHFCAYAAAAe+pb9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy81sbWrAAAACXBIWXMAAA9hAAAPYQGoP6dpAABBxUlEQVR4nO3dd3xUVf7/8fekJ4SEnoRiAtIFKYEosEgR4g9YmgoorCBlARtSxBVxKa7KolJsIEpbV0BUwMU1CFmkRFBpwQZIJ4CJmCAJNZDk/P7gwXwdk0ASZjLJ3Nfz8ZjHwzlz77mfc8+EeXvvnTs2Y4wRAACABXm5uwAAAAB3IQgBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBbnbixAmNHj1a7dq1U7ly5WSz2bR48eKb6nPnzp167LHH1LhxY5UtW1ZhYWHq1KmTvvjiizyXP3z4sO69916VK1dOwcHB6ty5s3bt2uWwTHJysp577jm1atVKlSpVUkhIiKKjo/XOO+8oOzv7uvXMnz9fNptNwcHBeb6+a9cuderUScHBwSpXrpzuvfdeHT582GGZ/fv366mnnlJ0dLTKlSunChUqqE2bNvr4449z9VfYfXr+/HlNmjRJdevWlb+/vypWrKgOHTrowIEDkqSoqCjZbLYbPq5t47333tMDDzygevXqycvLS1FRUdfdPwXZTw8//HCe26xfv36efR07dkxDhgxR1apV5e/vr2rVqql3794Oy6xcuVIPPvigateurcDAQEVFRWnAgAH2cV+TkZGhF198Ue3bt1d4eLiCg4PVuHFjTZ8+XZcuXSrQ2ICSysfdBQBWd/DgQS1ZskRNmzZV165dtWzZspvuc9myZdq2bZuGDBmiJk2a6Pz583r77bd1991361//+pcGDhxoX/bXX39V27ZtVb58eS1cuFABAQGaNm2a2rdvr+3bt6tevXqSroar9957TwMHDtTf//53+fr6as2aNXrkkUf09ddfa+HChXnWcvLkST311FOqWrWq0tPTc72+b98+tW/fXk2bNtWHH36oS5cuadKkSWrbtq12796typUrS5LWrVunzz77TA899JBatmyprKwsLV++XH369NHUqVM1adKkIu3Tc+fOqUOHDvr555/1zDPP6Pbbb1d6erq2bt2qCxcuSJJWrVqlzMxM+zrz58/XggUL9Pnnnys0NNTefuutt0qS/v3vfyslJUUxMTHKycnRlStXbjhnN9pPkhQYGJgrzAYGBuZa7ocfflD79u1Vq1Ytvfrqq6pevbqSk5O1du1ah+WmT5+u8PBwTZw4UbVq1dLx48f10ksvqXnz5vr666912223SZKSkpI0e/ZsPfTQQxo7dqyCg4OVkJCgKVOmKD4+XvHx8bLZbDccI1AiGQBulZ2dbf/v7du3G0lm0aJFN9XnL7/8kqstKyvL3H777ebWW291aB8/frzx9fU1R48etbelp6ebSpUqmb59+9rbTp8+bS5fvpyr38cee8xIMklJSXnW8uc//9l0797dDBo0yJQpUybX63369DGVKlUy6enp9rajR48aX19f8/TTT9vbfv31V5OTk5Nr/W7dupmgoCBz6dIle1th9umTTz5pypQpYw4dOpTn63mZPHmykWR+/fXXPF///fa7detmIiMjb9jnjfZTfu1/lJOTY5o2bWqaNm3qsE/yktf75OTJk8bX19cMHTrU3nbu3Dlz7ty5XMu+8sorRpJJSEi4YV1AScWpMcAFLl26pGbNmql27doO/3efkpKi8PBwtW/f3n46ycvL+X+GVapUydXm7e2t6OhoHT9+3KF91apV6tixoyIjI+1tISEhuvfee/Xpp58qKytLklS+fHn5+vrm6jcmJkbS1dNRf/T+++9r06ZNmjNnTp51ZmVl6b///a/uu+8+hYSE2NsjIyPVoUMHrVq1yt5WqVKlPI86xMTE6MKFCzp9+rS9raD79MKFC5o/f7769OmjWrVqFWidgijsnN5oPxXG5s2btXv3bo0ePVr+/v7XXTav90nVqlVVvXp1h/dJmTJlVKZMmVzLXpv7P76ngNKEIAS4QEBAgD788EOdOnVKQ4YMkSTl5ORowIABMsZo2bJl8vb2LtaasrKylJCQYD/dIUkXL17UoUOHdPvtt+da/vbbb9fFixdzXavzR1988YV8fHxUt25dh/ZTp05p9OjR+uc//6nq1avnue6hQ4d08eLFfLd/8ODBG16DsmHDBlWuXDnPD/Ub2blzp86fP686derokUceUfny5eXn56cWLVros88+K3R/RVGQ/XTNxYsXFR4eLm9vb1WvXl2PP/64QwCUrgYhSSpbtqy6du2qgIAABQcH689//rP27dt3w3oOHz6sY8eOObxP8nPtNF1BlgVKKq4RAlykTp06mj9/vvr166fXXntNp0+f1saNG/X5558rIiKi2OuZMmWKDh48qE8++cTe9ttvv8kYowoVKuRa/lpbWlpavn2uW7dO//73v/Xkk0+qYsWKDq89+uijqlevnh555JF817/Wd37bN8bot99+y3d/zZ8/Xxs3btRrr71WpGB58uRJSVevlWncuLHee+89eXl5acaMGerevbvWrFmje+65p9D9FkZB9pMkNWnSRE2aNFGjRo0kSZs2bdKsWbO0fv16bd++3X6B9bUxDR48WH369NFnn31mv9C9bdu2+u677/Ldn1lZWRo6dKiCg4M1ZsyY69bz3Xff6eWXX1bv3r3zDLJAaUEQAlyob9++2rhxo8aPH6/s7Gw9++yz6ty5c7HXMX/+fL344osaN26cevbsmev1613omt9ru3btUt++fXXnnXdq2rRpDq+tWLFCn376qRITEwt0EW1Rtr9mzRo99thjuv/++/XEE0/ccBt5ycnJkST5+flpzZo1Klu2rCSpQ4cOqlOnjv7xj3+4NAgVZj/9MZh07txZzZo10/333693333X/vq1MbVq1Urz58+3L9+oUSM1a9ZMb731ll544YVc/RtjNHToUCUkJGjFihWqUaNGvrUcPXpUf/7zn1WjRg2HbQClEafGABcbMmSIrly5Ih8fH40aNarYt79o0SKNGDFCw4cP1yuvvOLwWvny5WWz2fI86nPtlEteR2sSExPVuXNn1alTR3FxcQ7Xopw7d06PPfaYnnjiCVWtWlVnzpzRmTNndPnyZUnSmTNndP78eUmyH0XKb/s2m03lypXL9dratWt17733qnPnzlqyZEmRv7F0bfutW7e2hyBJCgoKUrt27XLdQsCZCrOf8tO7d2+VKVNGX3/9tb3t2pj+GOCaNm2qiIiIPMdkjNGwYcP0/vvva/HixXmG5WuOHTumDh06yMfHR+vXr8/z/QGUJgQhwIXOnz+vhx56SHXr1lVgYKCGDRtWrNtftGiRhg0bpkGDBuntt9/OFRgCAwNVu3Ztff/997nW/f777xUYGJjrIuLExER16tRJkZGRWrduncPXxyUpNTVVv/zyi2bMmKHy5cvbH8uWLdP58+dVvnx5DRgwQNLVr5sHBgbmu/3atWsrICDAoX3t2rXq1auX2rVrpxUrVsjPz69I+0bS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XKm5c+fq8OHD2rJli0aNGqWYmBhVrVrVXcMAAAClkFtPjUlSv379lJaWpueff17Jyclq1KiR4uLiFBkZKUlKTk52uKfQww8/rLNnz+rNN9/UuHHjVK5cOXXs2FHTp0931xAAAEApZTMWPJ+UkZGh0NBQxax6Uj5lXHPtkJ93lkv6LW4BPh4yDg+YjwDvK+4uwSn8vLLdXcJNC/S+7O4SnCLQQ95TQV6lfz6CPOQ95eq5uHQuS8/GbFB6erpCQkKc0qfbT40BAAC4C0EIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYFkEIAABYlo+7C3CnlP2V5RUY4JK+jbdxSb/FzsczxmE8YBw2DxiDJNm8c9xdwk3z8in9Y5Akbw8Zh493trtLuGm+PqV/DJLk5+K5yL6QKWmDU/vkiBAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALAsghAAALCsEhGE5syZo5o1ayogIEDR0dFKSEi47vKZmZmaOHGiIiMj5e/vr1tvvVULFy4spmoBAICn8HF3AcuXL9fo0aM1Z84ctWnTRvPmzVOXLl20Z88e3XLLLXmu07dvX/3yyy9asGCBateurVOnTikrK6uYKwcAAKWdzRhj3FnAHXfcoebNm2vu3Ln2tgYNGqhXr16aNm1aruU///xzPfDAAzp8+LAqVKhQpG1mZGQoNDRUt0x/QV6BAUWu/XqMt1t3q/P4eMY4jAeMw+YBY5Akm3eOu0u4aV4+pX8MkuTtIePw8c52dwk3zden9I9BkvxcPBfZFzKVeP9MpaenKyQkxCl9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4Ye52pcvX646dercdFEAAADFoUinxqZOnap+/fpp8+bNatOmjWw2m7788kutX78+z4AEAABQEhXpiNB9992nb775RpUqVdInn3yilStXqlKlStq2bZt69+7t7BoBAABcoshfdIuOjtb777/vzFoAAACKVZGOCHXo0EELFixQenq6s+sBAAAoNkUKQo0bN9Zzzz2n8PBw3Xffffrkk090+fJlZ9cGAADgUkUKQq+//rpOnjyp//znPypbtqwGDRqk8PBwDR8+XJs2bXJ2jQAAAC5R5N+X8PLyUmxsrBYvXqxffvlF8+bN07Zt29SxY0dn1gcAAOAyN/2rICkpKfrggw/0/vvv67vvvlPLli2dURcAAIDLFemIUEZGhhYtWqTOnTurRo0amjt3rrp37679+/frm2++cXaNAAAALlGkI0JhYWEqX768+vbtq5deeomjQAAAoFQqUhD6z3/+o06dOsnL6/oHlLZs2aIWLVrI39+/SMUBAAC4UpFOjcXGxt4wBElSly5ddPLkyaJsAgAAwOWK/K2xgjDGuLJ7AACAm+LSIAQAAFCSEYQAAIBlEYQAAIBluTQI2Ww2V3YPAABwU7hYGgAAWNZN/8TG9Zw9e9aV3QMAANyUQh8R+vbbb/XCCy9ozpw5Sk1NdXgtIyNDQ4YMcVpxAAAArlSoILRu3TrFxMTogw8+0PTp09WgQQNt2LDB/vrFixf1r3/9y+lFAgAAuEKhgtCUKVP01FNP6YcfftDRo0f19NNPq0ePHvr8889dVR8AAIDLFOoaoR9//FH//ve/JV39Rtj48eNVvXp13X///Vq2bJliYmJcUiQAAIArFCoI+fv768yZMw5tDz74oLy8vPTAAw9oxowZzqwNAADApQoVhJo2baoNGzYoOjraob1fv37KycnRoEGDnFocAACAKxUqCD3yyCPavHlznq89+OCDkqR33nnn5qsCAAAoBoW6WLp3796aNWuWBg8erPXr1+e6YeKDDz7o8C0yAACAkqxId5ZOS0tTt27dVL16dY0bN067d+92clkAAACuV6QgtHr1aqWkpGjy5MnauXOnoqOj1bBhQ7300ks6evRoofubM2eOatasqYCAAEVHRyshIaFA623ZskU+Pj5q2rRpobcJAABQ5N8aK1eunIYPH66NGzfq2LFjGjx4sP7973+rdu3ahepn+fLlGj16tCZO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OnVSuXLltGLFCntbdna2PvzwQ3v/O3fuVN++ffXAAw/o+++/15QpU/T3v/9dixcvzrPP7du3S5IWLVqk5ORk+/Nz586pa9eu+t///qfExETdc8896t69u5KSkuzrDhw4UD///LM2btyoFStW6J133tGpU6fsrxtj1K1bN6WkpCguLk47d+5U8+bNdffdd+v06dP5jjMhIUEtWrQo1Lj/6ODBg/r8888dAmJ+6tWrp+joaC1ZssShfenSperfv79sNptOnjyprl27qmXLlvr22281d+5cLViwQC+88EKefa5cuVLVq1fX888/r+TkZCUnJ0uSLl26pOjoaP33v//VDz/8oOHDh+uhhx7SN998Y1937Nix2rJli1avXq34+HglJCRo165dDv0PHjxYW7Zs0QcffKDvvvtOffr00f/7f/9PBw4csC8TGRmpKlWqFCgMAqXeTf5YLAAn6dGjhxkyZIj9+bx580x4eLjJysrKtWy7du3Mk08+Waj+R40aZTp27Gh/vnbtWuPn52dOnz5tjDGmf//+pnPnzg7rjB8/3jRs2ND+PDIy0syaNcv+XJJZtWrVDbfdsGFD88Ybbxhjrv6KuiSzfft2++sHDhwwkux9r1+/3oSEhJhLly459HPrrbeaefPm5bmN3377zUgymzdvLtS4r2nVqpXx9/c3kszw4cNNdnb2DcdljDEzZ840tWrVsj//6aefjCTz448/GmOMefbZZ029evVMTk6OfZm33nrLBAcH27fxx/n8437OT9euXc24ceOMMcZkZGQYX19f89FHH9lfP3PmjAkKCrL3ffDgQWOz2czJkycd+rn77rvNhAkTHNqaNWtmpkyZcuMdAJRyHBECSogBAwZoxYoVyszMlCQtWbJEDzzwgLy9vZ3W/8aNG/Xzzz/b++/atavKly8vSdq7d6/atGnjsE6bNm104MABZWdnF3g758+f19NPP62GDRuqXLlyCg4O1r59++xHhH766Sf5+PioefPm9nVq165tr0O6enTq3LlzqlixooKDg+2PI0eO6NChQ3lu9+LFi5KkgICAQo37muXLl2vXrl1aunSpPvvsM7366qsFGu8DDzygY8eO6euvv7b337RpUzVs2FDS1f3aqlUr+yku6ep+PXfunE6cOFGgbUhXj2S9+OKLuv322+37Zd26dfb9evjwYV25ckUxMTH2dUJDQ1WvXj378127dskYo7p16zrs102bNuXar4GBgbpw4UKB6wNKKx93FwDgqu7duysnJ0efffaZWrZsqYSEBM2cOdNp/cfExOjWW2/VBx98oEceeUSrVq3SokWL7K8bYxw+rK+1Fdb48eO1du1avfrqq6pdu7YCAwN1//336/Lly9ft8/ftOTk5ioiIyHXtiiSVK1cuz/UrVqwom82m3377zaH9RuO+pkaNGpKkhg0bKjs7W8OHD9e4ceNuGEQjIiLUoUMHLV26VHfeeaeWLVumESNGOIwrv/36x/brmTFjhmbNmqXZs2ercePGKlOmjEaPHp1rv15vDnNycuTt7a2dO3fmGldwcLDD89OnT6ty5coFrg8orQhCQAkRGBioe++9V0uWLNHBgwdVt25dRUdHO3Ub/fv315IlS1S9enV5eXmpW7du9tcaNmyoL7/80mH5rVu3qm7duvmGAV9f31xHixISEvTwww+rd+/ekq5eM3T06FH76/Xr11dWVpYSExPt4zt48KDDBc3NmzdXSkqKfHx87Bdh34ifn58aNmyoPXv25LqP0PXGnRdjjK5cuVLgIDhgwAD97W9/04MPPqhDhw7pgQcesL/WsGFDrVixwiEQbd26VWXLllW1atXyHUte+7Vnz576y1/+IulqqDlw4IAaNGggSbr11lvl6+urbdu22UNdRkaGDhw4YL/eqVmzZsrOztapU6eu+826S5cu6dChQ2rWrFmBxg+UZpwaA0qQAQMG6LPPPtPChQvtH3i/t3v3bu3evVvnzp3Tr7/+qt27d2vPnj2F6n/Xrl168cUXdf/99zucRho3bpzWr1+vf/zjH9q/f7/+9a9/6c0339RTTz2Vb39RUVFav369UlJS7EdiateurZUrV2r37t369ttv1b9/f4dvw9WvX1+dOnXS8OHDtW3bNiUmJmr48OEKDAy0B4VOnTqpVatW6tWrl9auXaujR49q69ateu6557Rjx45867nnnntyhbkbjXvJkiX68MMPtXfvXh0+fFgfffSRJkyYoH79+snHp2D/r3jvvfcqIyNDjzzyiDp06OAQcB599FEdP35cTzzxhPbt26f//Oc/mjx5ssaOHSsvr7z/CY6KitLmzZt18uRJpaam2vdrfHy8tm7dqr1792rEiBFKSUmxr1O2bFkNGjRI48eP14YNG/Tjjz9qyJAh8vLysu/XunXrasCAARo4cKBWrlypI0eOaPv27Zo+fbri4uLsfX399dfy9/dXq1atCjR+oFRzz6VJAPKSlZVlIiIijCRz6NChXK9LyvWIjIws1DZatmxpJJkvvvgi12sff/yxadiwofH19TW33HKLeeWVVxxe/+NFvKtXrza1a9c2Pj4+9jqOHDliOnToYAIDA02NGjXMm2++meti4J9//tl06dLF+Pv7m8jISLN06VJTpUoV8/bbb9uXycjIME888YSpWrWq8fX1NTVq1DADBgwwSUlJ+Y5t7969JjAw0Jw5c6bA4/7ggw9M8+bNTXBwsClTpoxp2LCheemll8zFixevtxtz6dOnj5FkFi5cmOu1jRs3mpYtWxo/Pz8THh5u/va3v5krV67YX//j/vnqq6/M7bffbr942xhj0tLSTM+ePU1wcLCpUqWKee6558zAgQNNz5497etlZGSY/v37m6CgIBMeHm5mzpxpYmJizDPPPGNf5vLly2bSpEkmKirK+Pr6mvDwcNO7d2/z3Xff2ZcZPny4GTFiRKHGD5RWNmOKcBEAADjRiRMnVKNGDf3vf//T3XfffVN99e3bV82aNdOECROcVF3pdf78eVWrVk0zZszQ0KFDC7TOr7/+qvr162vHjh2qWbOmiysE3I9rhAAUuy+++ELnzp1T48aNlZycrKefflpRUVG66667brrvV155RatXr3ZClaVPYmKi9u3bp5iYGKWnp+v555+XJPXs2bPAfRw5ckRz5swhBMEyOCIEeIg/fuvn99asWVOon51wtbVr12rcuHE6fPiwypYtq9atW2v27NmKjIx0d2m53HbbbTp27Fier82bN88pN7x0lsTERA0bNkw//fST/Pz8FB0drZkzZ6px48buLg0osQhCgIc4ePBgvq9Vq1ZNgYGBxViN5zh27Fi+P8AaFhamsmXLFnNFAJyJIAQAACyLr88DAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADLIggBAADL+v+32H6BRszxXgAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -268,11 +215,18 @@ "source": [ "ps.live_plot2D(expt)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "base", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -286,7 +240,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.13" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 624107e8..4901abbf 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -22,7 +22,7 @@ def __init__(self, instrument): self.set_remote_unlocked() self.field_limit = 8 - self.field_rate_limit = 0.1 + self.field_rate_limit = 0.2 self.debug = False self.initialize_properties() From 2ceb4a58e5ec9fa32ef00a60a25af0230e109b7b Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Thu, 29 Feb 2024 12:26:19 -0700 Subject: [PATCH 02/36] updates to oxfordips120 driver --- pyscan/drivers/oxfordips120.py | 315 ++++++++++++++++++++++++++++++--- 1 file changed, 295 insertions(+), 20 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 4901abbf..b75ec38c 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -19,7 +19,7 @@ def __init__(self, instrument): self.instrument.read_termination = '\r' self.instrument.write_termination = '\r' - self.set_remote_unlocked() + self.remote() self.field_limit = 8 self.field_rate_limit = 0.2 @@ -28,13 +28,37 @@ def __init__(self, instrument): self.initialize_properties() def initialize_properties(self): - self.target_field - self.field_sweep_rate + self.field_set_point + self.target_field # legacy + self.field_rate + self.field_sweep_rate # legacy self.get_current - self.get_target_current - self.get_current_sweep_rate + self.get_current_set_point + self.get_target_current # legacy + self.get_current_rate + self.get_current_sweep_rate # legacy self.get_field + def __repr__(self): + status = self.status() + if status.heater(): + value = "heater on" + else: + value = "heater off" + value += "\n" + if status.sweeping(): + value += "sweeping" + else: + value += "at rest" + value += "\n" + value += f"field = {self.get_field()}" + value += "\n" + value += f"field_set_point = {self.field_set_point}" + value += "\n" + value += f"field_rate = {self.field_rate}" + + return value + def hold(self): self.write('$A0') @@ -47,26 +71,93 @@ def to_zero(self): def clamp(self): self.write('$A4') - def set_local_locked(self): - self.write('$C0') + def local(self, locked=False): + ''' + Set local mode. + Keyword arguments: + locked: boolean, defaults to unlocked + ''' - def set_remote_lock(self): - self.write('$C1') + if locked: + self.write('$C0') + else: + self.write('$C2') - def set_local_unlocked(self): - self.write('$C2') + def remote(self, locked=False): + ''' + Set remote mode. + Keyword arguments: + locked: boolean, defaults to unlocked + ''' - def set_remote_unlocked(self): - self.write('$C3') + if locked: + self.write('$C1') + else: + self.write('$C3') + + def heater(self, state): + ''' + Set the state of the heater. Record the time when the heater was changed + ''' + + if state == 'on': + _ = self.query('&H1') + elif state == 'off': + _ = self.query('&H0') + elif state == 'force': + # need to add some checks before allowing heater to be forced + _ = self.query('&H2') + else: + print("State must be 'on', 'off' or 'force'") + + @property + def field_set_point(self): + self._target_field = float(self.query_until_return('R8').replace('R', '')) + return self._target_field + + @field_set_point.setter + def field_set_point(self, new_value): + if (new_value >= -self.field_limit) and (new_value <= self.field_limit): + # normal resolution 0.0001 T + self.write('$J{}'.format(round(new_value, 4))) + self._target_field = round(new_value, 4) + # extended resolution 0.00001 T (see Q4 to set extended resolution) + else: + print(f'Target field out of range, must be {-self.field_limit} <= set point <= {self.field_limit}') + + @property + def field_rate(self): + self._field_rate = float(self.query_until_return('R9').replace('R', '')) + return self._field_rate + + @field_rate.setter + def field_rate(self, new_value): + if (new_value > 0) and (new_value <= self.field_rate_limit): + # normal resolution 0.001 T/min + self.write('$T{}'.format(round(new_value, 3))) + self._field_sweep_rate = round(new_value, 3) + # extended resolution 0.0001 T/min (see Q4 to set extended resolution) + else: + print(f'Sweep rate out of range, must be 0 < rate <= {self.field_rate_limit}') def get_current(self): self._current = float(self.query_until_return('R2').replace('R', '')) return self._current + def get_current_set_point(self): + self._current_set_point = float(self.query_until_return('R5').replace('R', '')) + return self._current_set_point + + # legacy, use get_current_set_point def get_target_current(self): self._target_current = float(self.query_until_return('R5').replace('R', '')) return self._target_current + def get_current_rate(self): + self._current_rate = float(self.query_until_return('R6').replace('R', '')) + return self._current_rate + + # legacy, use get_current_rate def get_current_sweep_rate(self): self._current_sweep_rate = float(self.query_until_return('R6').replace('R', '')) return self._current_sweep_rate @@ -75,6 +166,7 @@ def get_field(self): self._field = float(self.query_until_return('R7').replace('R', '')) return self._field + # legacy, use field_set_point() @property def target_field(self): self._target_field = float(self.query_until_return('R8').replace('R', '')) @@ -101,14 +193,10 @@ def field_sweep_rate(self, new_value): else: print('Sweep rate out of range, must be 0 < rate < {}'.format(self.field_rate_limit)) - def set_heat_on(self): - return self.query('&H0') + def status(self): - def set_heat_off(self): - return self.query('&H1') - - def set_target_field(self, new_value): - self.write('$J{}'.format(round(new_value, 4))) + status_string = self.query_until_return('X') + return Status(status_string) def get_status(self): @@ -188,3 +276,190 @@ def query_until_return(self, query, n=10): return message else: message = self.query('&') + + # --- legacy commands below here --- + # legacy + def set_local_locked(self): + self.write('$C0') + + # legacy + def set_remote_lock(self): + self.write('$C1') + + # legacy + def set_local_unlocked(self): + self.write('$C2') + + # legacy + def set_remote_unlocked(self): + self.write('$C3') + + # legacy, use heater('on') and these are reversed + def set_heat_on(self): + return self.query('&H0') + + # legacy, use heater('off') and these are reversed + def set_heat_off(self): + return self.query('&H1') + + # legacy, must use field_set_point attribute + def set_target_field(self, new_value): + self.write('$J{}'.format(round(new_value, 4))) + print("deprecated: use 'field_set_point = value'") + +class Status(): + def __init__(self, status_string='X00A1C3H1M10P03'): + ''' + Returns: + Tuple of dictionaries, X1, X2, A, C, H, M1 and M2 + Each dictionary has a + 'description': what status is reported + 'value': integer from status string + 'states': text explanation of the status from the integer value + ''' + + # get index values + self.X1 = int(status_string[1]) + self.X2 = int(status_string[2]) + self.A = int(status_string[4]) + self.C = int(status_string[6]) + self.H = int(status_string[8]) + self.M1 = int(status_string[10]) + self.M2 = int(status_string[11]) + + def __repr__(self): + value = f"X1: {self.X1}; {self.X1_value()}" + value += "\n" + value += f"X2: {self.X2}; {self.X2_value()}" + value += "\n" + value += f"A: {self.A}; {self.A_value()}" + value += "\n" + value += f"C: {self.C}; {self.C_value()}" + value += "\n" + value += f"H: {self.H}; {self.H_value()}" + value += "\n" + value += f"M1: {self.M1}; {self.M1_value()}" + value += "\n" + value += f"M2: {self.M2}; {self.M2_value()}" + + return value + + def X1_value(self): + name = 'system status (operation)' + indexed_values = { + 0: 'Normal', + 1: 'Quenched', + 2: 'Over Heated', + 4: 'Warming Up', + 8: 'Fault'} + return indexed_values[self.X1] + + def X2_value(self): + name = 'system status (voltage)' + indexed_values = { + 0: 'Normal', + 1: 'On Positive Voltage Limit', + 2: 'On Negative Voltage Limit', + 4: 'Outside Negative Current Limit', + 8: 'Outside Positive Current Limit'} + return indexed_values[self.X2] + + def A_value(self): + name = 'Activity' + indexed_values = { + 0: 'Hold', + 1: 'To Set Point', + 2: 'To Zero', + 4: 'Clamped'} + return indexed_values[self.A] + + def C_value(self): + name = 'Activity' + indexed_values = { + 0: 'Local & Locked', + 1: 'Remote & Locked', + 2: 'Local & Unlocked', + 3: 'Remote & Unlocked', + 4: 'Auto-Run-Down', + 5: 'Auto-Run-Down', + 6: 'Auto-Run-Down', + 7: 'Auto-Run-Down' } + return indexed_values[self.C] + + def C_value(self): + name = 'LOC/REM status' + indexed_values = { + 0: 'Local & Locked', + 1: 'Remote & Locked', + 2: 'Local & Unlocked', + 3: 'Remote & Unlocked', + 4: 'Auto-Run-Down', + 5: 'Auto-Run-Down', + 6: 'Auto-Run-Down', + 7: 'Auto-Run-Down' } + return indexed_values[self.C] + + def H_value(self): + name = 'Heater' + indexed_values = { + 0: 'Off Magnet at Zero', + 1: 'On', + 2: 'Off Magnet at Field', + 5: 'Heater Fault', + 8: 'No Switch Fitted' } + return indexed_values[self.H] + + def M1_value(self): + name = 'Mode (rate)' + indexed_values = { + 0: 'Amps, Immediate, Fast', + 1: 'Tesla, Immediate, Fast', + 2: 'Amps, Sweep, Fast', + 3: 'Tesla, Sweep, Fast', + 4: 'Amps, Immediate, Slow', + 5: 'Tesla, Immediate, Slow', + 6: 'Amps, Sweep, Slow', + 7: 'Tesla, Sweep, Slow', } + return indexed_values[self.M1] + + def M2_value(self): + name = 'Mode (sweep)' + indexed_values = { + 0: 'At Rest', + 1: 'Sweeping', + 2: 'Sweep Limiting', + 3: 'Sweeping & Sweep Limiting', + 4: 'Polarity Fault', + 5: 'Sweeping & Polarity Fault', + 6: 'Amps, Sweep, Slow', + 7: 'Tesla, Sweep, Slow', } + return indexed_values[self.M2] + + def heater(self): + ''' + heater() True for on and False for off + ''' + if self.H==0: + return False + elif self.H==1: + return True + else: + # magnet persistant + return False + + def sweeping(self): + ''' + True when the field is changing, False when the field is at rest + ''' + + if self.M2 == 0: + return False + else: + return True + + def persistent(self): + if self.H==2: + return True + else: + return False + From 2fa9841a665df866dd3d45734b0bfc1d5e0d2e58 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Mon, 4 Mar 2024 11:13:34 -0700 Subject: [PATCH 03/36] updates to IPS120 driver --- pyscan/drivers/oxfordips120.py | 154 +++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 65 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index b75ec38c..32edcf6d 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +from datetime import datetime +from time import sleep from .instrumentdriver import InstrumentDriver @@ -28,6 +30,7 @@ def __init__(self, instrument): self.initialize_properties() def initialize_properties(self): + self.get_field self.field_set_point self.target_field # legacy self.field_rate @@ -37,21 +40,22 @@ def initialize_properties(self): self.get_target_current # legacy self.get_current_rate self.get_current_sweep_rate # legacy - self.get_field def __repr__(self): status = self.status() if status.heater(): - value = "heater on" + value = "Heater on (status.heater()=True)" else: - value = "heater off" + value = "Heater off (status.heater()=False)" value += "\n" if status.sweeping(): - value += "sweeping" + value += "Field is changing (status.sweeping=True)" else: - value += "at rest" + value += "At rest (status.sweeping=False)" value += "\n" - value += f"field = {self.get_field()}" + value += f"activity: {status.A_value()}" + value += "\n" + value += f"get_field() = {self.get_field()}" value += "\n" value += f"field_set_point = {self.field_set_point}" value += "\n" @@ -100,16 +104,26 @@ def heater(self, state): Set the state of the heater. Record the time when the heater was changed ''' + status = self.status() + if state == 'on': - _ = self.query('&H1') + if not status.heater: + self.time_heater_toggle = datetime.now() + _ = self.query('&H1') elif state == 'off': - _ = self.query('&H0') + if status.heater: + self.time_heater_toggle = datetime.now() + _ = self.query('&H0') elif state == 'force': # need to add some checks before allowing heater to be forced _ = self.query('&H2') else: print("State must be 'on', 'off' or 'force'") + def get_field(self): + self._field = float(self.query_until_return('R7').replace('R', '')) + return self._field + @property def field_set_point(self): self._target_field = float(self.query_until_return('R8').replace('R', '')) @@ -140,6 +154,11 @@ def field_rate(self, new_value): else: print(f'Sweep rate out of range, must be 0 < rate <= {self.field_rate_limit}') + def status(self): + + status_string = self.query_until_return('X') + return Status(status_string) + def get_current(self): self._current = float(self.query_until_return('R2').replace('R', '')) return self._current @@ -148,23 +167,61 @@ def get_current_set_point(self): self._current_set_point = float(self.query_until_return('R5').replace('R', '')) return self._current_set_point - # legacy, use get_current_set_point - def get_target_current(self): - self._target_current = float(self.query_until_return('R5').replace('R', '')) - return self._target_current - def get_current_rate(self): self._current_rate = float(self.query_until_return('R6').replace('R', '')) return self._current_rate - # legacy, use get_current_rate - def get_current_sweep_rate(self): - self._current_sweep_rate = float(self.query_until_return('R6').replace('R', '')) - return self._current_sweep_rate + def goto_field(self, B): + ''' + Setup the magnet to sweep to a magnetic field. - def get_field(self): - self._field = float(self.query_until_return('R7').replace('R', '')) - return self._field + Arguments + B: field set point + ''' + + pass + # check if heater is on + + def query_until_return(self, query, n=10): + + message = self.query(query) + + for i in range(n): + + if len(message) != 0: + return message + else: + message = self.query('&') + + # --- legacy commands below here --- + # legacy + def set_local_locked(self): + self.write('$C0') + + # legacy + def set_remote_lock(self): + self.write('$C1') + + # legacy + def set_local_unlocked(self): + self.write('$C2') + + # legacy + def set_remote_unlocked(self): + self.write('$C3') + + # legacy, use heater('on') and these are reversed + def set_heat_on(self): + return self.query('&H0') + + # legacy, use heater('off') and these are reversed + def set_heat_off(self): + return self.query('&H1') + + # legacy, must use field_set_point attribute + def set_target_field(self, new_value): + self.write('$J{}'.format(round(new_value, 4))) + print("deprecated: use 'field_set_point = value'") # legacy, use field_set_point() @property @@ -180,6 +237,17 @@ def target_field(self, new_value): else: print('Target field out of range, must be 0 < set point < {}'.format(self.field_limit)) + # legacy, use get_current_set_point + def get_target_current(self): + self._target_current = float(self.query_until_return('R5').replace('R', '')) + return self._target_current + + # legacy, use get_current_rate + def get_current_sweep_rate(self): + self._current_sweep_rate = float(self.query_until_return('R6').replace('R', '')) + return self._current_sweep_rate + + # legacy, use field_rate @property def field_sweep_rate(self): self._field_sweep_rate = float(self.query_until_return('R9').replace('R', '')) @@ -193,11 +261,7 @@ def field_sweep_rate(self, new_value): else: print('Sweep rate out of range, must be 0 < rate < {}'.format(self.field_rate_limit)) - def status(self): - - status_string = self.query_until_return('X') - return Status(status_string) - + # legacy, use status() def get_status(self): status = self.query_until_return('X') @@ -266,46 +330,6 @@ def get_status(self): print('Mode: {}; {}'.format(mode1, mode2)) - def query_until_return(self, query, n=10): - - message = self.query(query) - - for i in range(n): - - if len(message) != 0: - return message - else: - message = self.query('&') - - # --- legacy commands below here --- - # legacy - def set_local_locked(self): - self.write('$C0') - - # legacy - def set_remote_lock(self): - self.write('$C1') - - # legacy - def set_local_unlocked(self): - self.write('$C2') - - # legacy - def set_remote_unlocked(self): - self.write('$C3') - - # legacy, use heater('on') and these are reversed - def set_heat_on(self): - return self.query('&H0') - - # legacy, use heater('off') and these are reversed - def set_heat_off(self): - return self.query('&H1') - - # legacy, must use field_set_point attribute - def set_target_field(self, new_value): - self.write('$J{}'.format(round(new_value, 4))) - print("deprecated: use 'field_set_point = value'") class Status(): def __init__(self, status_string='X00A1C3H1M10P03'): From 1e982e111838f443407f19af4a1c12e61ce223bb Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Mon, 4 Mar 2024 15:31:46 -0700 Subject: [PATCH 04/36] IPS120 begin removing legacy methods/attributes from active methods/attributes in the driver --- pyscan/drivers/oxfordips120.py | 44 ++++++++-------------------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 32edcf6d..a2ad3fe2 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -32,14 +32,14 @@ def __init__(self, instrument): def initialize_properties(self): self.get_field self.field_set_point - self.target_field # legacy + # self.target_field # legacy self.field_rate - self.field_sweep_rate # legacy + # self.field_sweep_rate # legacy self.get_current self.get_current_set_point - self.get_target_current # legacy + # self.get_target_current # legacy self.get_current_rate - self.get_current_sweep_rate # legacy + # self.get_current_sweep_rate # legacy def __repr__(self): status = self.status() @@ -126,18 +126,18 @@ def get_field(self): @property def field_set_point(self): - self._target_field = float(self.query_until_return('R8').replace('R', '')) - return self._target_field + self._field_set_point = float(self.query_until_return('R8').replace('R', '')) + return self._field_set_point @field_set_point.setter def field_set_point(self, new_value): if (new_value >= -self.field_limit) and (new_value <= self.field_limit): # normal resolution 0.0001 T self.write('$J{}'.format(round(new_value, 4))) - self._target_field = round(new_value, 4) + self._field_set_point = round(new_value, 4) # extended resolution 0.00001 T (see Q4 to set extended resolution) else: - print(f'Target field out of range, must be {-self.field_limit} <= set point <= {self.field_limit}') + print(f'field_set_point out of range, must be {-self.field_limit} <= set point <= {self.field_limit}') @property def field_rate(self): @@ -149,10 +149,10 @@ def field_rate(self, new_value): if (new_value > 0) and (new_value <= self.field_rate_limit): # normal resolution 0.001 T/min self.write('$T{}'.format(round(new_value, 3))) - self._field_sweep_rate = round(new_value, 3) + self._field_rate = round(new_value, 3) # extended resolution 0.0001 T/min (see Q4 to set extended resolution) else: - print(f'Sweep rate out of range, must be 0 < rate <= {self.field_rate_limit}') + print(f'field_rate out of range, must be 0 < rate <= {self.field_rate_limit}') def status(self): @@ -171,17 +171,6 @@ def get_current_rate(self): self._current_rate = float(self.query_until_return('R6').replace('R', '')) return self._current_rate - def goto_field(self, B): - ''' - Setup the magnet to sweep to a magnetic field. - - Arguments - B: field set point - ''' - - pass - # check if heater is on - def query_until_return(self, query, n=10): message = self.query(query) @@ -397,19 +386,6 @@ def A_value(self): 4: 'Clamped'} return indexed_values[self.A] - def C_value(self): - name = 'Activity' - indexed_values = { - 0: 'Local & Locked', - 1: 'Remote & Locked', - 2: 'Local & Unlocked', - 3: 'Remote & Unlocked', - 4: 'Auto-Run-Down', - 5: 'Auto-Run-Down', - 6: 'Auto-Run-Down', - 7: 'Auto-Run-Down' } - return indexed_values[self.C] - def C_value(self): name = 'LOC/REM status' indexed_values = { From 7616b391e3540d6a1a0ad4b003ea47dd17ab75e3 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Fri, 15 Mar 2024 16:29:19 -0600 Subject: [PATCH 05/36] completed oxford IPS120 driver --- pyscan/drivers/oxfordips120.py | 243 +++++++++++++++++++++++---------- 1 file changed, 170 insertions(+), 73 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index a2ad3fe2..7df50441 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from datetime import datetime -from time import sleep -from .instrumentdriver import InstrumentDriver +from time import sleep, time +from .instrument_driver import InstrumentDriver class OxfordIPS120(InstrumentDriver): @@ -23,6 +23,21 @@ def __init__(self, instrument): self.instrument.write_termination = '\r' self.remote() + # setup timeout for query_until_return + self.timeout = 1 + + # make sure the buffer is empty: + # read status byte by performing a serial poll + # bit 1: byte available + # bit 4: message available + # bit 6: requesting service (reset after read) + stb = self.instrument.read_stb() + if not stb: + stb = self.instrument.read_stb() + if (stb&16): + message = self.instrument.read() + print(f"Message in buffer: {message}") + self.field_limit = 8 self.field_rate_limit = 0.2 @@ -43,15 +58,23 @@ def initialize_properties(self): def __repr__(self): status = self.status() - if status.heater(): - value = "Heater on (status.heater()=True)" + value = "" + if self.quench_status(): + value += "QUENCHED" + if self.heater_status(): + value += "Heater on" else: - value = "Heater off (status.heater()=False)" + value += "Heater off" value += "\n" - if status.sweeping(): - value += "Field is changing (status.sweeping=True)" + if self.sweeping_status(): + value += "Field is changing" else: - value += "At rest (status.sweeping=False)" + value += "At rest" + if self.persistent_status(): + value += "\n" + value += "Magnet is persistent" + value += "\n" + value += f"get_persistent_field() = {self.get_persistent_field()}" value += "\n" value += f"activity: {status.A_value()}" value += "\n" @@ -64,16 +87,32 @@ def __repr__(self): return value def hold(self): - self.write('$A0') + + if not self.remote_status(): + raise IPS120Error('Control commands require the power supply to be in remote mode') + + self.query_until_return('A0') def to_set_point(self): - self.write('$A1') + + if not self.remote_status(): + raise IPS120Error('Control commands require the power supply to be in remote mode') + + self.query_until_return('A1') def to_zero(self): - self.write('$A2') + + if not self.remote_status(): + raise IPS120Error('Control commands require the power supply to be in remote mode') + + self.query_until_return('A2') def clamp(self): - self.write('$A4') + + if not self.remote_status(): + raise IPS120Error('Control commands require the power supply to be in remote mode') + + self.query_until_return('A4') def local(self, locked=False): ''' @@ -104,26 +143,23 @@ def heater(self, state): Set the state of the heater. Record the time when the heater was changed ''' - status = self.status() + if not self.remote_status(): + raise IPS120Error('Control commands require the power supply to be in remote mode') if state == 'on': - if not status.heater: + if not self.heater_status(): self.time_heater_toggle = datetime.now() - _ = self.query('&H1') + _ = self.query_until_return('H1') elif state == 'off': - if status.heater: + if self.heater_status(): self.time_heater_toggle = datetime.now() - _ = self.query('&H0') + _ = self.query_until_return('H0') elif state == 'force': # need to add some checks before allowing heater to be forced - _ = self.query('&H2') + _ = self.query_until_return('H2') else: print("State must be 'on', 'off' or 'force'") - def get_field(self): - self._field = float(self.query_until_return('R7').replace('R', '')) - return self._field - @property def field_set_point(self): self._field_set_point = float(self.query_until_return('R8').replace('R', '')) @@ -154,11 +190,75 @@ def field_rate(self, new_value): else: print(f'field_rate out of range, must be 0 < rate <= {self.field_rate_limit}') + def get_field(self): + self._field = float(self.query_until_return('R7').replace('R', '')) + return self._field + + def get_persistent_field(self): + self._field = float(self.query_until_return('R18').replace('R', '')) + return self._field + def status(self): status_string = self.query_until_return('X') return Status(status_string) + def quench_status(self): + ''' + True when the magnet quenched + ''' + + status = self.status() + if status.X1 == 1: + return True + + def heater_status(self): + ''' + heater() True for on and False for off + ''' + + status = self.status() + if status.H==0: + return False + elif status.H==1: + return True + else: + # magnet persistant + return False + + def sweeping_status(self): + ''' + True when the field is changing, False when the field is at rest + ''' + + status = self.status() + if status.M2 == 0: + return False + else: + return True + + def remote_status(self): + ''' + True when magnet is in remote mode, False when it is in local mode. + ''' + + status = self.status() + if (status.C == 1) or (status.C ==3): + return True + else: + return False + + def persistent_status(self): + ''' + True when the magnet is in persistent mode + ''' + + status = self.status() + if status.H==2: + return True + else: + return False + def get_current(self): self._current = float(self.query_until_return('R2').replace('R', '')) return self._current @@ -171,16 +271,37 @@ def get_current_rate(self): self._current_rate = float(self.query_until_return('R6').replace('R', '')) return self._current_rate - def query_until_return(self, query, n=10): - - message = self.query(query) - - for i in range(n): - - if len(message) != 0: - return message - else: - message = self.query('&') + def query_until_return(self, query, n=10, debug=False): + + # message = self.query(query) + self.write(query) + stb = self.instrument.read_stb() + # wait for message + i = 0 + start_time = time() + while (time()-start_time) Date: Fri, 15 Mar 2024 16:32:13 -0600 Subject: [PATCH 06/36] notebook for testing Oxford IPS120 magnet power supply driver (oxfordips120.py) --- test/drivers/using_OxfordIPS120_driver.ipynb | 578 +++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 test/drivers/using_OxfordIPS120_driver.ipynb diff --git a/test/drivers/using_OxfordIPS120_driver.ipynb b/test/drivers/using_OxfordIPS120_driver.ipynb new file mode 100644 index 00000000..b65695aa --- /dev/null +++ b/test/drivers/using_OxfordIPS120_driver.ipynb @@ -0,0 +1,578 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "114e61cb-2ac0-4a53-bf0e-c7888e554e2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Could not load Keysight SD1\n", + "Could not load Keysight SD1\n", + "pylablib not found, AttocubeANC350 not loaded\n", + "Basler Camera software not found, BaserCamera not loaded\n", + "Helios Camera not installed\n", + "msl not installed, Thorlabs BSC203 driver not loaded\n", + "seabreeze module not found, Ocean Optics not imported\n", + "Failed to load spinapi library.\n", + "spinapi is not installed, PulseBlaster driver not loaded.\n", + "Thorlabs Kinesis not found, ThorlabsBSC203 not loaded\n", + "Thorlabs Kinesis not found, ThorlabsBPC303 not loaded\n", + "Thorlabs Kinesis not found, ThorlabsMFF101 not loaded\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import time\n", + "import threading\n", + "from pathlib import Path\n", + "from time import sleep\n", + "import pyscan as ps\n", + "import ipywidgets as widgets\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8d0c2aef-d8d9-4d6a-aaa4-22aeb849691f", + "metadata": {}, + "outputs": [], + "source": [ + "instruments = ps.ItemAttribute()\n", + "instruments.ips120 = ps.new_instrument(gpib_address=25)\n", + "devices = ps.ItemAttribute()\n", + "devices.magnet = ps.OxfordIPS120(instruments.ips120)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a888bba8-a6aa-44cd-938c-28433e4a52a0", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Heater off\n", + "At rest\n", + "activity: To Zero\n", + "get_field() = 0.0\n", + "field_set_point = 0.05\n", + "field_rate = 0.1" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "devices.magnet" + ] + }, + { + "cell_type": "markdown", + "id": "06ae8d88-9194-432f-9aa9-192556f1bea4", + "metadata": {}, + "source": [ + "## Issues with original magnet driver in pyscan" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "cf6b33a2-ccd5-4898-8410-9cd94efaba10", + "metadata": {}, + "outputs": [], + "source": [ + "# fix clamp()\n", + "# need to ensure that the field is at zero before clamping" + ] + }, + { + "cell_type": "markdown", + "id": "d478caef-dd58-4011-98a5-c98e0689658c", + "metadata": {}, + "source": [ + "## Fix magnet operation by adding a new interface (keeping legacy commands for compatibility)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a949ee96-2c31-45a4-ad21-618d460c7371", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Heater off\n", + "At rest\n", + "activity: To Zero\n", + "get_field() = 0.0\n", + "field_set_point = 0.05\n", + "field_rate = 0.1" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "devices.magnet" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9c303dc9-a9d9-43bf-bbd8-9ec87380c857", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.local() # keyword: locked=False" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a935a8bb-2f23-45bc-a598-11e61ffca0d2", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.remote() # keyword: locked=False" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "14712428-c581-478c-b572-b610c7bdbbe3", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "devices.magnet.heater('off')" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "372d5931-36ca-4c8a-8a15-c7d0baa01164", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.heater('on')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6634fdf8-d0d8-4b95-9b11-cf26bf3e05ea", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "devices.magnet.get_field()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "28cda43b-8a02-4050-9318-cd7cd7c71c03", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0.0" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "devices.magnet.get_persistent_field() # remembers the persistent field when the heater was last turned off" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "5e0bd57a-56e7-432f-9b27-2c63c0dccdf3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.05\n", + "0.1\n" + ] + } + ], + "source": [ + "print(devices.magnet.field_set_point)\n", + "print(devices.magnet.field_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "7f5e22a9-cb14-4c48-be5a-f9e3ee0d9ee1", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.to_set_point()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "e3c46264-dcef-44e9-a36f-44e61a9abe8c", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.hold()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "f5d09c36-fbf5-4b5b-a88f-d44c13e27cd3", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.to_zero()" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "8d9547e5-422c-4d3b-bf9c-cb2478305bda", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "system status (operation) : Normal (X1=0)\n", + "system status (voltage) : Normal (X2=0)\n", + "Activity ; To Zero (A=2)\n", + "LOC/REM status ; Remote & Unlocked (C=3)\n", + "Heater ; On (H=1)\n", + "Mode (rate) ; Tesla, Immediate, Fast (M1=1)\n", + "Mode (sweep) ; At Rest (M2=0)\n" + ] + } + ], + "source": [ + "status = devices.magnet.status()\n", + "print(status)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "354e5ced-6408-465c-9332-d65bd2a46662", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "remote: True\n", + "heater: True\n", + "sweeping: False\n", + "persistent: False\n" + ] + } + ], + "source": [ + "print(f\"remote: {devices.magnet.remote_status()}\")\n", + "print(f\"heater: {devices.magnet.heater_status()}\")\n", + "print(f\"sweeping: {devices.magnet.sweeping_status()}\")\n", + "print(f\"persistent: {devices.magnet.persistent_status()}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ccc48384-25e0-42b3-b183-95db4781ee32", + "metadata": {}, + "source": [ + "## GUI for Oxford IPS120" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "ef553b6a-10f9-4721-baec-504ab9e44300", + "metadata": {}, + "outputs": [], + "source": [ + "heater_button = widgets.ToggleButton(description='heater')\n", + "field_float = widgets.FloatText(description='field')\n", + "activity_buttons = widgets.ToggleButtons(description='activity', options=['hold', 'to set point', 'to zero'])\n", + "field_set_point_float = widgets.FloatText(description='set point')\n", + "field_rate_float = widgets.FloatText(description='rate')\n", + "persistent_button = widgets.ToggleButton(description='persistent', value=False, disabled=True)\n", + "persistent_field_float = widgets.FloatText(description='persistent field', value=0.0, disabled=True)\n", + "layout = widgets.VBox([\n", + " widgets.HBox([field_set_point_float, field_rate_float]), \n", + " activity_buttons,\n", + " heater_button, \n", + " field_float,\n", + " widgets.HBox([persistent_button, persistent_field_float]),\n", + " ])\n", + "\n", + "# relying on a global variable, need to address this\n", + "widget_dict = {}\n", + "widget_dict['heater_button'] = heater_button\n", + "widget_dict['activity_buttons'] = activity_buttons\n", + "widget_dict['field_float'] = field_float\n", + "widget_dict['field_rate_float'] = field_rate_float\n", + "widget_dict['field_set_point_float'] = field_set_point_float\n", + "widget_dict['persistent_button'] = persistent_button\n", + "widget_dict['persistent_field_float'] = persistent_field_float\n", + "start = time.time()\n", + "def update(widget_dict, magnet, event):\n", + " '''\n", + " Update the magnet on a schedule\n", + " '''\n", + " slow_update = 2\n", + " fast_update = 0.5\n", + " update_time = 0.1\n", + " t = threading.current_thread()\n", + " slow_time = time.time()-slow_update # do a slow update right away\n", + " fast_time = time.time()-fast_update # do a fast update right away\n", + " while getattr(t, \"do_run\", True):\n", + " current_time = time.time()\n", + " if (current_time - slow_time) > slow_update:\n", + " slow_time = time.time()\n", + " # slow updates\n", + " event.wait()\n", + " status = magnet.status()\n", + " if status.H == 0:\n", + " widget_dict['heater_button'].value = False\n", + " # enter bad reverse persistent mode\n", + " if (widget_dict['field_float'].value != 0) and not widget_dict['heater_button'].disabled:\n", + " # must return to 0 before the heater can be turned on\n", + " widget_dict['heater_button'].disabled = True\n", + " widget_dict['persistent_button'].value = True\n", + " widget_dict['persistent_button'].button_style = 'danger'\n", + " widget_dict['persistent_button'].disabled = False\n", + " widget_dict['persistent_field_float'].value = 0\n", + " widget_dict['persistent_field_float'].disabled = False\n", + " # exit bad reverse persisent mode\n", + " if widget_dict['field_float'].value == 0 and widget_dict['heater_button'].disabled:\n", + " widget_dict['heater_button'].disabled = False\n", + " widget_dict['persistent_button'].value = False\n", + " widget_dict['persistent_button'].button_style = ''\n", + " widget_dict['persistent_button'].disabled = True\n", + " widget_dict['persistent_field_float'].value = 0\n", + " widget_dict['persistent_field_float'].disabled = True\n", + " if status.H == 1:\n", + " widget_dict['heater_button'].value = True\n", + " # exit persistent\n", + " if not widget_dict['persistent_button'].disabled:\n", + " widget_dict['persistent_button'].value = False\n", + " widget_dict['persistent_button'].disabled = True\n", + " widget_dict['persistent_button'].button_style = ''\n", + " widget_dict['persistent_field_float'].value = 0\n", + " widget_dict['persistent_field_float'].disabled = True\n", + " event.wait()\n", + " magnet.hold()\n", + " widget_dict['field_set_point_float'].disabled = False\n", + " # maybe GUI starts in persistent mode\n", + " try:\n", + " widget_dict['field_set_point_float'].value = nonpersistent_set_point\n", + " except NameError:\n", + " widget_dict['field_set_point_float'].value = magnet.get_field()\n", + " \n", + " if status.H == 2:\n", + " # only allow the heater to turn on when the field and the persistent field are the same\n", + " event.wait()\n", + " if (widget_dict['field_float'].value != magnet.get_persistent_field()) and not widget_dict['heater_button'].disabled:\n", + " widget_dict['heater_button'].disabled = True\n", + " event.wait()\n", + " if (widget_dict['field_float'].value == magnet.get_persistent_field()) and widget_dict['heater_button'].disabled:\n", + " widget_dict['heater_button'].disabled = False\n", + " # enter_persistent\n", + " if not widget_dict['field_set_point_float'].disabled:\n", + " widget_dict['persistent_button'].value = True\n", + " widget_dict['persistent_button'].disabled = False\n", + " widget_dict['persistent_button'].button_style = 'warning'\n", + " event.wait()\n", + " widget_dict['persistent_field_float'].value = magnet.get_persistent_field()\n", + " widget_dict['persistent_field_float'].disabled = False\n", + " event.wait()\n", + " nonpersistent_set_point = magnet.field_set_point\n", + " event.wait()\n", + " magnet.field_set_point = magnet.get_persistent_field()\n", + " widget_dict['field_set_point_float'].disabled = True\n", + " \n", + " if status.A == 0:\n", + " # hold\n", + " widget_dict['activity_buttons'].value = \"hold\"\n", + " elif status.A == 1:\n", + " widget_dict['activity_buttons'].value = \"to set point\"\n", + " elif status.A == 2:\n", + " widget_dict['activity_buttons'].value = \"to zero\"\n", + " event.wait()\n", + " widget_dict['field_set_point_float'].value = magnet.field_set_point\n", + " event.wait()\n", + " widget_dict['field_rate_float'].value = magnet.field_rate\n", + " if (current_time-fast_time) > fast_update:\n", + " fast_time = time.time()\n", + " event.wait()\n", + " widget_dict['field_float'].value = magnet.get_field()\n", + " sleep(update_time)\n", + " print(\"stopping update\")\n", + " \n", + "# @observe(\"heater_button\")\n", + "def heater_button_clicked(change):\n", + " # requires global devices.magnet\n", + " event.clear()\n", + " if change['new']:\n", + " devices.magnet.heater('on')\n", + " else:\n", + " devices.magnet.heater('off')\n", + " event.set()\n", + " \n", + "def activity_buttons_clicked(change):\n", + " event.clear()\n", + " match change['new']:\n", + " case \"to set point\":\n", + " devices.magnet.to_set_point()\n", + " case \"to zero\":\n", + " devices.magnet.to_zero()\n", + " case \"hold\":\n", + " devices.magnet.hold()\n", + " event.set()\n", + "\n", + "def field_set_point_float_changed(change):\n", + " event.clear() # pause the thread\n", + " devices.magnet.field_set_point = change['new']\n", + " event.set() # restart the thread\n", + "\n", + "def field_rate_float_changed(change):\n", + " event.clear() # pause the thread\n", + " devices.magnet.field_rate = change['new']\n", + " event.set() # restart the thread\n", + " \n", + "#heater_button.on_trait_change(heater_button_clicked)\n", + "heater_button.observe(heater_button_clicked, names=\"value\")\n", + "activity_buttons.observe(activity_buttons_clicked, names=\"value\")\n", + "field_set_point_float.observe(field_set_point_float_changed, names=\"value\")\n", + "field_rate_float.observe(field_rate_float_changed, names=\"value\")" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "09af2347-ab72-4f01-a4b1-7f2e3ae01b49", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "13b63bb93a1f4c1086b7aa09985b74ce", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(HBox(children=(FloatText(value=0.0, description='set point'), FloatText(value=0.0, description=…" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "event = threading.Event()\n", + "event.set()\n", + "thread = threading.Thread(target=update, args=(widget_dict, devices.magnet, event))\n", + "display(layout)\n", + "thread.start()" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "6fea831d-2f8e-4c65-8ec6-e9cf4e643bbf", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "True" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "thread.is_alive()" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "b38ef6b0-67b3-4f3b-b507-5fe0d8bb6234", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "stopping update\n" + ] + } + ], + "source": [ + "thread.do_run = False" + ] + } + ], + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 015851bfccdfe6fd2f1201364cc840c223f7c878 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Fri, 15 Mar 2024 16:36:54 -0600 Subject: [PATCH 07/36] reverted 01-example_property_scans.ipynb back to the main branch since it was not changed --- .../01-example_property_scans.ipynb | 57 ++++++++++++++++--- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/demo_notebooks/01-example_property_scans.ipynb b/demo_notebooks/01-example_property_scans.ipynb index 2bb42138..fe8d440e 100644 --- a/demo_notebooks/01-example_property_scans.ipynb +++ b/demo_notebooks/01-example_property_scans.ipynb @@ -11,6 +11,12 @@ "cell_type": "code", "execution_count": 1, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:04.166557Z", + "iopub.status.busy": "2023-12-14T21:19:04.165721Z", + "iopub.status.idle": "2023-12-14T21:19:06.257252Z", + "shell.execute_reply": "2023-12-14T21:19:06.256870Z" + }, "tags": [] }, "outputs": [ @@ -70,6 +76,12 @@ "cell_type": "code", "execution_count": 3, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:06.260031Z", + "iopub.status.busy": "2023-12-14T21:19:06.259668Z", + "iopub.status.idle": "2023-12-14T21:19:06.263607Z", + "shell.execute_reply": "2023-12-14T21:19:06.263143Z" + }, "tags": [] }, "outputs": [], @@ -111,6 +123,12 @@ "cell_type": "code", "execution_count": 4, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:06.269666Z", + "iopub.status.busy": "2023-12-14T21:19:06.269406Z", + "iopub.status.idle": "2023-12-14T21:19:06.275415Z", + "shell.execute_reply": "2023-12-14T21:19:06.275036Z" + }, "tags": [] }, "outputs": [], @@ -132,6 +150,12 @@ "cell_type": "code", "execution_count": 5, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:06.277440Z", + "iopub.status.busy": "2023-12-14T21:19:06.277286Z", + "iopub.status.idle": "2023-12-14T21:19:07.487927Z", + "shell.execute_reply": "2023-12-14T21:19:07.487470Z" + }, "tags": [] }, "outputs": [ @@ -161,6 +185,12 @@ "cell_type": "code", "execution_count": 6, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:07.489935Z", + "iopub.status.busy": "2023-12-14T21:19:07.489733Z", + "iopub.status.idle": "2023-12-14T21:19:07.495266Z", + "shell.execute_reply": "2023-12-14T21:19:07.494893Z" + }, "tags": [] }, "outputs": [], @@ -180,6 +210,12 @@ "cell_type": "code", "execution_count": 7, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:07.497338Z", + "iopub.status.busy": "2023-12-14T21:19:07.497003Z", + "iopub.status.idle": "2023-12-14T21:19:10.927831Z", + "shell.execute_reply": "2023-12-14T21:19:10.927275Z" + }, "tags": [] }, "outputs": [ @@ -211,6 +247,12 @@ "cell_type": "code", "execution_count": 8, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:10.929993Z", + "iopub.status.busy": "2023-12-14T21:19:10.929815Z", + "iopub.status.idle": "2023-12-14T21:19:10.935762Z", + "shell.execute_reply": "2023-12-14T21:19:10.935330Z" + }, "tags": [] }, "outputs": [], @@ -232,6 +274,12 @@ "cell_type": "code", "execution_count": 9, "metadata": { + "execution": { + "iopub.execute_input": "2023-12-14T21:19:10.937899Z", + "iopub.status.busy": "2023-12-14T21:19:10.937572Z", + "iopub.status.idle": "2023-12-14T21:19:14.317469Z", + "shell.execute_reply": "2023-12-14T21:19:14.317010Z" + }, "tags": [] }, "outputs": [ @@ -249,18 +297,11 @@ "source": [ "ps.live_plot2D(expt, data_name=\"vsum\")" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "base", "language": "python", "name": "python3" }, From e09fc97e7e61126ea03be90f6b471f59b2b0f615 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Fri, 15 Mar 2024 17:17:01 -0600 Subject: [PATCH 08/36] adding standard documentation to the IPS120 driver --- pyscan/drivers/oxfordips120.py | 36 ++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 7df50441..0638abc4 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -10,9 +10,41 @@ class OxfordIPS120(InstrumentDriver): Parameters ---------- instrument : - Visa string or an instantiated instrument (return value - from :func:`~pyscan.drivers.newinstrument.new_instrument`) + Visa string or an instantiated instrument (return value from + :func:`.new_instrument`) + Properties + ---------- + field_set_point + field_set_rate + + Methods + ------- + get_field() + returns the magnetic field + get_persistent_field() + returns the magnetic field where the heater was turned off + and the magnet put in persistent mode + remote() + put the power supply in remote mode, keyword argument: locked=False + local() + put the power supply in local mode, keyword argument: locked=False + heater() + "on", "off" or "force" + hold() + hold the magnetic field + to_zero() + sweep the field to zero + to_set_point() + sweep hte field to the set point + heater_status() + True if the heater is on + remote_status() + True if the power supply is in remote mode (required to issue control commands) + sweeping_status() + True if the field is changing + persistent_status() + True if the magnet is in persisent mode ''' def __init__(self, instrument): From deded95b8d25efcf49a3fec34dcd442438a8413b Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Tue, 26 Mar 2024 16:12:45 -0600 Subject: [PATCH 09/36] update ips120 to deal with heater fault --- pyscan/drivers/oxfordips120.py | 38 +----- test/drivers/using_OxfordIPS120_driver.ipynb | 118 +++++++++++++++++-- 2 files changed, 110 insertions(+), 46 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 0638abc4..f8fdb04e 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -10,41 +10,9 @@ class OxfordIPS120(InstrumentDriver): Parameters ---------- instrument : - Visa string or an instantiated instrument (return value from - :func:`.new_instrument`) + Visa string or an instantiated instrument (return value + from :func:`~pyscan.drivers.newinstrument.new_instrument`) - Properties - ---------- - field_set_point - field_set_rate - - Methods - ------- - get_field() - returns the magnetic field - get_persistent_field() - returns the magnetic field where the heater was turned off - and the magnet put in persistent mode - remote() - put the power supply in remote mode, keyword argument: locked=False - local() - put the power supply in local mode, keyword argument: locked=False - heater() - "on", "off" or "force" - hold() - hold the magnetic field - to_zero() - sweep the field to zero - to_set_point() - sweep hte field to the set point - heater_status() - True if the heater is on - remote_status() - True if the power supply is in remote mode (required to issue control commands) - sweeping_status() - True if the field is changing - persistent_status() - True if the magnet is in persisent mode ''' def __init__(self, instrument): @@ -254,6 +222,8 @@ def heater_status(self): return False elif status.H==1: return True + elif status.H==5: + pass # need to have an error mode, heater is on and can be turned off, but it is not on for sweeping else: # magnet persistant return False diff --git a/test/drivers/using_OxfordIPS120_driver.ipynb b/test/drivers/using_OxfordIPS120_driver.ipynb index b65695aa..f6247163 100644 --- a/test/drivers/using_OxfordIPS120_driver.ipynb +++ b/test/drivers/using_OxfordIPS120_driver.ipynb @@ -61,7 +61,7 @@ "text/plain": [ "Heater off\n", "At rest\n", - "activity: To Zero\n", + "activity: Clamped\n", "get_field() = 0.0\n", "field_set_point = 0.05\n", "field_rate = 0.1" @@ -105,7 +105,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 8, "id": "a949ee96-2c31-45a4-ad21-618d460c7371", "metadata": {}, "outputs": [ @@ -114,13 +114,13 @@ "text/plain": [ "Heater off\n", "At rest\n", - "activity: To Zero\n", + "activity: Clamped\n", "get_field() = 0.0\n", "field_set_point = 0.05\n", "field_rate = 0.1" ] }, - "execution_count": 5, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -151,7 +151,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "14712428-c581-478c-b572-b610c7bdbbe3", "metadata": { "scrolled": true @@ -163,12 +163,106 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 34, "id": "372d5931-36ca-4c8a-8a15-c7d0baa01164", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "system status (operation) : Normal (X1=0)\n", + "system status (voltage) : Normal (X2=0)\n", + "Activity ; Clamped (A=4)\n", + "LOC/REM status ; Remote & Unlocked (C=3)\n", + "Heater ; Heater Fault (H=5)\n", + "Mode (rate) ; Amps, Immediate, Fast (M1=0)\n", + "Mode (sweep) ; At Rest (M2=0)\n" + ] + } + ], + "source": [ + "devices.magnet.heater('on')\n", + "status = devices.magnet.status()\n", + "print(status)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "24a036da-2369-4766-87b7-a40b5e553a27", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'X00A4C3H5M00P04'" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "devices.magnet.instrument.query('X')" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "fb8b8eab-84b4-4483-b40b-32ca5810ca57", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "system status (operation) : Normal (X1=0)\n", + "system status (voltage) : Normal (X2=0)\n", + "Activity ; Clamped (A=4)\n", + "LOC/REM status ; Remote & Unlocked (C=3)\n", + "Heater ; Off Magnet at Zero (H=0)\n", + "Mode (rate) ; Amps, Immediate, Fast (M1=0)\n", + "Mode (sweep) ; At Rest (M2=0)\n" + ] + } + ], + "source": [ + "status = devices.magnet.status()\n", + "if status.H == 5:\n", + " print(\"switch heater fault\")\n", + " _ = devices.magnet.instrument.query(\"H0\")\n", + " sleep(0.5)\n", + " status = devices.magnet.status()\n", + "print(status)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "52724b6c-714b-4375-9a10-512e8e038a8d", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'X00A4C3H0M00P04'" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "devices.magnet.heater('on')" + "devices.magnet.instrument.read()" ] }, { @@ -265,7 +359,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, "id": "8d9547e5-422c-4d3b-bf9c-cb2478305bda", "metadata": {}, "outputs": [ @@ -276,10 +370,10 @@ "\n", "system status (operation) : Normal (X1=0)\n", "system status (voltage) : Normal (X2=0)\n", - "Activity ; To Zero (A=2)\n", + "Activity ; Clamped (A=4)\n", "LOC/REM status ; Remote & Unlocked (C=3)\n", - "Heater ; On (H=1)\n", - "Mode (rate) ; Tesla, Immediate, Fast (M1=1)\n", + "Heater ; Heater Fault (H=5)\n", + "Mode (rate) ; Amps, Immediate, Fast (M1=0)\n", "Mode (sweep) ; At Rest (M2=0)\n" ] } From 997dd6efce694895de7f0b86b6c8873682cdab84 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Tue, 26 Mar 2024 16:20:31 -0600 Subject: [PATCH 10/36] recover documentation and keep the changes for the heater_status --- pyscan/drivers/oxfordips120.py | 40 +++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index f8fdb04e..d9cb2ce1 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -10,9 +10,41 @@ class OxfordIPS120(InstrumentDriver): Parameters ---------- instrument : - Visa string or an instantiated instrument (return value - from :func:`~pyscan.drivers.newinstrument.new_instrument`) + Visa string or an instantiated instrument (return value from + :func:`.new_instrument`) + Properties + ---------- + field_set_point + field_set_rate + + Methods + ------- + get_field() + returns the magnetic field + get_persistent_field() + returns the magnetic field where the heater was turned off + and the magnet put in persistent mode + remote() + put the power supply in remote mode, keyword argument: locked=False + local() + put the power supply in local mode, keyword argument: locked=False + heater() + "on", "off" or "force" + hold() + hold the magnetic field + to_zero() + sweep the field to zero + to_set_point() + sweep hte field to the set point + heater_status() + True if the heater is on + remote_status() + True if the power supply is in remote mode (required to issue control commands) + sweeping_status() + True if the field is changing + persistent_status() + True if the magnet is in persisent mode ''' def __init__(self, instrument): @@ -222,10 +254,8 @@ def heater_status(self): return False elif status.H==1: return True - elif status.H==5: - pass # need to have an error mode, heater is on and can be turned off, but it is not on for sweeping else: - # magnet persistant + # magnet persistant or heater fault - may need to have a heater_status object in future return False def sweeping_status(self): From 07ad74a884a66ffbdd67a9bb05524ab4bc689400 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Tue, 26 Mar 2024 16:27:09 -0600 Subject: [PATCH 11/36] complete documentation for oxfordips120.py --- pyscan/drivers/oxfordips120.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index d9cb2ce1..9d2cc728 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -13,10 +13,13 @@ class OxfordIPS120(InstrumentDriver): Visa string or an instantiated instrument (return value from :func:`.new_instrument`) - Properties + Yields ---------- - field_set_point - field_set_rate + Properties which can be get and set: + field_set_point: float + range defined by property field_limit (T): [-8, 8] + field_set_rate: float + range defined by property field_rate_limit (T/min): [0, 0.2] Methods ------- From c292eea83591daf192b8a1aa87acff64f84c732d Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Wed, 27 Mar 2024 14:20:47 -0600 Subject: [PATCH 12/36] oxfordips120.py after black --- pyscan/drivers/oxfordips120.py | 366 +++++++++++++++++---------------- 1 file changed, 189 insertions(+), 177 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 9d2cc728..89cfab9c 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -5,7 +5,7 @@ class OxfordIPS120(InstrumentDriver): - '''Class to control Oxford Instruments Intelligent Power Supply IPS120 Superconducting Magnet Power Supply + """Class to control Oxford Instruments Intelligent Power Supply IPS120 Superconducting Magnet Power Supply Parameters ---------- @@ -26,7 +26,7 @@ class OxfordIPS120(InstrumentDriver): get_field() returns the magnetic field get_persistent_field() - returns the magnetic field where the heater was turned off + returns the magnetic field where the heater was turned off and the magnet put in persistent mode remote() put the power supply in remote mode, keyword argument: locked=False @@ -48,28 +48,27 @@ class OxfordIPS120(InstrumentDriver): True if the field is changing persistent_status() True if the magnet is in persisent mode - ''' + """ def __init__(self, instrument): - super().__init__(instrument) - self.instrument.read_termination = '\r' - self.instrument.write_termination = '\r' + self.instrument.read_termination = "\r" + self.instrument.write_termination = "\r" self.remote() # setup timeout for query_until_return self.timeout = 1 - + # make sure the buffer is empty: # read status byte by performing a serial poll # bit 1: byte available # bit 4: message available # bit 6: requesting service (reset after read) - stb = self.instrument.read_stb() + stb = self.instrument.read_stb() if not stb: stb = self.instrument.read_stb() - if (stb&16): + if stb & 16: message = self.instrument.read() print(f"Message in buffer: {message}") @@ -122,149 +121,158 @@ def __repr__(self): return value def hold(self): - if not self.remote_status(): - raise IPS120Error('Control commands require the power supply to be in remote mode') + raise IPS120Error( + "Control commands require the power supply to be in remote mode" + ) - self.query_until_return('A0') + self.query_until_return("A0") def to_set_point(self): - if not self.remote_status(): - raise IPS120Error('Control commands require the power supply to be in remote mode') + raise IPS120Error( + "Control commands require the power supply to be in remote mode" + ) - self.query_until_return('A1') + self.query_until_return("A1") def to_zero(self): - if not self.remote_status(): - raise IPS120Error('Control commands require the power supply to be in remote mode') + raise IPS120Error( + "Control commands require the power supply to be in remote mode" + ) - self.query_until_return('A2') + self.query_until_return("A2") def clamp(self): - if not self.remote_status(): - raise IPS120Error('Control commands require the power supply to be in remote mode') + raise IPS120Error( + "Control commands require the power supply to be in remote mode" + ) - self.query_until_return('A4') + self.query_until_return("A4") def local(self, locked=False): - ''' + """ Set local mode. Keyword arguments: locked: boolean, defaults to unlocked - ''' + """ if locked: - self.write('$C0') + self.write("$C0") else: - self.write('$C2') + self.write("$C2") def remote(self, locked=False): - ''' + """ Set remote mode. Keyword arguments: locked: boolean, defaults to unlocked - ''' + """ if locked: - self.write('$C1') + self.write("$C1") else: - self.write('$C3') + self.write("$C3") def heater(self, state): - ''' + """ Set the state of the heater. Record the time when the heater was changed - ''' + """ if not self.remote_status(): - raise IPS120Error('Control commands require the power supply to be in remote mode') + raise IPS120Error( + "Control commands require the power supply to be in remote mode" + ) - if state == 'on': + if state == "on": if not self.heater_status(): self.time_heater_toggle = datetime.now() - _ = self.query_until_return('H1') - elif state == 'off': + _ = self.query_until_return("H1") + elif state == "off": if self.heater_status(): self.time_heater_toggle = datetime.now() - _ = self.query_until_return('H0') - elif state == 'force': + _ = self.query_until_return("H0") + elif state == "force": # need to add some checks before allowing heater to be forced - _ = self.query_until_return('H2') + _ = self.query_until_return("H2") else: print("State must be 'on', 'off' or 'force'") @property def field_set_point(self): - self._field_set_point = float(self.query_until_return('R8').replace('R', '')) + self._field_set_point = float(self.query_until_return("R8").replace("R", "")) return self._field_set_point @field_set_point.setter def field_set_point(self, new_value): if (new_value >= -self.field_limit) and (new_value <= self.field_limit): # normal resolution 0.0001 T - self.write('$J{}'.format(round(new_value, 4))) + self.write("$J{}".format(round(new_value, 4))) self._field_set_point = round(new_value, 4) # extended resolution 0.00001 T (see Q4 to set extended resolution) else: - print(f'field_set_point out of range, must be {-self.field_limit} <= set point <= {self.field_limit}') + print( + f"field_set_point out of range, must be {-self.field_limit} <= set point <= {self.field_limit}" + ) @property def field_rate(self): - self._field_rate = float(self.query_until_return('R9').replace('R', '')) + self._field_rate = float(self.query_until_return("R9").replace("R", "")) return self._field_rate @field_rate.setter def field_rate(self, new_value): if (new_value > 0) and (new_value <= self.field_rate_limit): # normal resolution 0.001 T/min - self.write('$T{}'.format(round(new_value, 3))) + self.write("$T{}".format(round(new_value, 3))) self._field_rate = round(new_value, 3) # extended resolution 0.0001 T/min (see Q4 to set extended resolution) else: - print(f'field_rate out of range, must be 0 < rate <= {self.field_rate_limit}') + print( + f"field_rate out of range, must be 0 < rate <= {self.field_rate_limit}" + ) def get_field(self): - self._field = float(self.query_until_return('R7').replace('R', '')) + self._field = float(self.query_until_return("R7").replace("R", "")) return self._field def get_persistent_field(self): - self._field = float(self.query_until_return('R18').replace('R', '')) + self._field = float(self.query_until_return("R18").replace("R", "")) return self._field def status(self): - - status_string = self.query_until_return('X') + status_string = self.query_until_return("X") return Status(status_string) def quench_status(self): - ''' + """ True when the magnet quenched - ''' + """ status = self.status() if status.X1 == 1: return True def heater_status(self): - ''' + """ heater() True for on and False for off - ''' + """ status = self.status() - if status.H==0: + if status.H == 0: return False - elif status.H==1: + elif status.H == 1: return True else: # magnet persistant or heater fault - may need to have a heater_status object in future return False def sweeping_status(self): - ''' + """ True when the field is changing, False when the field is at rest - ''' + """ status = self.status() if status.M2 == 0: @@ -273,51 +281,50 @@ def sweeping_status(self): return True def remote_status(self): - ''' + """ True when magnet is in remote mode, False when it is in local mode. - ''' + """ status = self.status() - if (status.C == 1) or (status.C ==3): + if (status.C == 1) or (status.C == 3): return True else: return False def persistent_status(self): - ''' + """ True when the magnet is in persistent mode - ''' + """ status = self.status() - if status.H==2: + if status.H == 2: return True else: return False def get_current(self): - self._current = float(self.query_until_return('R2').replace('R', '')) + self._current = float(self.query_until_return("R2").replace("R", "")) return self._current def get_current_set_point(self): - self._current_set_point = float(self.query_until_return('R5').replace('R', '')) + self._current_set_point = float(self.query_until_return("R5").replace("R", "")) return self._current_set_point def get_current_rate(self): - self._current_rate = float(self.query_until_return('R6').replace('R', '')) + self._current_rate = float(self.query_until_return("R6").replace("R", "")) return self._current_rate def query_until_return(self, query, n=10, debug=False): - # message = self.query(query) self.write(query) stb = self.instrument.read_stb() # wait for message i = 0 start_time = time() - while (time()-start_time)= -self.field_limit) and (new_value < self.field_limit): - self.write('$J{}'.format(round(new_value, 4))) + self.write("$J{}".format(round(new_value, 4))) self._target_field = round(new_value, 4) else: - print('Target field out of range, must be 0 < set point < {}'.format(self.field_limit)) + print( + "Target field out of range, must be 0 < set point < {}".format( + self.field_limit + ) + ) # legacy, use get_current_set_point def get_target_current(self): - self._target_current = float(self.query_until_return('R5').replace('R', '')) + self._target_current = float(self.query_until_return("R5").replace("R", "")) return self._target_current # legacy, use get_current_rate def get_current_sweep_rate(self): - self._current_sweep_rate = float(self.query_until_return('R6').replace('R', '')) + self._current_sweep_rate = float(self.query_until_return("R6").replace("R", "")) return self._current_sweep_rate # legacy, use field_rate @property def field_sweep_rate(self): - self._field_sweep_rate = float(self.query_until_return('R9').replace('R', '')) + self._field_sweep_rate = float(self.query_until_return("R9").replace("R", "")) return self._field_sweep_rate @field_sweep_rate.setter def field_sweep_rate(self, new_value): if (new_value >= 0) and (new_value < self.field_rate_limit): - self.write('$T{}'.format(round(new_value, 3))) + self.write("$T{}".format(round(new_value, 3))) self._field_sweep_rate = round(new_value, 3) else: - print('Sweep rate out of range, must be 0 < rate < {}'.format(self.field_rate_limit)) + print( + "Sweep rate out of range, must be 0 < rate < {}".format( + self.field_rate_limit + ) + ) # legacy, use status() def get_status(self): + status = self.query_until_return("X") - status = self.query_until_return('X') - - X1 = {0: 'Normal', - 1: 'Quenched', - 2: 'Over Heated', - 4: 'Warming Up', - 8: 'Fault'} + X1 = {0: "Normal", 1: "Quenched", 2: "Over Heated", 4: "Warming Up", 8: "Fault"} status1 = X1[int(status[1])] # X2 = {0: 'Normal', @@ -426,81 +436,81 @@ def get_status(self): status2 = X1[int(status[2])] - print('Status: {}, {}'.format(status1, status2)) + print("Status: {}, {}".format(status1, status2)) - A = {0: 'Hold', - 1: 'To Set Point', - 2: 'To Zero', - 4: 'Clamped'} + A = {0: "Hold", 1: "To Set Point", 2: "To Zero", 4: "Clamped"} activity = A[int(status[4])] - print('Activity: {}'.format(activity)) + print("Activity: {}".format(activity)) - C = {0: 'Local & Locked', - 1: 'Remote & Locked', - 2: 'Local & Unlocked', - 3: 'Remote & Unlocked', - 4: 'Auto-Run-Down', - 5: 'Auto-Run-Down', - 6: 'Auto-Run-Down', - 7: 'Auto-Run-Down'} + C = { + 0: "Local & Locked", + 1: "Remote & Locked", + 2: "Local & Unlocked", + 3: "Remote & Unlocked", + 4: "Auto-Run-Down", + 5: "Auto-Run-Down", + 6: "Auto-Run-Down", + 7: "Auto-Run-Down", + } loc = C[int(status[6])] - print('Local/Remote Status: {}'.format(loc)) + print("Local/Remote Status: {}".format(loc)) - H = {0: 'Off, Magnet at Zero', - 1: 'On', - 2: 'Off, Maget at Field', - 5: 'Heater Fault', - 8: 'No Switch Fitted'} + H = { + 0: "Off, Magnet at Zero", + 1: "On", + 2: "Off, Maget at Field", + 5: "Heater Fault", + 8: "No Switch Fitted", + } heater = H[int(status[8])] - print('Heater: {}'.format(heater)) + print("Heater: {}".format(heater)) - M1 = {0: 'Amps, Fast', - 1: 'Tesla, Fast', - 4: 'Amps, Slow', - 5: 'Tesla, Slow'} + M1 = {0: "Amps, Fast", 1: "Tesla, Fast", 4: "Amps, Slow", 5: "Tesla, Slow"} - M2 = {0: 'at rest', - 1: 'Sweeping', - 2: 'Sweep Limit', - 3: 'Sweeping & Sweep Limiting'} + M2 = { + 0: "at rest", + 1: "Sweeping", + 2: "Sweep Limit", + 3: "Sweeping & Sweep Limiting", + } mode1 = M1[int(status[10])] mode2 = M2[int(status[11])] - print('Mode: {}; {}'.format(mode1, mode2)) + print("Mode: {}; {}".format(mode1, mode2)) -class Status(): - def __init__(self, status_string='X00A1C3H1M10P03'): - ''' +class Status: + def __init__(self, status_string="X00A1C3H1M10P03"): + """ Returns: Tuple of dictionaries, X1, X2, A, C, H, M1 and M2 - Each dictionary has a + Each dictionary has a 'description': what status is reported 'value': integer from status string 'states': text explanation of the status from the integer value - ''' + """ # get index values - self.X1_name = 'system status (operation)' + self.X1_name = "system status (operation)" self.X1 = int(status_string[1]) - self.X2_name = 'system status (voltage)' + self.X2_name = "system status (voltage)" self.X2 = int(status_string[2]) - self.A_name = 'Activity' + self.A_name = "Activity" self.A = int(status_string[4]) - self.C_name = 'LOC/REM status' + self.C_name = "LOC/REM status" self.C = int(status_string[6]) - self.H_name = 'Heater' + self.H_name = "Heater" self.H = int(status_string[8]) - self.M1_name = 'Mode (rate)' + self.M1_name = "Mode (rate)" self.M1 = int(status_string[10]) - self.M2_name = 'Mode (sweep)' + self.M2_name = "Mode (sweep)" self.M2 = int(status_string[11]) def __repr__(self): @@ -523,73 +533,75 @@ def __repr__(self): def X1_value(self): indexed_values = { - 0: 'Normal', - 1: 'Quenched', - 2: 'Over Heated', - 4: 'Warming Up', - 8: 'Fault'} + 0: "Normal", + 1: "Quenched", + 2: "Over Heated", + 4: "Warming Up", + 8: "Fault", + } return indexed_values[self.X1] def X2_value(self): indexed_values = { - 0: 'Normal', - 1: 'On Positive Voltage Limit', - 2: 'On Negative Voltage Limit', - 4: 'Outside Negative Current Limit', - 8: 'Outside Positive Current Limit'} + 0: "Normal", + 1: "On Positive Voltage Limit", + 2: "On Negative Voltage Limit", + 4: "Outside Negative Current Limit", + 8: "Outside Positive Current Limit", + } return indexed_values[self.X2] def A_value(self): - indexed_values = { - 0: 'Hold', - 1: 'To Set Point', - 2: 'To Zero', - 4: 'Clamped'} + indexed_values = {0: "Hold", 1: "To Set Point", 2: "To Zero", 4: "Clamped"} return indexed_values[self.A] def C_value(self): indexed_values = { - 0: 'Local & Locked', - 1: 'Remote & Locked', - 2: 'Local & Unlocked', - 3: 'Remote & Unlocked', - 4: 'Auto-Run-Down', - 5: 'Auto-Run-Down', - 6: 'Auto-Run-Down', - 7: 'Auto-Run-Down' } + 0: "Local & Locked", + 1: "Remote & Locked", + 2: "Local & Unlocked", + 3: "Remote & Unlocked", + 4: "Auto-Run-Down", + 5: "Auto-Run-Down", + 6: "Auto-Run-Down", + 7: "Auto-Run-Down", + } return indexed_values[self.C] def H_value(self): indexed_values = { - 0: 'Off Magnet at Zero', - 1: 'On', - 2: 'Off Magnet at Field', - 5: 'Heater Fault', - 8: 'No Switch Fitted' } + 0: "Off Magnet at Zero", + 1: "On", + 2: "Off Magnet at Field", + 5: "Heater Fault", + 8: "No Switch Fitted", + } return indexed_values[self.H] def M1_value(self): indexed_values = { - 0: 'Amps, Immediate, Fast', - 1: 'Tesla, Immediate, Fast', - 2: 'Amps, Sweep, Fast', - 3: 'Tesla, Sweep, Fast', - 4: 'Amps, Immediate, Slow', - 5: 'Tesla, Immediate, Slow', - 6: 'Amps, Sweep, Slow', - 7: 'Tesla, Sweep, Slow', } + 0: "Amps, Immediate, Fast", + 1: "Tesla, Immediate, Fast", + 2: "Amps, Sweep, Fast", + 3: "Tesla, Sweep, Fast", + 4: "Amps, Immediate, Slow", + 5: "Tesla, Immediate, Slow", + 6: "Amps, Sweep, Slow", + 7: "Tesla, Sweep, Slow", + } return indexed_values[self.M1] def M2_value(self): indexed_values = { - 0: 'At Rest', - 1: 'Sweeping', - 2: 'Sweep Limiting', - 3: 'Sweeping & Sweep Limiting', - 4: 'Polarity Fault', - 5: 'Sweeping & Polarity Fault', - 6: 'Amps, Sweep, Slow', - 7: 'Tesla, Sweep, Slow', } + 0: "At Rest", + 1: "Sweeping", + 2: "Sweep Limiting", + 3: "Sweeping & Sweep Limiting", + 4: "Polarity Fault", + 5: "Sweeping & Polarity Fault", + 6: "Amps, Sweep, Slow", + 7: "Tesla, Sweep, Slow", + } return indexed_values[self.M2] From 2d38539673c83b504e40c2a56223d77586a0271d Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Wed, 27 Mar 2024 14:24:01 -0600 Subject: [PATCH 13/36] remove sleep import since it is unused --- pyscan/drivers/oxfordips120.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 89cfab9c..e532b5e8 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from datetime import datetime -from time import sleep, time +from time import time from .instrument_driver import InstrumentDriver From 0ee216ac2edaf051e92849b70570dfe168b925d1 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Thu, 4 Apr 2024 16:45:22 -0600 Subject: [PATCH 14/36] begin working on add_device_property in the ips120 driver --- pyscan/drivers/oxfordips120.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index e532b5e8..e3020eec 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -79,6 +79,28 @@ def __init__(self, instrument): self.initialize_properties() def initialize_properties(self): + """ + The IPS120 does not have traditional get/set parameters. The control + parameters are set using command. Getting values is accomplished with + either "examine status", X or "read parameter" R {}. + """ + + # control commands + + # self.add_device_property({ + # 'name': '', + # 'write_string': '', + # 'query_string': '', + # 'range': [], + # 'return_type': float}) + + # self.add_device_property({ + # 'name': 'activity', + # 'write_string': 'A{}', + # 'query_string': 'A{}', + # 'range': [], + # 'return_type': float}) + self.get_field self.field_set_point # self.target_field # legacy @@ -90,7 +112,10 @@ def initialize_properties(self): self.get_current_rate # self.get_current_sweep_rate # legacy - def __repr__(self): + def print_state(self): + """ + Print operating state of the IPS120 power supply + """ status = self.status() value = "" if self.quench_status(): @@ -120,6 +145,11 @@ def __repr__(self): return value + def print_status(self): + """ + Print current status of the IPS120 power supply + """ + def hold(self): if not self.remote_status(): raise IPS120Error( From 90eaf24d6e40c78f1ad929f559bdd3f125b57579 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Wed, 1 May 2024 10:12:13 -0600 Subject: [PATCH 15/36] oxford driver, add all read parameter commands, add properties using pyscan approach --- pyscan/drivers/oxfordips120.py | 212 +++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 64 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index e3020eec..7ae19606 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -16,18 +16,33 @@ class OxfordIPS120(InstrumentDriver): Yields ---------- Properties which can be get and set: + current_rate: float + current_set_point: float field_set_point: float - range defined by property field_limit (T): [-8, 8] - field_set_rate: float - range defined by property field_rate_limit (T/min): [0, 0.2] + range defined by property _field_limit (T): [-8, 8] + field_rate: float + range defined by property _field_rate_limit (T/min): [0, 0.2] Methods ------- + print_state() + print_status() + get_current() + get_voltage() + get_measured_current() + get_current_set_point() + get_current_rate() get_field() returns the magnetic field + get_field_set_point() + get_field_rate() + get_persistent_current() get_persistent_field() returns the magnetic field where the heater was turned off and the magnet put in persistent mode + get_trip_field() + get_safe_current_limit_negative() + get_safe_current_limit_positive() remote() put the power supply in remote mode, keyword argument: locked=False local() @@ -57,9 +72,6 @@ def __init__(self, instrument): self.instrument.write_termination = "\r" self.remote() - # setup timeout for query_until_return - self.timeout = 1 - # make sure the buffer is empty: # read status byte by performing a serial poll # bit 1: byte available @@ -72,8 +84,8 @@ def __init__(self, instrument): message = self.instrument.read() print(f"Message in buffer: {message}") - self.field_limit = 8 - self.field_rate_limit = 0.2 + self._field_limit = 8 + self._field_rate_limit = 0.2 self.debug = False self.initialize_properties() @@ -94,23 +106,78 @@ def initialize_properties(self): # 'range': [], # 'return_type': float}) + self.add_device_property( + { + "name": "field_set_point", + "write_string": "J{}", + "query_string": "R8", + "range": [-self._field_limit, self._field_limit], + "return_type": float, + } + ) + + self.add_device_property( + { + "name": "field_rate", + "write_string": "T{}", + "query_string": "R9", + "range": [-self._field_rate_limit, self._field_rate_limit], + "return_type": float, + } + ) + + self.add_device_property( + { + "name": "current_set_point", + "write_string": "I{}", + "query_string": "R5", + "range": [-self._field_limit, self._field_limit], + "return_type": float, + } + ) + + self.add_device_property( + { + "name": "current_rate", + "write_string": "S{}", + "query_string": "R6", + "range": [-self._field_rate_limit, self._field_rate_limit], + "return_type": float, + } + ) + # self.add_device_property({ - # 'name': 'activity', - # 'write_string': 'A{}', - # 'query_string': 'A{}', - # 'range': [], - # 'return_type': float}) + # 'name': 'extended', + # 'write_string': 'Q{}', + # 'query_string': '', + # 'dict_values': {'normal':0, 'extended':4}, + # 'return_type': int}) self.get_field self.field_set_point - # self.target_field # legacy self.field_rate - # self.field_sweep_rate # legacy - self.get_current - self.get_current_set_point - # self.get_target_current # legacy - self.get_current_rate - # self.get_current_sweep_rate # legacy + + def query_until_return(self, query, timeout=1, debug=False): + # message = self.query(query) + self.write(query) + stb = self.instrument.read_stb() + # wait for message + i = 0 + start_time = time() + while (time() - start_time) < timeout: + while not (stb & 16): + if debug: + if stb & 2: + print("byte available but not message") + stb = self.instrument.read_stb() + i += 1 + if debug: + # typically i=4, time = 0.01 + print(f"needed to wait {i}; {time()-start_time} seconds") + message = self.read() + return message + + raise IPS120Error("query_until_return Timeout") def print_state(self): """ @@ -149,6 +216,7 @@ def print_status(self): """ Print current status of the IPS120 power supply """ + pass def hold(self): if not self.remote_status(): @@ -237,14 +305,14 @@ def field_set_point(self): @field_set_point.setter def field_set_point(self, new_value): - if (new_value >= -self.field_limit) and (new_value <= self.field_limit): + if (new_value >= -self._field_limit) and (new_value <= self._field_limit): # normal resolution 0.0001 T self.write("$J{}".format(round(new_value, 4))) self._field_set_point = round(new_value, 4) # extended resolution 0.00001 T (see Q4 to set extended resolution) else: print( - f"field_set_point out of range, must be {-self.field_limit} <= set point <= {self.field_limit}" + f"field_set_point out of range, must be {-self._field_limit} <= set point <= {self._field_limit}" ) @property @@ -254,23 +322,73 @@ def field_rate(self): @field_rate.setter def field_rate(self, new_value): - if (new_value > 0) and (new_value <= self.field_rate_limit): + if (new_value > 0) and (new_value <= self._field_rate_limit): # normal resolution 0.001 T/min self.write("$T{}".format(round(new_value, 3))) self._field_rate = round(new_value, 3) # extended resolution 0.0001 T/min (see Q4 to set extended resolution) else: print( - f"field_rate out of range, must be 0 < rate <= {self.field_rate_limit}" + f"field_rate out of range, must be 0 < rate <= {self._field_rate_limit}" ) + def get_current(self): + self._current = float(self.query_until_return("R0").replace("R", "")) + return self._current + + def get_voltage(self): + self._voltage = float(self.query_until_return("R1").replace("R", "")) + return self._voltage + + def get_measured_current(self): + self._measured_current = float(self.query_until_return("R2").replace("R", "")) + return self._measured_current + + def get_current_set_point(self): + self._current_set_point = float(self.query_until_return("R5").replace("R", "")) + return self._current_set_point + + def get_current_rate(self): + self._current_rate = float(self.query_until_return("R6").replace("R", "")) + return self._current_rate + def get_field(self): self._field = float(self.query_until_return("R7").replace("R", "")) return self._field + def get_field_set_point(self): + self._field_set_point = float(self.query_until_return("R8").replace("R", "")) + return self._field_set_point + + def get_field_rate(self): + self._field_rate = float(self.query_until_return("R9").replace("R", "")) + return self._field_rate + + def get_persistent_current(self): + self._persistent_current = float( + self.query_until_return("R16").replace("R", "") + ) + return self._persistent_current + def get_persistent_field(self): - self._field = float(self.query_until_return("R18").replace("R", "")) - return self._field + self._persistent_field = float(self.query_until_return("R18").replace("R", "")) + return self._persistent_field + + def get_trip_field(self): + self._trip_field = float(self.query_until_return("R19").replace("R", "")) + return self._trip_field + + def get_safe_current_limit_negative(self): + self._safe_current_limit_negative = float( + self.query_until_return("R21").replace("R", "") + ) + return self._safe_current_limit_negative + + def get_safe_current_limit_positive(self): + self._safe_current_limit_positive = float( + self.query_until_return("R22").replace("R", "") + ) + return self._safe_current_limit_positive def status(self): status_string = self.query_until_return("X") @@ -332,40 +450,6 @@ def persistent_status(self): else: return False - def get_current(self): - self._current = float(self.query_until_return("R2").replace("R", "")) - return self._current - - def get_current_set_point(self): - self._current_set_point = float(self.query_until_return("R5").replace("R", "")) - return self._current_set_point - - def get_current_rate(self): - self._current_rate = float(self.query_until_return("R6").replace("R", "")) - return self._current_rate - - def query_until_return(self, query, n=10, debug=False): - # message = self.query(query) - self.write(query) - stb = self.instrument.read_stb() - # wait for message - i = 0 - start_time = time() - while (time() - start_time) < self.timeout: - while not (stb & 16): - if debug: - if stb & 2: - print("byte available but not message") - stb = self.instrument.read_stb() - i += 1 - if debug: - # typically i=4, time = 0.01 - print(f"needed to wait {i}; {time()-start_time} seconds") - message = self.read() - return message - - raise IPS120Error("query_until_return Timeout") - # for i in range(n): # # if len(message) != 0: @@ -413,13 +497,13 @@ def target_field(self): @target_field.setter def target_field(self, new_value): - if (new_value >= -self.field_limit) and (new_value < self.field_limit): + if (new_value >= -self._field_limit) and (new_value < self._field_limit): self.write("$J{}".format(round(new_value, 4))) self._target_field = round(new_value, 4) else: print( "Target field out of range, must be 0 < set point < {}".format( - self.field_limit + self._field_limit ) ) @@ -441,13 +525,13 @@ def field_sweep_rate(self): @field_sweep_rate.setter def field_sweep_rate(self, new_value): - if (new_value >= 0) and (new_value < self.field_rate_limit): + if (new_value >= 0) and (new_value < self._field_rate_limit): self.write("$T{}".format(round(new_value, 3))) self._field_sweep_rate = round(new_value, 3) else: print( "Sweep rate out of range, must be 0 < rate < {}".format( - self.field_rate_limit + self._field_rate_limit ) ) From e49c40b61da64368fef007107f87d4e487a4e295 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Wed, 1 May 2024 11:52:52 -0600 Subject: [PATCH 16/36] add field command so that the driver can be used with Experiment --- pyscan/drivers/oxfordips120.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index e3020eec..300ee99a 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from datetime import datetime -from time import time +from time import time, sleep from .instrument_driver import InstrumentDriver @@ -230,6 +230,19 @@ def heater(self, state): else: print("State must be 'on', 'off' or 'force'") + @property + def field(self): + self._field = self.get_field() + return self._field + + @field.setter + def field_set_point(self, new_value): + self.hold() + self.field_set_point = new_value + self.to_set_point() + while self.sweeping_status(): + sleep(0.1) + @property def field_set_point(self): self._field_set_point = float(self.query_until_return("R8").replace("R", "")) From 686aa959b746f95e086fd35120ae18078c96da15 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Tue, 7 May 2024 13:16:35 -0600 Subject: [PATCH 17/36] update field property to ensure there is enough time before checking if the sweep is complete --- pyscan/drivers/oxfordips120.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 300ee99a..2f27e0f2 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -236,10 +236,12 @@ def field(self): return self._field @field.setter - def field_set_point(self, new_value): + def field(self, new_value): self.hold() self.field_set_point = new_value self.to_set_point() + # check if field is sweeping or at set point (if not either, then wait until it is sweeping) + sleep(0.1) while self.sweeping_status(): sleep(0.1) From 4da200893c8438d75bb471c5774593d66603515a Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Tue, 7 May 2024 14:44:38 -0600 Subject: [PATCH 18/36] modify ips120 driver to align with anticipated (but not yet existing) pyscan driver guide --- pyscan/drivers/oxfordips120.py | 154 ++++++++++++++++++++++----------- 1 file changed, 102 insertions(+), 52 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 289224f4..0c9b5d46 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -13,9 +13,10 @@ class OxfordIPS120(InstrumentDriver): Visa string or an instantiated instrument (return value from :func:`.new_instrument`) - Yields + Attributes ---------- Properties which can be get and set: + field: float current_rate: float current_set_point: float field_set_point: float @@ -90,6 +91,77 @@ def __init__(self, instrument): self.debug = False self.initialize_properties() + def query(self, string, timeout=1, debug=False): + ''' + Overload query for IPS120 + Read until status indicates full message is received. + ''' + # message = self.query(string) + self.write(string) + stb = self.instrument.read_stb() + # wait for message + i = 0 + start_time = time() + while (time() - start_time) < timeout: + while not (stb & 16): + if debug: + if stb & 2: + print("byte available but not message") + stb = self.instrument.read_stb() + i += 1 + if debug: + # typically i=4, time = 0.01 + print(f"needed to wait {i}; {time()-start_time} seconds") + message = self.read() + return message + + raise IPS120Error("IPS120 query Timeout") + + def get_instrument_property(self, obj, settings, debug=False): + ''' + Generator function for a query function of the instrument + that sends the query string and formats the return based on + settings['return_type'] + + Overloaded for IPS120 to remove the first character (repeat of command) + + Parameters + obj : + parent object + settings : dict + settings dictionary + debug : bool + returns query string instead of querying instrument + + Returns + ------- + value formatted to setting's ['return_type'] + ''' + + if not obj.debug: + value = obj.query(settings['query_string']) + value = value[1:] + assert type(value) is str, ".query method for instrument {} did not return string".format(obj) + value = value.strip("\n") + + if ('values' in settings) and ('indexed_' not in settings) and ('dict_' not in settings): + value = settings['return_type'](value) + elif 'indexed_values' in settings: + values = settings['indexed_values'] + value = values[int(value)] + elif 'dict_values' in settings: + dictionary = settings['dict_values'] + value = self.find_first_key(dictionary, value) + else: + value = settings['return_type'](value) + + else: + value = settings['query_string'] + + setattr(obj, '_' + settings['name'], value) + + return value + def initialize_properties(self): """ The IPS120 does not have traditional get/set parameters. The control @@ -157,28 +229,6 @@ def initialize_properties(self): self.field_set_point self.field_rate - def query_until_return(self, query, timeout=1, debug=False): - # message = self.query(query) - self.write(query) - stb = self.instrument.read_stb() - # wait for message - i = 0 - start_time = time() - while (time() - start_time) < timeout: - while not (stb & 16): - if debug: - if stb & 2: - print("byte available but not message") - stb = self.instrument.read_stb() - i += 1 - if debug: - # typically i=4, time = 0.01 - print(f"needed to wait {i}; {time()-start_time} seconds") - message = self.read() - return message - - raise IPS120Error("query_until_return Timeout") - def print_state(self): """ Print operating state of the IPS120 power supply @@ -224,7 +274,7 @@ def hold(self): "Control commands require the power supply to be in remote mode" ) - self.query_until_return("A0") + self.query("A0") def to_set_point(self): if not self.remote_status(): @@ -232,7 +282,7 @@ def to_set_point(self): "Control commands require the power supply to be in remote mode" ) - self.query_until_return("A1") + self.query("A1") def to_zero(self): if not self.remote_status(): @@ -240,7 +290,7 @@ def to_zero(self): "Control commands require the power supply to be in remote mode" ) - self.query_until_return("A2") + self.query("A2") def clamp(self): if not self.remote_status(): @@ -248,7 +298,7 @@ def clamp(self): "Control commands require the power supply to be in remote mode" ) - self.query_until_return("A4") + self.query("A4") def local(self, locked=False): """ @@ -287,14 +337,14 @@ def heater(self, state): if state == "on": if not self.heater_status(): self.time_heater_toggle = datetime.now() - _ = self.query_until_return("H1") + _ = self.query("H1") elif state == "off": if self.heater_status(): self.time_heater_toggle = datetime.now() - _ = self.query_until_return("H0") + _ = self.query("H0") elif state == "force": # need to add some checks before allowing heater to be forced - _ = self.query_until_return("H2") + _ = self.query("H2") else: print("State must be 'on', 'off' or 'force'") @@ -308,14 +358,14 @@ def field(self, new_value): self.hold() self.field_set_point = new_value self.to_set_point() - # check if field is sweeping or at set point (if not either, then wait until it is sweeping) + # TODO check if field is sweeping or at set point (if not either, then wait until it is sweeping) sleep(0.1) while self.sweeping_status(): sleep(0.1) @property def field_set_point(self): - self._field_set_point = float(self.query_until_return("R8").replace("R", "")) + self._field_set_point = float(self.query("R8").replace("R", "")) return self._field_set_point @field_set_point.setter @@ -332,7 +382,7 @@ def field_set_point(self, new_value): @property def field_rate(self): - self._field_rate = float(self.query_until_return("R9").replace("R", "")) + self._field_rate = float(self.query("R9").replace("R", "")) return self._field_rate @field_rate.setter @@ -348,65 +398,65 @@ def field_rate(self, new_value): ) def get_current(self): - self._current = float(self.query_until_return("R0").replace("R", "")) + self._current = float(self.query("R0").replace("R", "")) return self._current def get_voltage(self): - self._voltage = float(self.query_until_return("R1").replace("R", "")) + self._voltage = float(self.query("R1").replace("R", "")) return self._voltage def get_measured_current(self): - self._measured_current = float(self.query_until_return("R2").replace("R", "")) + self._measured_current = float(self.query("R2").replace("R", "")) return self._measured_current def get_current_set_point(self): - self._current_set_point = float(self.query_until_return("R5").replace("R", "")) + self._current_set_point = float(self.query("R5").replace("R", "")) return self._current_set_point def get_current_rate(self): - self._current_rate = float(self.query_until_return("R6").replace("R", "")) + self._current_rate = float(self.query("R6").replace("R", "")) return self._current_rate def get_field(self): - self._field = float(self.query_until_return("R7").replace("R", "")) + self._field = float(self.query("R7").replace("R", "")) return self._field def get_field_set_point(self): - self._field_set_point = float(self.query_until_return("R8").replace("R", "")) + self._field_set_point = float(self.query("R8").replace("R", "")) return self._field_set_point def get_field_rate(self): - self._field_rate = float(self.query_until_return("R9").replace("R", "")) + self._field_rate = float(self.query("R9").replace("R", "")) return self._field_rate def get_persistent_current(self): self._persistent_current = float( - self.query_until_return("R16").replace("R", "") + self.query("R16").replace("R", "") ) return self._persistent_current def get_persistent_field(self): - self._persistent_field = float(self.query_until_return("R18").replace("R", "")) + self._persistent_field = float(self.query("R18").replace("R", "")) return self._persistent_field def get_trip_field(self): - self._trip_field = float(self.query_until_return("R19").replace("R", "")) + self._trip_field = float(self.query("R19").replace("R", "")) return self._trip_field def get_safe_current_limit_negative(self): self._safe_current_limit_negative = float( - self.query_until_return("R21").replace("R", "") + self.query("R21").replace("R", "") ) return self._safe_current_limit_negative def get_safe_current_limit_positive(self): self._safe_current_limit_positive = float( - self.query_until_return("R22").replace("R", "") + self.query("R22").replace("R", "") ) return self._safe_current_limit_positive def status(self): - status_string = self.query_until_return("X") + status_string = self.query("X") return Status(status_string) def quench_status(self): @@ -507,7 +557,7 @@ def set_target_field(self, new_value): # legacy, use field_set_point() @property def target_field(self): - self._target_field = float(self.query_until_return("R8").replace("R", "")) + self._target_field = float(self.query("R8").replace("R", "")) return self._target_field @target_field.setter @@ -524,18 +574,18 @@ def target_field(self, new_value): # legacy, use get_current_set_point def get_target_current(self): - self._target_current = float(self.query_until_return("R5").replace("R", "")) + self._target_current = float(self.query("R5").replace("R", "")) return self._target_current # legacy, use get_current_rate def get_current_sweep_rate(self): - self._current_sweep_rate = float(self.query_until_return("R6").replace("R", "")) + self._current_sweep_rate = float(self.query("R6").replace("R", "")) return self._current_sweep_rate # legacy, use field_rate @property def field_sweep_rate(self): - self._field_sweep_rate = float(self.query_until_return("R9").replace("R", "")) + self._field_sweep_rate = float(self.query("R9").replace("R", "")) return self._field_sweep_rate @field_sweep_rate.setter @@ -552,7 +602,7 @@ def field_sweep_rate(self, new_value): # legacy, use status() def get_status(self): - status = self.query_until_return("X") + status = self.query("X") X1 = {0: "Normal", 1: "Quenched", 2: "Over Heated", 4: "Warming Up", 8: "Fault"} status1 = X1[int(status[1])] From c93ee53c2f9f503813845df0b83aaa3aa998a96b Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Wed, 8 May 2024 17:02:07 -0600 Subject: [PATCH 19/36] change properties so the floats get converted correctly and extra response is dropped on write --- pyscan/drivers/oxfordips120.py | 220 ++++++++++++++++----------------- 1 file changed, 107 insertions(+), 113 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 0c9b5d46..ccadf9b8 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -66,8 +66,8 @@ class OxfordIPS120(InstrumentDriver): True if the magnet is in persisent mode """ - def __init__(self, instrument): - super().__init__(instrument) + def __init__(self, instrument, debug=False): + super().__init__(instrument, debug) self.instrument.read_termination = "\r" self.instrument.write_termination = "\r" @@ -88,10 +88,10 @@ def __init__(self, instrument): self._field_limit = 8 self._field_rate_limit = 0.2 - self.debug = False + self.debug = debug self.initialize_properties() - def query(self, string, timeout=1, debug=False): + def query(self, string, timeout=1): ''' Overload query for IPS120 Read until status indicates full message is received. @@ -104,12 +104,12 @@ def query(self, string, timeout=1, debug=False): start_time = time() while (time() - start_time) < timeout: while not (stb & 16): - if debug: + if self.debug: if stb & 2: print("byte available but not message") stb = self.instrument.read_stb() i += 1 - if debug: + if self.debug: # typically i=4, time = 0.01 print(f"needed to wait {i}; {time()-start_time} seconds") message = self.read() @@ -117,50 +117,50 @@ def query(self, string, timeout=1, debug=False): raise IPS120Error("IPS120 query Timeout") - def get_instrument_property(self, obj, settings, debug=False): - ''' - Generator function for a query function of the instrument - that sends the query string and formats the return based on - settings['return_type'] - - Overloaded for IPS120 to remove the first character (repeat of command) - - Parameters - obj : - parent object - settings : dict - settings dictionary - debug : bool - returns query string instead of querying instrument - - Returns - ------- - value formatted to setting's ['return_type'] - ''' - - if not obj.debug: - value = obj.query(settings['query_string']) - value = value[1:] - assert type(value) is str, ".query method for instrument {} did not return string".format(obj) - value = value.strip("\n") - - if ('values' in settings) and ('indexed_' not in settings) and ('dict_' not in settings): - value = settings['return_type'](value) - elif 'indexed_values' in settings: - values = settings['indexed_values'] - value = values[int(value)] - elif 'dict_values' in settings: - dictionary = settings['dict_values'] - value = self.find_first_key(dictionary, value) - else: - value = settings['return_type'](value) - - else: - value = settings['query_string'] - - setattr(obj, '_' + settings['name'], value) - - return value + # def get_instrument_property(self, obj, settings, debug=False): + # ''' + # Generator function for a query function of the instrument + # that sends the query string and formats the return based on + # settings['return_type'] + # + # Overloaded for IPS120 to remove the first character (repeat of command) + # + # Parameters + # obj : + # parent object + # settings : dict + # settings dictionary + # debug : bool + # returns query string instead of querying instrument + # + # Returns + # ------- + # value formatted to setting's ['return_type'] + # ''' + # + # if not obj.debug: + # value = obj.query(settings['query_string']) + # value = value[1:] + # assert type(value) is str, ".query method for instrument {} did not return string".format(obj) + # value = value.strip("\n") + # + # if ('values' in settings) and ('indexed_' not in settings) and ('dict_' not in settings): + # value = settings['return_type'](value) + # elif 'indexed_values' in settings: + # values = settings['indexed_values'] + # value = values[int(value)] + # elif 'dict_values' in settings: + # dictionary = settings['dict_values'] + # value = self.find_first_key(dictionary, value) + # else: + # value = settings['return_type'](value) + # + # else: + # value = settings['query_string'] + # + # setattr(obj, '_' + settings['name'], value) + # + # return value def initialize_properties(self): """ @@ -181,40 +181,40 @@ def initialize_properties(self): self.add_device_property( { "name": "field_set_point", - "write_string": "J{}", + "write_string": "$J{}", "query_string": "R8", "range": [-self._field_limit, self._field_limit], - "return_type": float, + "return_type": ips120_float, } ) self.add_device_property( { "name": "field_rate", - "write_string": "T{}", + "write_string": "$T{}", "query_string": "R9", "range": [-self._field_rate_limit, self._field_rate_limit], - "return_type": float, + "return_type": ips120_float, } ) self.add_device_property( { "name": "current_set_point", - "write_string": "I{}", + "write_string": "$I{}", "query_string": "R5", "range": [-self._field_limit, self._field_limit], - "return_type": float, + "return_type": ips120_float, } ) self.add_device_property( { "name": "current_rate", - "write_string": "S{}", + "write_string": "$S{}", "query_string": "R6", "range": [-self._field_rate_limit, self._field_rate_limit], - "return_type": float, + "return_type": ips120_float, } ) @@ -228,6 +228,8 @@ def initialize_properties(self): self.get_field self.field_set_point self.field_rate + self.current_set_point + self.current_rate def print_state(self): """ @@ -350,6 +352,9 @@ def heater(self, state): @property def field(self): + ''' + TODO add settings dictionary with range to make this consistent with pyscan property + ''' self._field = self.get_field() return self._field @@ -363,96 +368,76 @@ def field(self, new_value): while self.sweeping_status(): sleep(0.1) - @property def field_set_point(self): - self._field_set_point = float(self.query("R8").replace("R", "")) - return self._field_set_point - - @field_set_point.setter - def field_set_point(self, new_value): - if (new_value >= -self._field_limit) and (new_value <= self._field_limit): - # normal resolution 0.0001 T - self.write("$J{}".format(round(new_value, 4))) - self._field_set_point = round(new_value, 4) - # extended resolution 0.00001 T (see Q4 to set extended resolution) - else: - print( - f"field_set_point out of range, must be {-self._field_limit} <= set point <= {self._field_limit}" - ) - - @property - def field_rate(self): - self._field_rate = float(self.query("R9").replace("R", "")) - return self._field_rate - - @field_rate.setter - def field_rate(self, new_value): - if (new_value > 0) and (new_value <= self._field_rate_limit): - # normal resolution 0.001 T/min - self.write("$T{}".format(round(new_value, 3))) - self._field_rate = round(new_value, 3) - # extended resolution 0.0001 T/min (see Q4 to set extended resolution) - else: - print( - f"field_rate out of range, must be 0 < rate <= {self._field_rate_limit}" - ) + print("use field_set_point property") + + # @property + # def field_rate(self): + # self._field_rate = ips120_float(self.query("R9")) + # return self._field_rate + # + # @field_rate.setter + # def field_rate(self, new_value): + # if (new_value > 0) and (new_value <= self._field_rate_limit): + # # normal resolution 0.001 T/min + # self.write("$T{}".format(round(new_value, 3))) + # self._field_rate = round(new_value, 3) + # # extended resolution 0.0001 T/min (see Q4 to set extended resolution) + # else: + # print( + # f"field_rate out of range, must be 0 < rate <= {self._field_rate_limit}" + # ) def get_current(self): - self._current = float(self.query("R0").replace("R", "")) + self._current = ips120_float(self.query("R0")) return self._current def get_voltage(self): - self._voltage = float(self.query("R1").replace("R", "")) + self._voltage = ips120_float(self.query("R1")) return self._voltage def get_measured_current(self): - self._measured_current = float(self.query("R2").replace("R", "")) + self._measured_current = ips120_float(self.query("R2")) return self._measured_current def get_current_set_point(self): - self._current_set_point = float(self.query("R5").replace("R", "")) + self._current_set_point = ips120_float(self.query("R5")) return self._current_set_point def get_current_rate(self): - self._current_rate = float(self.query("R6").replace("R", "")) + self._current_rate = ips120_float(self.query("R6")) return self._current_rate def get_field(self): - self._field = float(self.query("R7").replace("R", "")) + self._field = ips120_float(self.query("R7")) return self._field def get_field_set_point(self): - self._field_set_point = float(self.query("R8").replace("R", "")) + self._field_set_point = ips120_float(self.query("R8")) return self._field_set_point def get_field_rate(self): - self._field_rate = float(self.query("R9").replace("R", "")) + self._field_rate = ips120_float(self.query("R9")) return self._field_rate def get_persistent_current(self): - self._persistent_current = float( - self.query("R16").replace("R", "") - ) + self._persistent_current = ips120_float(self.query("R16")) return self._persistent_current def get_persistent_field(self): - self._persistent_field = float(self.query("R18").replace("R", "")) + self._persistent_field = ips120_float(self.query("R18")) return self._persistent_field def get_trip_field(self): - self._trip_field = float(self.query("R19").replace("R", "")) + self._trip_field = ips120_float(self.query("R19")) return self._trip_field def get_safe_current_limit_negative(self): - self._safe_current_limit_negative = float( - self.query("R21").replace("R", "") - ) + self._safe_current_limit_negative = ips120_float(self.query("R21")) return self._safe_current_limit_negative def get_safe_current_limit_positive(self): - self._safe_current_limit_positive = float( - self.query("R22").replace("R", "") - ) + self._safe_current_limit_positive = ips120_float(self.query("R22")) return self._safe_current_limit_positive def status(self): @@ -557,7 +542,7 @@ def set_target_field(self, new_value): # legacy, use field_set_point() @property def target_field(self): - self._target_field = float(self.query("R8").replace("R", "")) + self._target_field = ips120_float(self.query("R8")) return self._target_field @target_field.setter @@ -574,18 +559,18 @@ def target_field(self, new_value): # legacy, use get_current_set_point def get_target_current(self): - self._target_current = float(self.query("R5").replace("R", "")) + self._target_current = ips120_float(self.query("R5")) return self._target_current # legacy, use get_current_rate def get_current_sweep_rate(self): - self._current_sweep_rate = float(self.query("R6").replace("R", "")) + self._current_sweep_rate = ips120_float(self.query("R6")) return self._current_sweep_rate # legacy, use field_rate @property def field_sweep_rate(self): - self._field_sweep_rate = float(self.query("R9").replace("R", "")) + self._field_sweep_rate = ips120_float(self.query("R9")) return self._field_sweep_rate @field_sweep_rate.setter @@ -786,3 +771,12 @@ def M2_value(self): class IPS120Error(Exception): pass + + +def ips120_float(string): + ''' + Helper function used with return_type to drop character and convert to a float. + ''' + + return float(string[1:]) + From 3cd3831d182fb828a489826110e81edb25e92b0a Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Fri, 17 May 2024 17:52:39 -0600 Subject: [PATCH 20/36] add read-only properties to replace get_property() to ips120 driver --- pyscan/drivers/instrument_driver.py | 37 ++- pyscan/drivers/oxfordips120.py | 344 ++++++++++++---------------- 2 files changed, 182 insertions(+), 199 deletions(-) diff --git a/pyscan/drivers/instrument_driver.py b/pyscan/drivers/instrument_driver.py index 9eb2411c..761a4212 100644 --- a/pyscan/drivers/instrument_driver.py +++ b/pyscan/drivers/instrument_driver.py @@ -91,31 +91,54 @@ def add_device_property(self, settings): self['_{}_settings'.format(settings['name'])] = settings + # get/set required parameters + required_setting = ['values', 'range', 'indexed_values', 'dict_values'] + + # readonly properties do not need to have a required key in setting + if 'write_string' in settings: + readonly = False + else: + readonly = True + prop_type = '' if 'values' in settings: set_function = self.set_values_property prop_type = 'values' + doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, + prop_type, settings[prop_type]) elif ('range' in settings) and ('ranges' not in settings): set_function = self.set_range_property prop_type = 'range' + doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, + prop_type, settings[prop_type]) elif 'ranges' in settings: assert False, "ranges no longer accepted, must use method to set multiple properties at the same time." elif 'indexed_values' in settings: set_function = self.set_indexed_values_property prop_type = 'indexed_values' + doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, + prop_type, settings[prop_type]) elif 'dict_values' in settings: set_function = self.set_dict_values_property prop_type = 'dict_values' + doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, + prop_type, settings[prop_type]) + elif readonly: + prop_type = 'readonly' + doc_string = "{} : {}".format(settings['name'], settings['return_type'].__name__) else: assert False, "key used but not allowed" - doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, - prop_type, settings[prop_type]) - - property_definition = property( - fget=lambda obj: self.get_instrument_property(obj, settings), - fset=lambda obj, new_value: set_function(obj, new_value, settings), - doc=doc_string) + if 'write_string' not in settings: + property_definition = property( + fget=lambda obj: self.get_instrument_property(obj, settings), + fset=None, + doc=doc_string) + else: + property_definition = property( + fget=lambda obj: self.get_instrument_property(obj, settings), + fset=lambda obj, new_value: set_function(obj, new_value, settings), + doc=doc_string) setattr(self.__class__, settings['name'], property_definition) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index ccadf9b8..6d60aef8 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -15,55 +15,20 @@ class OxfordIPS120(InstrumentDriver): Attributes ---------- - Properties which can be get and set: + Properties: + field: float current_rate: float current_set_point: float field_set_point: float - range defined by property _field_limit (T): [-8, 8] + range defined by property field_limit (T): [-8, 8] field_rate: float - range defined by property _field_rate_limit (T/min): [0, 0.2] + range defined by property field_rate_limit (T/min): [0, 0.2] + + Read-only properties: Methods ------- - print_state() - print_status() - get_current() - get_voltage() - get_measured_current() - get_current_set_point() - get_current_rate() - get_field() - returns the magnetic field - get_field_set_point() - get_field_rate() - get_persistent_current() - get_persistent_field() - returns the magnetic field where the heater was turned off - and the magnet put in persistent mode - get_trip_field() - get_safe_current_limit_negative() - get_safe_current_limit_positive() - remote() - put the power supply in remote mode, keyword argument: locked=False - local() - put the power supply in local mode, keyword argument: locked=False - heater() - "on", "off" or "force" - hold() - hold the magnetic field - to_zero() - sweep the field to zero - to_set_point() - sweep hte field to the set point - heater_status() - True if the heater is on - remote_status() - True if the power supply is in remote mode (required to issue control commands) - sweeping_status() - True if the field is changing - persistent_status() - True if the magnet is in persisent mode """ def __init__(self, instrument, debug=False): @@ -85,8 +50,10 @@ def __init__(self, instrument, debug=False): message = self.instrument.read() print(f"Message in buffer: {message}") - self._field_limit = 8 - self._field_rate_limit = 0.2 + # magnet specific settings + self.field_limit = 8 # oxford triton in 518/1112 + self.field_rate_limit = 0.2 + self.field_to_current = 5/22.456 # Tesla/Amp self.debug = debug self.initialize_properties() @@ -117,51 +84,6 @@ def query(self, string, timeout=1): raise IPS120Error("IPS120 query Timeout") - # def get_instrument_property(self, obj, settings, debug=False): - # ''' - # Generator function for a query function of the instrument - # that sends the query string and formats the return based on - # settings['return_type'] - # - # Overloaded for IPS120 to remove the first character (repeat of command) - # - # Parameters - # obj : - # parent object - # settings : dict - # settings dictionary - # debug : bool - # returns query string instead of querying instrument - # - # Returns - # ------- - # value formatted to setting's ['return_type'] - # ''' - # - # if not obj.debug: - # value = obj.query(settings['query_string']) - # value = value[1:] - # assert type(value) is str, ".query method for instrument {} did not return string".format(obj) - # value = value.strip("\n") - # - # if ('values' in settings) and ('indexed_' not in settings) and ('dict_' not in settings): - # value = settings['return_type'](value) - # elif 'indexed_values' in settings: - # values = settings['indexed_values'] - # value = values[int(value)] - # elif 'dict_values' in settings: - # dictionary = settings['dict_values'] - # value = self.find_first_key(dictionary, value) - # else: - # value = settings['return_type'](value) - # - # else: - # value = settings['query_string'] - # - # setattr(obj, '_' + settings['name'], value) - # - # return value - def initialize_properties(self): """ The IPS120 does not have traditional get/set parameters. The control @@ -171,19 +93,12 @@ def initialize_properties(self): # control commands - # self.add_device_property({ - # 'name': '', - # 'write_string': '', - # 'query_string': '', - # 'range': [], - # 'return_type': float}) - self.add_device_property( { "name": "field_set_point", "write_string": "$J{}", "query_string": "R8", - "range": [-self._field_limit, self._field_limit], + "range": [-self.field_limit, self.field_limit], "return_type": ips120_float, } ) @@ -193,7 +108,7 @@ def initialize_properties(self): "name": "field_rate", "write_string": "$T{}", "query_string": "R9", - "range": [-self._field_rate_limit, self._field_rate_limit], + "range": [-self.field_rate_limit, self.field_rate_limit], "return_type": ips120_float, } ) @@ -203,7 +118,8 @@ def initialize_properties(self): "name": "current_set_point", "write_string": "$I{}", "query_string": "R5", - "range": [-self._field_limit, self._field_limit], + "range": [-self.field_limit/self.field_to_current, + self.field_limit/self.field_to_current], "return_type": ips120_float, } ) @@ -213,23 +129,141 @@ def initialize_properties(self): "name": "current_rate", "write_string": "$S{}", "query_string": "R6", - "range": [-self._field_rate_limit, self._field_rate_limit], + "range": [-self.field_rate_limit/self.field_to_current, + self.field_rate_limit/self.field_to_current], "return_type": ips120_float, } ) - # self.add_device_property({ - # 'name': 'extended', - # 'write_string': 'Q{}', - # 'query_string': '', - # 'dict_values': {'normal':0, 'extended':4}, - # 'return_type': int}) + self.add_device_property( + { + "name": "output_current", + "query_string": "R0", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "voltage", + "query_string": "R1", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "measured_current", + "query_string": "R2", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "output_field", + "query_string": "R7", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "software_voltage_limit", + "query_string": "R15", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "persistent_current", + "query_string": "R16", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "trip_field", # mA + "query_string": "R17", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "persistent_current", + "query_string": "R18", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "switch_heater_current", + "query_string": "R20", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "safe_current_limit_negative", + "query_string": "R21", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "safe_current_limit_positive", + "query_string": "R22", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "lead_resistance", + "query_string": "R23", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "magnet_inductance", + "query_string": "R24", + "return_type": ips120_float + } + ) + + self.add_device_property( + { + "name": "version", + "query_string": "V", + "return_type": str + } + ) - self.get_field self.field_set_point self.field_rate self.current_set_point self.current_rate + self.output_current + self.voltage + self.output_field + self.software_voltage_limit + self.persistent_current + self.trip_field # mA + self.persistent_current + self.switch_heater_current + self.safe_current_limit_negative + self.safe_current_limit_positive + self.lead_resistance + self.magnet_inductance + self.version def print_state(self): """ @@ -355,7 +389,7 @@ def field(self): ''' TODO add settings dictionary with range to make this consistent with pyscan property ''' - self._field = self.get_field() + self._field = self.output_field return self._field @field.setter @@ -368,78 +402,6 @@ def field(self, new_value): while self.sweeping_status(): sleep(0.1) - def field_set_point(self): - print("use field_set_point property") - - # @property - # def field_rate(self): - # self._field_rate = ips120_float(self.query("R9")) - # return self._field_rate - # - # @field_rate.setter - # def field_rate(self, new_value): - # if (new_value > 0) and (new_value <= self._field_rate_limit): - # # normal resolution 0.001 T/min - # self.write("$T{}".format(round(new_value, 3))) - # self._field_rate = round(new_value, 3) - # # extended resolution 0.0001 T/min (see Q4 to set extended resolution) - # else: - # print( - # f"field_rate out of range, must be 0 < rate <= {self._field_rate_limit}" - # ) - - def get_current(self): - self._current = ips120_float(self.query("R0")) - return self._current - - def get_voltage(self): - self._voltage = ips120_float(self.query("R1")) - return self._voltage - - def get_measured_current(self): - self._measured_current = ips120_float(self.query("R2")) - return self._measured_current - - def get_current_set_point(self): - self._current_set_point = ips120_float(self.query("R5")) - return self._current_set_point - - def get_current_rate(self): - self._current_rate = ips120_float(self.query("R6")) - return self._current_rate - - def get_field(self): - self._field = ips120_float(self.query("R7")) - return self._field - - def get_field_set_point(self): - self._field_set_point = ips120_float(self.query("R8")) - return self._field_set_point - - def get_field_rate(self): - self._field_rate = ips120_float(self.query("R9")) - return self._field_rate - - def get_persistent_current(self): - self._persistent_current = ips120_float(self.query("R16")) - return self._persistent_current - - def get_persistent_field(self): - self._persistent_field = ips120_float(self.query("R18")) - return self._persistent_field - - def get_trip_field(self): - self._trip_field = ips120_float(self.query("R19")) - return self._trip_field - - def get_safe_current_limit_negative(self): - self._safe_current_limit_negative = ips120_float(self.query("R21")) - return self._safe_current_limit_negative - - def get_safe_current_limit_positive(self): - self._safe_current_limit_positive = ips120_float(self.query("R22")) - return self._safe_current_limit_positive - def status(self): status_string = self.query("X") return Status(status_string) @@ -500,15 +462,6 @@ def persistent_status(self): else: return False - # for i in range(n): - # - # if len(message) != 0: - # return message - # else: - # # &: ignore ISOBUS control characters, $: don't send a reply - # message = self.query('&') - # print("query again") - # --- legacy commands below here --- # legacy def set_local_locked(self): @@ -547,13 +500,13 @@ def target_field(self): @target_field.setter def target_field(self, new_value): - if (new_value >= -self._field_limit) and (new_value < self._field_limit): + if (new_value >= -self.field_limit) and (new_value < self.field_limit): self.write("$J{}".format(round(new_value, 4))) self._target_field = round(new_value, 4) else: print( "Target field out of range, must be 0 < set point < {}".format( - self._field_limit + self.field_limit ) ) @@ -575,13 +528,13 @@ def field_sweep_rate(self): @field_sweep_rate.setter def field_sweep_rate(self, new_value): - if (new_value >= 0) and (new_value < self._field_rate_limit): + if (new_value >= 0) and (new_value < self.field_rate_limit): self.write("$T{}".format(round(new_value, 3))) self._field_sweep_rate = round(new_value, 3) else: print( "Sweep rate out of range, must be 0 < rate < {}".format( - self._field_rate_limit + self.field_rate_limit ) ) @@ -780,3 +733,10 @@ def ips120_float(string): return float(string[1:]) +def ips120_int(string): + ''' + Helper function used with return_type to drop character and convert to a float. + ''' + + return int(string[1:]) + From 72de4dac1b96ac13fab6ce0e9c3ab29765b8a947 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Mon, 20 May 2024 16:41:13 -0600 Subject: [PATCH 21/36] add write-only parameters to ips120 driver --- pyscan/drivers/oxfordips120.py | 42 ++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 6d60aef8..44dad50c 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -139,6 +139,7 @@ def initialize_properties(self): { "name": "output_current", "query_string": "R0", + "readonly": float, "return_type": ips120_float } ) @@ -247,6 +248,43 @@ def initialize_properties(self): } ) + # write-only property + self.add_device_property( + { + "name": "control", + "write_string": "$C{}", + "dict_values": {"local_locked":0, "remote_locked":1, "local_unlocked": 2, "remote_unlocked":3}, + "return_type": int + } + ) + + self.add_device_property( + { + "name": "communications_protocol", + "write_string": "Q{}", # no echo since this command clears output buffer + "dict_values": {"normal":0, "extended":4}, # do not allow switch to + "return_type": int + } + ) + + self.add_device_property( + { + "name": "heater_control", + "write_string": "$H{}", + "indexed_values": ["off","on", "force"], + "return_type": int + } + ) + + self.add_device_property( + { + "name": "activity_control", + "write_string": "$A{}", + "dict_values": {"hold": 0, "to_set_point": 1, "to_zero": 2, "clamp": 4}, + "return_type": int + } + ) + self.field_set_point self.field_rate self.current_set_point @@ -286,11 +324,11 @@ def print_state(self): value += "\n" value += "Magnet is persistent" value += "\n" - value += f"get_persistent_field() = {self.get_persistent_field()}" + value += f"persistent_field = {self.get_persistent_field}" value += "\n" value += f"activity: {status.A_value()}" value += "\n" - value += f"get_field() = {self.get_field()}" + value += f"output_field = {self.output_field}" value += "\n" value += f"field_set_point = {self.field_set_point}" value += "\n" From 40e5d4c449282666af1efe314014901552fee116 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Mon, 20 May 2024 16:41:41 -0600 Subject: [PATCH 22/36] read-only and write-only properties for instrument drivers --- pyscan/drivers/instrument_driver.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyscan/drivers/instrument_driver.py b/pyscan/drivers/instrument_driver.py index 761a4212..45474a89 100644 --- a/pyscan/drivers/instrument_driver.py +++ b/pyscan/drivers/instrument_driver.py @@ -94,46 +94,50 @@ def add_device_property(self, settings): # get/set required parameters required_setting = ['values', 'range', 'indexed_values', 'dict_values'] - # readonly properties do not need to have a required key in setting + # readonly properties do not need to have a required key in setting, + # but for the docstring to work a readonly property is created. if 'write_string' in settings: readonly = False else: readonly = True + if 'readonly' not in settings: + settings['readonly'] = settings['return_type'].__name__ prop_type = '' if 'values' in settings: set_function = self.set_values_property prop_type = 'values' - doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, - prop_type, settings[prop_type]) elif ('range' in settings) and ('ranges' not in settings): set_function = self.set_range_property prop_type = 'range' - doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, - prop_type, settings[prop_type]) elif 'ranges' in settings: assert False, "ranges no longer accepted, must use method to set multiple properties at the same time." elif 'indexed_values' in settings: set_function = self.set_indexed_values_property prop_type = 'indexed_values' - doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, - prop_type, settings[prop_type]) elif 'dict_values' in settings: set_function = self.set_dict_values_property prop_type = 'dict_values' - doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, - prop_type, settings[prop_type]) elif readonly: prop_type = 'readonly' - doc_string = "{} : {}".format(settings['name'], settings['return_type'].__name__) else: assert False, "key used but not allowed" + doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, + prop_type, settings[prop_type]) + + # read-only if 'write_string' not in settings: property_definition = property( fget=lambda obj: self.get_instrument_property(obj, settings), fset=None, doc=doc_string) + # write-only + elif 'query_string' not in settings: + property_definition = property( + fget=None, + fset=lambda obj, new_value: set_function(obj, new_value, settings), + doc=doc_string) else: property_definition = property( fget=lambda obj: self.get_instrument_property(obj, settings), From be5f0ff07c898d218beaf4f50c9750f535c0dca9 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Thu, 23 May 2024 16:26:39 -0600 Subject: [PATCH 23/36] discard changes to instrument_driver in favor of read_only properties in main --- pyscan/drivers/instrument_driver.py | 40 +++++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/pyscan/drivers/instrument_driver.py b/pyscan/drivers/instrument_driver.py index 45474a89..089f7c18 100644 --- a/pyscan/drivers/instrument_driver.py +++ b/pyscan/drivers/instrument_driver.py @@ -13,8 +13,12 @@ class InstrumentDriver(ItemAttribute): Parameters ---------- instrument : string or pyvisa :class:`Resource` - visa string or an instantiated instrument (return value from - :func:`.new_instrument`) + visa string or an instantiated instrument + + Methods + ------- + add_device_property(settings) + ''' def __init__(self, instrument, debug=False): @@ -82,7 +86,10 @@ def add_device_property(self, settings): Parameters ---------- settings : dict - dict containing settings for property. Must have the key "values", "range", or "indexed_values". + dict containing settings for property. Must have: + - the key "values", "range", or "indexed_values", or "dict_values" + - "write_string" and/or "query_string" to communication with instrument + - "return_type" is a function that converts the return string to a python type Returns ------- @@ -91,17 +98,15 @@ def add_device_property(self, settings): self['_{}_settings'.format(settings['name'])] = settings - # get/set required parameters - required_setting = ['values', 'range', 'indexed_values', 'dict_values'] + command_strings = ['write_string', 'query_string'] + if not any(string in settings for string in command_strings): + assert False, "'write_string' and/or 'query_string' must be in settings" - # readonly properties do not need to have a required key in setting, - # but for the docstring to work a readonly property is created. - if 'write_string' in settings: - readonly = False - else: - readonly = True - if 'readonly' not in settings: - settings['readonly'] = settings['return_type'].__name__ + # read_only properties do not need to have a required key in setting, + # but for the docstring to work a read_only property is created. + if 'write_string' not in settings: + if 'read_only' not in settings: + settings['read_only'] = settings['return_type'].__name__ prop_type = '' if 'values' in settings: @@ -118,10 +123,10 @@ def add_device_property(self, settings): elif 'dict_values' in settings: set_function = self.set_dict_values_property prop_type = 'dict_values' - elif readonly: - prop_type = 'readonly' + elif 'read_only' in settings: + prop_type = 'read_only' else: - assert False, "key used but not allowed" + assert False, "Key 'values', 'range', indexed_values', 'read_only', or 'dict_values' must be in settings." doc_string = "{} : {}\n {}: {}".format(settings['name'], settings['return_type'].__name__, prop_type, settings[prop_type]) @@ -142,7 +147,8 @@ def add_device_property(self, settings): property_definition = property( fget=lambda obj: self.get_instrument_property(obj, settings), fset=lambda obj, new_value: set_function(obj, new_value, settings), - doc=doc_string) + doc=doc_string, + ) setattr(self.__class__, settings['name'], property_definition) From c65def818240932d2491d2b65dbe84bd7be2707b Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Thu, 23 May 2024 18:04:13 -0600 Subject: [PATCH 24/36] properties and methods are all in place for the magnet driver --- pyscan/drivers/oxfordips120.py | 341 +++++++++++++-------------------- 1 file changed, 128 insertions(+), 213 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 44dad50c..7f8a1fa7 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -27,8 +27,61 @@ class OxfordIPS120(InstrumentDriver): Read-only properties: + output_current: float + current in magnet power supply + voltage: float + voltage across leads + output_field: float + field from current in magnet power supply (not actual field if persistent) + software_voltage_limit: float + max voltage + persistent_current: float + current in magnet where heater was turned off + trip_field: float + field where last magnet quench occurred + persistent_current: float + field in magnet where heater was turned off + switch_heater_current: float + current in switch heater + safe_current_limit_negative: float + max negative current + safe_current_limit_positive: float + max positive current + lead_resistance: float + resistance + magnet_inductance: float + inductance + version: str + power supply model + status_string: str + + Write-only properties: + + remote_control + "local_locked", "remote_locked", "local_unlocked", "remote_unlocked" + communications_protocol + "normal", "extended" + heater_control + "off", "on", "force" + activity_control + "hold", "to_set_point", "to_zero", "clamp" + Methods ------- + heater() + turn heater on/off and deal correctly with persistent mode + print_state() + summarize state of the magnet + print_status() + not implemented yet + hold() + activity_control with checks + to_zero() + activity_control with checks + to_set_point() + activity_control with checks + clamp() + activity_control with checks """ def __init__(self, instrument, debug=False): @@ -36,7 +89,7 @@ def __init__(self, instrument, debug=False): self.instrument.read_termination = "\r" self.instrument.write_termination = "\r" - self.remote() + self.remote_control = "remote_unlocked" # make sure the buffer is empty: # read status byte by performing a serial poll @@ -194,7 +247,7 @@ def initialize_properties(self): self.add_device_property( { - "name": "persistent_current", + "name": "persistent_field", "query_string": "R18", "return_type": ips120_float } @@ -248,10 +301,18 @@ def initialize_properties(self): } ) + self.add_device_property( + { + "name": "status_string", + "query_string": "X", + "return_type": str + } + ) + # write-only property self.add_device_property( { - "name": "control", + "name": "remote_control", "write_string": "$C{}", "dict_values": {"local_locked":0, "remote_locked":1, "local_unlocked": 2, "remote_unlocked":3}, "return_type": int @@ -285,6 +346,7 @@ def initialize_properties(self): } ) + self.field self.field_set_point self.field_rate self.current_set_point @@ -294,8 +356,8 @@ def initialize_properties(self): self.output_field self.software_voltage_limit self.persistent_current - self.trip_field # mA - self.persistent_current + self.trip_field + self.persistent_field self.switch_heater_current self.safe_current_limit_negative self.safe_current_limit_positive @@ -309,18 +371,18 @@ def print_state(self): """ status = self.status() value = "" - if self.quench_status(): + if self.quench_status: value += "QUENCHED" - if self.heater_status(): + if self.heater_status: value += "Heater on" else: value += "Heater off" value += "\n" - if self.sweeping_status(): + if self.sweeping_status: value += "Field is changing" else: value += "At rest" - if self.persistent_status(): + if self.persistent_status: value += "\n" value += "Magnet is persistent" value += "\n" @@ -336,6 +398,7 @@ def print_state(self): return value + # TODO: implement status message def print_status(self): """ Print current status of the IPS120 power supply @@ -343,84 +406,64 @@ def print_status(self): pass def hold(self): - if not self.remote_status(): + if not self.remote_status: raise IPS120Error( "Control commands require the power supply to be in remote mode" ) - - self.query("A0") + self.activity_control = "hold" def to_set_point(self): - if not self.remote_status(): + if not self.remote_status: raise IPS120Error( "Control commands require the power supply to be in remote mode" ) - - self.query("A1") + self.activity_control = "to_set_point" def to_zero(self): - if not self.remote_status(): + if not self.remote_status: raise IPS120Error( "Control commands require the power supply to be in remote mode" ) - - self.query("A2") - - def clamp(self): - if not self.remote_status(): - raise IPS120Error( - "Control commands require the power supply to be in remote mode" - ) - - self.query("A4") - - def local(self, locked=False): - """ - Set local mode. - Keyword arguments: - locked: boolean, defaults to unlocked - """ - - if locked: - self.write("$C0") - else: - self.write("$C2") - - def remote(self, locked=False): - """ - Set remote mode. - Keyword arguments: - locked: boolean, defaults to unlocked - """ - - if locked: - self.write("$C1") - else: - self.write("$C3") + self.activity_control = "to_zero" def heater(self, state): """ Set the state of the heater. Record the time when the heater was changed """ - if not self.remote_status(): + if not self.remote_status: raise IPS120Error( "Control commands require the power supply to be in remote mode" ) + # TODO: deal with persistent magnet + if self.persistent_status: + raise IPS120Error("take magnet out of persistent mode before changing heater") + if state == "on": - if not self.heater_status(): + if not self.heater_status: self.time_heater_toggle = datetime.now() - _ = self.query("H1") + self.heater_control = "on" + sleep(30) elif state == "off": - if self.heater_status(): + if self.heater_status: self.time_heater_toggle = datetime.now() - _ = self.query("H0") - elif state == "force": - # need to add some checks before allowing heater to be forced - _ = self.query("H2") + self.heater_control = "off" + sleep(30) else: - print("State must be 'on', 'off' or 'force'") + print("State must be 'on' or 'off'") + + def remote(self, locked=False): + if locked: + self.remote_control = "remote_locked" + else: + self.remote_control = "remote_unlocked" + + def local(self, locked=False): + if locked: + self.remote_control = "local_locked" + else: + self.remote_control = "local_unlocked" @property def field(self): @@ -435,15 +478,16 @@ def field(self, new_value): self.hold() self.field_set_point = new_value self.to_set_point() - # TODO check if field is sweeping or at set point (if not either, then wait until it is sweeping) + # TODO: check if field is sweeping or at set point (if not either, then wait until it is sweeping) sleep(0.1) - while self.sweeping_status(): + while self.sweeping_status: sleep(0.1) def status(self): status_string = self.query("X") return Status(status_string) + @property def quench_status(self): """ True when the magnet quenched @@ -451,22 +495,28 @@ def quench_status(self): status = self.status() if status.X1 == 1: - return True + self._quench_status = True + else: + self._quench_status = False + return self._quench_status + @property def heater_status(self): """ - heater() True for on and False for off + heater True for on and False for off """ status = self.status() if status.H == 0: - return False + self._heater_status = False elif status.H == 1: - return True + self._heater_status = True else: # magnet persistant or heater fault - may need to have a heater_status object in future - return False + self._heater_status = False + return self._heater_status + @property def sweeping_status(self): """ True when the field is changing, False when the field is at rest @@ -474,10 +524,12 @@ def sweeping_status(self): status = self.status() if status.M2 == 0: - return False + self._sweeping_status = False else: - return True + self._sweeping_status = True + return self._sweeping_status + @property def remote_status(self): """ True when magnet is in remote mode, False when it is in local mode. @@ -485,10 +537,13 @@ def remote_status(self): status = self.status() if (status.C == 1) or (status.C == 3): - return True + self._remote_status = True else: - return False + self._remote_status = False + + return self._remote_status + @property def persistent_status(self): """ True when the magnet is in persistent mode @@ -496,150 +551,10 @@ def persistent_status(self): status = self.status() if status.H == 2: - return True + self._persistent_status = True else: - return False - - # --- legacy commands below here --- - # legacy - def set_local_locked(self): - self.write("$C0") - - # legacy - def set_remote_lock(self): - self.write("$C1") - - # legacy - def set_local_unlocked(self): - self.write("$C2") - - # legacy - def set_remote_unlocked(self): - self.write("$C3") - - # legacy, use heater('on') and these are reversed - def set_heat_on(self): - return self.query("&H0") - - # legacy, use heater('off') and these are reversed - def set_heat_off(self): - return self.query("&H1") - - # legacy, must use field_set_point attribute - def set_target_field(self, new_value): - self.write("$J{}".format(round(new_value, 4))) - print("deprecated: use 'field_set_point = value'") - - # legacy, use field_set_point() - @property - def target_field(self): - self._target_field = ips120_float(self.query("R8")) - return self._target_field - - @target_field.setter - def target_field(self, new_value): - if (new_value >= -self.field_limit) and (new_value < self.field_limit): - self.write("$J{}".format(round(new_value, 4))) - self._target_field = round(new_value, 4) - else: - print( - "Target field out of range, must be 0 < set point < {}".format( - self.field_limit - ) - ) - - # legacy, use get_current_set_point - def get_target_current(self): - self._target_current = ips120_float(self.query("R5")) - return self._target_current - - # legacy, use get_current_rate - def get_current_sweep_rate(self): - self._current_sweep_rate = ips120_float(self.query("R6")) - return self._current_sweep_rate - - # legacy, use field_rate - @property - def field_sweep_rate(self): - self._field_sweep_rate = ips120_float(self.query("R9")) - return self._field_sweep_rate - - @field_sweep_rate.setter - def field_sweep_rate(self, new_value): - if (new_value >= 0) and (new_value < self.field_rate_limit): - self.write("$T{}".format(round(new_value, 3))) - self._field_sweep_rate = round(new_value, 3) - else: - print( - "Sweep rate out of range, must be 0 < rate < {}".format( - self.field_rate_limit - ) - ) - - # legacy, use status() - def get_status(self): - status = self.query("X") - - X1 = {0: "Normal", 1: "Quenched", 2: "Over Heated", 4: "Warming Up", 8: "Fault"} - status1 = X1[int(status[1])] - - # X2 = {0: 'Normal', - # 1: 'On Positive Voltage Limit', - # 2: 'On Negative Voltage Limit', - # 4: 'Outside Negative Current Limit', - # 8: 'Outside Positive Current Limit'} - - status2 = X1[int(status[2])] - - print("Status: {}, {}".format(status1, status2)) - - A = {0: "Hold", 1: "To Set Point", 2: "To Zero", 4: "Clamped"} - - activity = A[int(status[4])] - - print("Activity: {}".format(activity)) - - C = { - 0: "Local & Locked", - 1: "Remote & Locked", - 2: "Local & Unlocked", - 3: "Remote & Unlocked", - 4: "Auto-Run-Down", - 5: "Auto-Run-Down", - 6: "Auto-Run-Down", - 7: "Auto-Run-Down", - } - - loc = C[int(status[6])] - - print("Local/Remote Status: {}".format(loc)) - - H = { - 0: "Off, Magnet at Zero", - 1: "On", - 2: "Off, Maget at Field", - 5: "Heater Fault", - 8: "No Switch Fitted", - } - - heater = H[int(status[8])] - - print("Heater: {}".format(heater)) - - M1 = {0: "Amps, Fast", 1: "Tesla, Fast", 4: "Amps, Slow", 5: "Tesla, Slow"} - - M2 = { - 0: "at rest", - 1: "Sweeping", - 2: "Sweep Limit", - 3: "Sweeping & Sweep Limiting", - } - - mode1 = M1[int(status[10])] - mode2 = M2[int(status[11])] - - print("Mode: {}; {}".format(mode1, mode2)) - + self._persistent_status = False + return self._persistent_status class Status: def __init__(self, status_string="X00A1C3H1M10P03"): From efa6fe60795060cc76645a3a6568b3987e76aaaf Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Thu, 23 May 2024 18:05:11 -0600 Subject: [PATCH 25/36] test notebook for the Oxford IPS120 --- .../OxfordIPS120_test_notebook.ipynb | 1001 +++++++++++++++++ 1 file changed, 1001 insertions(+) create mode 100644 drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb diff --git a/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb b/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb new file mode 100644 index 00000000..c17079fe --- /dev/null +++ b/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb @@ -0,0 +1,1001 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "114e61cb-2ac0-4a53-bf0e-c7888e554e2d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Could not load Keysight SD1\n", + "Could not load Keysight SD1\n", + "pylablib not found, AttocubeANC350 not loaded\n", + "Basler Camera software not found, BaserCamera not loaded\n", + "Helios Camera not installed\n", + "msl not installed, Thorlabs BSC203 driver not loaded\n", + "seabreeze module not found, Ocean Optics not imported\n", + "Failed to load spinapi library.\n", + "spinapi is not installed, PulseBlaster driver not loaded.\n", + "Thorlabs Kinesis not found, ThorlabsBSC203 not loaded\n", + "Thorlabs Kinesis not found, ThorlabsBPC303 not loaded\n", + "Thorlabs Kinesis not found, ThorlabsMFF101 not loaded\n" + ] + } + ], + "source": [ + "import os\n", + "import sys\n", + "import time\n", + "import threading\n", + "from pathlib import Path\n", + "from time import sleep\n", + "import pyscan as ps\n", + "import ipywidgets as widgets\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "8d0c2aef-d8d9-4d6a-aaa4-22aeb849691f", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "instruments = ps.ItemAttribute()\n", + "instruments.ips120 = ps.new_instrument(gpib_address=25)\n", + "devices = ps.ItemAttribute()\n", + "devices.magnet = ps.OxfordIPS120(instruments.ips120, debug=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "a888bba8-a6aa-44cd-938c-28433e4a52a0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Heater on\n", + "At rest\n", + "activity: To Set Point\n", + "output_field = 0.0\n", + "field_set_point = 0.0\n", + "field_rate = 0.1\n" + ] + } + ], + "source": [ + "print(devices.magnet.print_state())" + ] + }, + { + "cell_type": "markdown", + "id": "8184656e-8650-4f65-9b63-4d9b2f703165", + "metadata": {}, + "source": [ + "## direct read and write commands" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3a00c3ac-1ad2-49d2-a7cd-a12812a5cf58", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "X00A1C3H1M10P03\n" + ] + } + ], + "source": [ + "result = devices.magnet.query('X')\n", + "print(result)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "338da351-ca41-4488-94e6-2a229749ef6b", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "# extra read if necessary\n", + "result = instruments.ips120.read()\n", + "print(result)" + ] + }, + { + "cell_type": "markdown", + "id": "d478caef-dd58-4011-98a5-c98e0689658c", + "metadata": {}, + "source": [ + "## Testing magnet driver" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ef5d656f-7e6c-4aec-95cc-8929cf983ac4", + "metadata": {}, + "outputs": [], + "source": [ + "# don't sweep anywhere while testing\n", + "devices.magnet.hold()" + ] + }, + { + "cell_type": "markdown", + "id": "c03e3fee-fada-4b0d-8918-cc0fc7311eac", + "metadata": {}, + "source": [ + "### properties" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "c7a6bf3c-c19c-45e5-8dbd-baf701b954bf", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.0\n" + ] + } + ], + "source": [ + "print(devices.magnet.field_set_point)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "dccb8369-e95c-4c9c-8dd7-8bfa5e76b923", + "metadata": {}, + "outputs": [], + "source": [ + "initial_value = devices.magnet.field_set_point\n", + "new_value = 0.111\n", + "devices.magnet.field_set_point = new_value\n", + "result = devices.magnet.field_set_point\n", + "assert result==new_value\n", + "devices.magnet.field_set_point = initial_value\n", + "result = devices.magnet.field_set_point\n", + "assert result==initial_value" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f3940807", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.1\n" + ] + } + ], + "source": [ + "print(devices.magnet.field_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "e5596e5f", + "metadata": {}, + "outputs": [], + "source": [ + "initial_value = devices.magnet.field_rate\n", + "new_value = 0.111\n", + "devices.magnet.field_rate = new_value\n", + "result = devices.magnet.field_rate\n", + "assert result==new_value\n", + "devices.magnet.field_rate = initial_value\n", + "result = devices.magnet.field_rate\n", + "assert result==initial_value" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "158473d0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.0\n" + ] + } + ], + "source": [ + "print(devices.magnet.current_set_point)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "93df54cf", + "metadata": {}, + "outputs": [], + "source": [ + "initial_value = devices.magnet.current_set_point\n", + "new_value = 0.111\n", + "devices.magnet.current_set_point = new_value\n", + "result = devices.magnet.current_set_point\n", + "assert result==new_value\n", + "devices.magnet.current_set_point = initial_value\n", + "result = devices.magnet.current_set_point\n", + "assert result==initial_value" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "622aaf10", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.449\n" + ] + } + ], + "source": [ + "print(devices.magnet.current_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "ece5e1d5", + "metadata": {}, + "outputs": [], + "source": [ + "initial_value = devices.magnet.current_rate\n", + "new_value = 0.22 # current can only be set to two digits in non-extended mode\n", + "devices.magnet.current_rate = new_value\n", + "result = devices.magnet.current_rate\n", + "assert result==new_value\n", + "devices.magnet.current_rate = initial_value\n", + "result = devices.magnet.current_rate\n", + "assert result==initial_value" + ] + }, + { + "cell_type": "markdown", + "id": "c45141a5-c080-4df5-83d8-d19d82bbfe29", + "metadata": {}, + "source": [ + "### read-only properties" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "660c4640-ed68-44c1-af4a-5551a347e0e0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.0\n" + ] + } + ], + "source": [ + "print(devices.magnet.output_current)" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "45844641", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-0.02\n" + ] + } + ], + "source": [ + "print(devices.magnet.voltage)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "7178f259", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.09\n" + ] + } + ], + "source": [ + "print(devices.magnet.measured_current)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "4c4f458d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.0\n" + ] + } + ], + "source": [ + "print(devices.magnet.output_field)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "0492a897", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2.5\n" + ] + } + ], + "source": [ + "print(devices.magnet.software_voltage_limit)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "313cd9bc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.0\n" + ] + } + ], + "source": [ + "print(devices.magnet.persistent_current)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "46a98f50", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-32.8032\n" + ] + } + ], + "source": [ + "print(devices.magnet.trip_field)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "9cbfcec5-d623-4bc7-ab2e-7e9729bd7eef", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.0\n" + ] + } + ], + "source": [ + "print(devices.magnet.persistent_field)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "9e498bd2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-40.42\n" + ] + } + ], + "source": [ + "print(devices.magnet.safe_current_limit_negative)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "9270c945", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.42\n" + ] + } + ], + "source": [ + "print(devices.magnet.safe_current_limit_positive)" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "67b5a56b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "15.7\n" + ] + } + ], + "source": [ + "print(devices.magnet.lead_resistance)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "6f67ea45", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "51.7\n" + ] + } + ], + "source": [ + "print(devices.magnet.magnet_inductance)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "51951497", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "IPS120-10 Version 3.07 (c) OXFORD 1996\n" + ] + } + ], + "source": [ + "print(devices.magnet.version)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "cffbc249-92f7-4b6e-9b34-a190d058ed53", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "X00A0C3H1M10P03\n" + ] + } + ], + "source": [ + "print(devices.magnet.status_string)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1d463b2e-5c90-4cac-b8d7-a1b6e2a5374c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "quench_status = False\n" + ] + } + ], + "source": [ + "print(f\"quench_status = {devices.magnet.quench_status}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "id": "37244aed", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "heater_status = True\n" + ] + } + ], + "source": [ + "print(f\"heater_status = {devices.magnet.heater_status}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "a267eb62", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "sweeping_status = False\n" + ] + } + ], + "source": [ + "print(f\"sweeping_status = {devices.magnet.sweeping_status}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "d0f29612", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "persistent_status = False\n" + ] + } + ], + "source": [ + "print(f\"persistent_status = {devices.magnet.persistent_status}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "c328fa05", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "remote_status = True\n" + ] + } + ], + "source": [ + "print(f\"remote_status = {devices.magnet.remote_status}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ad484b76-01c0-40e3-bace-fee46414dfa3", + "metadata": {}, + "source": [ + "### write-only properties" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "f3ca2872-7e6a-4770-a276-02f74a232ff9", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.remote_control = \"local_locked\"" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "2e522e60", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.remote_control = \"remote_locked\"" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "7720458b", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.remote_control = \"local_unlocked\"" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "fda8a8dc", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.remote_control = \"remote_unlocked\"" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "id": "febaad6e-09a1-4435-9ddb-042d15568960", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.communications_protocol = \"normal\"" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "id": "09698c16-7a8b-44fb-8380-b2a7d414bc03", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.22\n" + ] + } + ], + "source": [ + "devices.magnet.current_rate = 0.222\n", + "print(devices.magnet.current_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "aa75d1b5", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.communications_protocol = \"extended\"" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "2caa82ab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0.222\n" + ] + } + ], + "source": [ + "devices.magnet.current_rate = 0.222\n", + "print(devices.magnet.current_rate)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "id": "7235f709-727f-4f25-b089-4396f7c4d85e", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.heater_control = 'off'" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "id": "1312fb4f", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.heater_control = 'on'" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "8b4a0755", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.heater_control = 'force'" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "id": "eb302f8d-f3f9-443c-bba8-0cbd15918f10", + "metadata": {}, + "outputs": [], + "source": [ + "# only run if magnet is at zero\n", + "# devices.magnet.activity_control = \"clamp\"" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "id": "e0aefdce", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.activity_control = \"hold\"" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "id": "17af6062", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.activity_control = \"to_zero\"" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "5f06f175", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.activity_control = \"to_set_point\"" + ] + }, + { + "cell_type": "markdown", + "id": "eaebdb30-c0f3-417a-942b-8da07126992e", + "metadata": {}, + "source": [ + "### methods" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "9c303dc9-a9d9-43bf-bbd8-9ec87380c857", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.local() # keyword: locked=False" + ] + }, + { + "cell_type": "code", + "execution_count": 56, + "id": "a935a8bb-2f23-45bc-a598-11e61ffca0d2", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.remote() # keyword: locked=False" + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "id": "14712428-c581-478c-b572-b610c7bdbbe3", + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "devices.magnet.heater('off')" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "372d5931-36ca-4c8a-8a15-c7d0baa01164", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.heater('on')" + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "id": "851993a8-6326-479c-b479-c03eef087c7e", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.to_set_point()" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "e3c46264-dcef-44e9-a36f-44e61a9abe8c", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.hold()" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "f5d09c36-fbf5-4b5b-a88f-d44c13e27cd3", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.to_zero()" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "8d9547e5-422c-4d3b-bf9c-cb2478305bda", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "system status (operation) : Normal (X1=0)\n", + "system status (voltage) : Normal (X2=0)\n", + "Activity ; To Zero (A=2)\n", + "LOC/REM status ; Remote & Unlocked (C=3)\n", + "Heater ; On (H=1)\n", + "Mode (rate) ; Tesla, Immediate, Fast (M1=1)\n", + "Mode (sweep) ; At Rest (M2=0)\n" + ] + } + ], + "source": [ + "status = devices.magnet.status()\n", + "print(status)" + ] + }, + { + "cell_type": "markdown", + "id": "69010774-8d57-47cf-84f1-2fc3ce10bba0", + "metadata": {}, + "source": [ + "### magnet field property - only test with magnet cold" + ] + }, + { + "cell_type": "code", + "execution_count": 63, + "id": "3c81d80e-aa14-4b31-89ee-dea52a10b00e", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.hold()\n", + "devices.magnet.remote()\n", + "devices.magnet.heater('on')\n", + "original_rate = devices.magnet.field_rate\n", + "devices.magnet.field_rate = 0.1\n", + "value = devices.magnet.output_field\n", + "db = 0.01\n", + "new_value = value + db\n", + "if new_value > devices.magnet.field_limit:\n", + " new_value = value - db\n", + "devices.magnet.field = new_value\n", + "assert devices.magnet.output_field == new_value" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "f5e5be95-8cca-4161-a084-1a30bebf3e4f", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.field = value\n", + "assert devices.magnet.output_field == value" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "id": "7bb77b13-3f67-4fb1-98db-4c75675dfa80", + "metadata": {}, + "outputs": [], + "source": [ + "devices.field_rate = original_rate" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "id": "b0624387-61c4-455c-b717-6a8c08b6796b", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.heater(\"off\")" + ] + } + ], + "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.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 1ae2f51d48aa1da5a636bb714a5a8f4e06bb56de Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Thu, 23 May 2024 18:12:13 -0600 Subject: [PATCH 26/36] remove outdated notebook --- test/drivers/using_OxfordIPS120_driver.ipynb | 672 ------------------- 1 file changed, 672 deletions(-) delete mode 100644 test/drivers/using_OxfordIPS120_driver.ipynb diff --git a/test/drivers/using_OxfordIPS120_driver.ipynb b/test/drivers/using_OxfordIPS120_driver.ipynb deleted file mode 100644 index f6247163..00000000 --- a/test/drivers/using_OxfordIPS120_driver.ipynb +++ /dev/null @@ -1,672 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "114e61cb-2ac0-4a53-bf0e-c7888e554e2d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Could not load Keysight SD1\n", - "Could not load Keysight SD1\n", - "pylablib not found, AttocubeANC350 not loaded\n", - "Basler Camera software not found, BaserCamera not loaded\n", - "Helios Camera not installed\n", - "msl not installed, Thorlabs BSC203 driver not loaded\n", - "seabreeze module not found, Ocean Optics not imported\n", - "Failed to load spinapi library.\n", - "spinapi is not installed, PulseBlaster driver not loaded.\n", - "Thorlabs Kinesis not found, ThorlabsBSC203 not loaded\n", - "Thorlabs Kinesis not found, ThorlabsBPC303 not loaded\n", - "Thorlabs Kinesis not found, ThorlabsMFF101 not loaded\n" - ] - } - ], - "source": [ - "import os\n", - "import sys\n", - "import time\n", - "import threading\n", - "from pathlib import Path\n", - "from time import sleep\n", - "import pyscan as ps\n", - "import ipywidgets as widgets\n", - "from IPython.display import display" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "8d0c2aef-d8d9-4d6a-aaa4-22aeb849691f", - "metadata": {}, - "outputs": [], - "source": [ - "instruments = ps.ItemAttribute()\n", - "instruments.ips120 = ps.new_instrument(gpib_address=25)\n", - "devices = ps.ItemAttribute()\n", - "devices.magnet = ps.OxfordIPS120(instruments.ips120)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a888bba8-a6aa-44cd-938c-28433e4a52a0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Heater off\n", - "At rest\n", - "activity: Clamped\n", - "get_field() = 0.0\n", - "field_set_point = 0.05\n", - "field_rate = 0.1" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "devices.magnet" - ] - }, - { - "cell_type": "markdown", - "id": "06ae8d88-9194-432f-9aa9-192556f1bea4", - "metadata": {}, - "source": [ - "## Issues with original magnet driver in pyscan" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "cf6b33a2-ccd5-4898-8410-9cd94efaba10", - "metadata": {}, - "outputs": [], - "source": [ - "# fix clamp()\n", - "# need to ensure that the field is at zero before clamping" - ] - }, - { - "cell_type": "markdown", - "id": "d478caef-dd58-4011-98a5-c98e0689658c", - "metadata": {}, - "source": [ - "## Fix magnet operation by adding a new interface (keeping legacy commands for compatibility)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a949ee96-2c31-45a4-ad21-618d460c7371", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Heater off\n", - "At rest\n", - "activity: Clamped\n", - "get_field() = 0.0\n", - "field_set_point = 0.05\n", - "field_rate = 0.1" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "devices.magnet" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "9c303dc9-a9d9-43bf-bbd8-9ec87380c857", - "metadata": {}, - "outputs": [], - "source": [ - "devices.magnet.local() # keyword: locked=False" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "a935a8bb-2f23-45bc-a598-11e61ffca0d2", - "metadata": {}, - "outputs": [], - "source": [ - "devices.magnet.remote() # keyword: locked=False" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "14712428-c581-478c-b572-b610c7bdbbe3", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "devices.magnet.heater('off')" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "id": "372d5931-36ca-4c8a-8a15-c7d0baa01164", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "system status (operation) : Normal (X1=0)\n", - "system status (voltage) : Normal (X2=0)\n", - "Activity ; Clamped (A=4)\n", - "LOC/REM status ; Remote & Unlocked (C=3)\n", - "Heater ; Heater Fault (H=5)\n", - "Mode (rate) ; Amps, Immediate, Fast (M1=0)\n", - "Mode (sweep) ; At Rest (M2=0)\n" - ] - } - ], - "source": [ - "devices.magnet.heater('on')\n", - "status = devices.magnet.status()\n", - "print(status)" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "id": "24a036da-2369-4766-87b7-a40b5e553a27", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'X00A4C3H5M00P04'" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "devices.magnet.instrument.query('X')" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "fb8b8eab-84b4-4483-b40b-32ca5810ca57", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "system status (operation) : Normal (X1=0)\n", - "system status (voltage) : Normal (X2=0)\n", - "Activity ; Clamped (A=4)\n", - "LOC/REM status ; Remote & Unlocked (C=3)\n", - "Heater ; Off Magnet at Zero (H=0)\n", - "Mode (rate) ; Amps, Immediate, Fast (M1=0)\n", - "Mode (sweep) ; At Rest (M2=0)\n" - ] - } - ], - "source": [ - "status = devices.magnet.status()\n", - "if status.H == 5:\n", - " print(\"switch heater fault\")\n", - " _ = devices.magnet.instrument.query(\"H0\")\n", - " sleep(0.5)\n", - " status = devices.magnet.status()\n", - "print(status)" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "id": "52724b6c-714b-4375-9a10-512e8e038a8d", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "'X00A4C3H0M00P04'" - ] - }, - "execution_count": 37, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "devices.magnet.instrument.read()" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "6634fdf8-d0d8-4b95-9b11-cf26bf3e05ea", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.0" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "devices.magnet.get_field()" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "28cda43b-8a02-4050-9318-cd7cd7c71c03", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "0.0" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "devices.magnet.get_persistent_field() # remembers the persistent field when the heater was last turned off" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "5e0bd57a-56e7-432f-9b27-2c63c0dccdf3", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.05\n", - "0.1\n" - ] - } - ], - "source": [ - "print(devices.magnet.field_set_point)\n", - "print(devices.magnet.field_rate)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "7f5e22a9-cb14-4c48-be5a-f9e3ee0d9ee1", - "metadata": {}, - "outputs": [], - "source": [ - "devices.magnet.to_set_point()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "e3c46264-dcef-44e9-a36f-44e61a9abe8c", - "metadata": {}, - "outputs": [], - "source": [ - "devices.magnet.hold()" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "f5d09c36-fbf5-4b5b-a88f-d44c13e27cd3", - "metadata": {}, - "outputs": [], - "source": [ - "devices.magnet.to_zero()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "8d9547e5-422c-4d3b-bf9c-cb2478305bda", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "system status (operation) : Normal (X1=0)\n", - "system status (voltage) : Normal (X2=0)\n", - "Activity ; Clamped (A=4)\n", - "LOC/REM status ; Remote & Unlocked (C=3)\n", - "Heater ; Heater Fault (H=5)\n", - "Mode (rate) ; Amps, Immediate, Fast (M1=0)\n", - "Mode (sweep) ; At Rest (M2=0)\n" - ] - } - ], - "source": [ - "status = devices.magnet.status()\n", - "print(status)" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "354e5ced-6408-465c-9332-d65bd2a46662", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "remote: True\n", - "heater: True\n", - "sweeping: False\n", - "persistent: False\n" - ] - } - ], - "source": [ - "print(f\"remote: {devices.magnet.remote_status()}\")\n", - "print(f\"heater: {devices.magnet.heater_status()}\")\n", - "print(f\"sweeping: {devices.magnet.sweeping_status()}\")\n", - "print(f\"persistent: {devices.magnet.persistent_status()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "ccc48384-25e0-42b3-b183-95db4781ee32", - "metadata": {}, - "source": [ - "## GUI for Oxford IPS120" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "ef553b6a-10f9-4721-baec-504ab9e44300", - "metadata": {}, - "outputs": [], - "source": [ - "heater_button = widgets.ToggleButton(description='heater')\n", - "field_float = widgets.FloatText(description='field')\n", - "activity_buttons = widgets.ToggleButtons(description='activity', options=['hold', 'to set point', 'to zero'])\n", - "field_set_point_float = widgets.FloatText(description='set point')\n", - "field_rate_float = widgets.FloatText(description='rate')\n", - "persistent_button = widgets.ToggleButton(description='persistent', value=False, disabled=True)\n", - "persistent_field_float = widgets.FloatText(description='persistent field', value=0.0, disabled=True)\n", - "layout = widgets.VBox([\n", - " widgets.HBox([field_set_point_float, field_rate_float]), \n", - " activity_buttons,\n", - " heater_button, \n", - " field_float,\n", - " widgets.HBox([persistent_button, persistent_field_float]),\n", - " ])\n", - "\n", - "# relying on a global variable, need to address this\n", - "widget_dict = {}\n", - "widget_dict['heater_button'] = heater_button\n", - "widget_dict['activity_buttons'] = activity_buttons\n", - "widget_dict['field_float'] = field_float\n", - "widget_dict['field_rate_float'] = field_rate_float\n", - "widget_dict['field_set_point_float'] = field_set_point_float\n", - "widget_dict['persistent_button'] = persistent_button\n", - "widget_dict['persistent_field_float'] = persistent_field_float\n", - "start = time.time()\n", - "def update(widget_dict, magnet, event):\n", - " '''\n", - " Update the magnet on a schedule\n", - " '''\n", - " slow_update = 2\n", - " fast_update = 0.5\n", - " update_time = 0.1\n", - " t = threading.current_thread()\n", - " slow_time = time.time()-slow_update # do a slow update right away\n", - " fast_time = time.time()-fast_update # do a fast update right away\n", - " while getattr(t, \"do_run\", True):\n", - " current_time = time.time()\n", - " if (current_time - slow_time) > slow_update:\n", - " slow_time = time.time()\n", - " # slow updates\n", - " event.wait()\n", - " status = magnet.status()\n", - " if status.H == 0:\n", - " widget_dict['heater_button'].value = False\n", - " # enter bad reverse persistent mode\n", - " if (widget_dict['field_float'].value != 0) and not widget_dict['heater_button'].disabled:\n", - " # must return to 0 before the heater can be turned on\n", - " widget_dict['heater_button'].disabled = True\n", - " widget_dict['persistent_button'].value = True\n", - " widget_dict['persistent_button'].button_style = 'danger'\n", - " widget_dict['persistent_button'].disabled = False\n", - " widget_dict['persistent_field_float'].value = 0\n", - " widget_dict['persistent_field_float'].disabled = False\n", - " # exit bad reverse persisent mode\n", - " if widget_dict['field_float'].value == 0 and widget_dict['heater_button'].disabled:\n", - " widget_dict['heater_button'].disabled = False\n", - " widget_dict['persistent_button'].value = False\n", - " widget_dict['persistent_button'].button_style = ''\n", - " widget_dict['persistent_button'].disabled = True\n", - " widget_dict['persistent_field_float'].value = 0\n", - " widget_dict['persistent_field_float'].disabled = True\n", - " if status.H == 1:\n", - " widget_dict['heater_button'].value = True\n", - " # exit persistent\n", - " if not widget_dict['persistent_button'].disabled:\n", - " widget_dict['persistent_button'].value = False\n", - " widget_dict['persistent_button'].disabled = True\n", - " widget_dict['persistent_button'].button_style = ''\n", - " widget_dict['persistent_field_float'].value = 0\n", - " widget_dict['persistent_field_float'].disabled = True\n", - " event.wait()\n", - " magnet.hold()\n", - " widget_dict['field_set_point_float'].disabled = False\n", - " # maybe GUI starts in persistent mode\n", - " try:\n", - " widget_dict['field_set_point_float'].value = nonpersistent_set_point\n", - " except NameError:\n", - " widget_dict['field_set_point_float'].value = magnet.get_field()\n", - " \n", - " if status.H == 2:\n", - " # only allow the heater to turn on when the field and the persistent field are the same\n", - " event.wait()\n", - " if (widget_dict['field_float'].value != magnet.get_persistent_field()) and not widget_dict['heater_button'].disabled:\n", - " widget_dict['heater_button'].disabled = True\n", - " event.wait()\n", - " if (widget_dict['field_float'].value == magnet.get_persistent_field()) and widget_dict['heater_button'].disabled:\n", - " widget_dict['heater_button'].disabled = False\n", - " # enter_persistent\n", - " if not widget_dict['field_set_point_float'].disabled:\n", - " widget_dict['persistent_button'].value = True\n", - " widget_dict['persistent_button'].disabled = False\n", - " widget_dict['persistent_button'].button_style = 'warning'\n", - " event.wait()\n", - " widget_dict['persistent_field_float'].value = magnet.get_persistent_field()\n", - " widget_dict['persistent_field_float'].disabled = False\n", - " event.wait()\n", - " nonpersistent_set_point = magnet.field_set_point\n", - " event.wait()\n", - " magnet.field_set_point = magnet.get_persistent_field()\n", - " widget_dict['field_set_point_float'].disabled = True\n", - " \n", - " if status.A == 0:\n", - " # hold\n", - " widget_dict['activity_buttons'].value = \"hold\"\n", - " elif status.A == 1:\n", - " widget_dict['activity_buttons'].value = \"to set point\"\n", - " elif status.A == 2:\n", - " widget_dict['activity_buttons'].value = \"to zero\"\n", - " event.wait()\n", - " widget_dict['field_set_point_float'].value = magnet.field_set_point\n", - " event.wait()\n", - " widget_dict['field_rate_float'].value = magnet.field_rate\n", - " if (current_time-fast_time) > fast_update:\n", - " fast_time = time.time()\n", - " event.wait()\n", - " widget_dict['field_float'].value = magnet.get_field()\n", - " sleep(update_time)\n", - " print(\"stopping update\")\n", - " \n", - "# @observe(\"heater_button\")\n", - "def heater_button_clicked(change):\n", - " # requires global devices.magnet\n", - " event.clear()\n", - " if change['new']:\n", - " devices.magnet.heater('on')\n", - " else:\n", - " devices.magnet.heater('off')\n", - " event.set()\n", - " \n", - "def activity_buttons_clicked(change):\n", - " event.clear()\n", - " match change['new']:\n", - " case \"to set point\":\n", - " devices.magnet.to_set_point()\n", - " case \"to zero\":\n", - " devices.magnet.to_zero()\n", - " case \"hold\":\n", - " devices.magnet.hold()\n", - " event.set()\n", - "\n", - "def field_set_point_float_changed(change):\n", - " event.clear() # pause the thread\n", - " devices.magnet.field_set_point = change['new']\n", - " event.set() # restart the thread\n", - "\n", - "def field_rate_float_changed(change):\n", - " event.clear() # pause the thread\n", - " devices.magnet.field_rate = change['new']\n", - " event.set() # restart the thread\n", - " \n", - "#heater_button.on_trait_change(heater_button_clicked)\n", - "heater_button.observe(heater_button_clicked, names=\"value\")\n", - "activity_buttons.observe(activity_buttons_clicked, names=\"value\")\n", - "field_set_point_float.observe(field_set_point_float_changed, names=\"value\")\n", - "field_rate_float.observe(field_rate_float_changed, names=\"value\")" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "09af2347-ab72-4f01-a4b1-7f2e3ae01b49", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "13b63bb93a1f4c1086b7aa09985b74ce", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(HBox(children=(FloatText(value=0.0, description='set point'), FloatText(value=0.0, description=…" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "event = threading.Event()\n", - "event.set()\n", - "thread = threading.Thread(target=update, args=(widget_dict, devices.magnet, event))\n", - "display(layout)\n", - "thread.start()" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "6fea831d-2f8e-4c65-8ec6-e9cf4e643bbf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "thread.is_alive()" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "b38ef6b0-67b3-4f3b-b507-5fe0d8bb6234", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "stopping update\n" - ] - } - ], - "source": [ - "thread.do_run = False" - ] - } - ], - "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.11.7" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} From 57cd6034e524e3e60319a67f7e2ef5b9aad66d40 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Thu, 23 May 2024 18:18:27 -0600 Subject: [PATCH 27/36] fix flake8 problems --- pyscan/drivers/oxfordips120.py | 45 +++++++++++++++++----------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 7f8a1fa7..44f1e309 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -104,9 +104,9 @@ def __init__(self, instrument, debug=False): print(f"Message in buffer: {message}") # magnet specific settings - self.field_limit = 8 # oxford triton in 518/1112 + self.field_limit = 8 # oxford triton in 518/1112 self.field_rate_limit = 0.2 - self.field_to_current = 5/22.456 # Tesla/Amp + self.field_to_current = 5/22.456 # Tesla/Amp self.debug = debug self.initialize_properties() @@ -131,7 +131,7 @@ def query(self, string, timeout=1): i += 1 if self.debug: # typically i=4, time = 0.01 - print(f"needed to wait {i}; {time()-start_time} seconds") + print(f"needed to wait {i}; {time() - start_time} seconds") message = self.read() return message @@ -171,8 +171,8 @@ def initialize_properties(self): "name": "current_set_point", "write_string": "$I{}", "query_string": "R5", - "range": [-self.field_limit/self.field_to_current, - self.field_limit/self.field_to_current], + "range": [-self.field_limit / self.field_to_current, + self.field_limit / self.field_to_current], "return_type": ips120_float, } ) @@ -182,8 +182,8 @@ def initialize_properties(self): "name": "current_rate", "write_string": "$S{}", "query_string": "R6", - "range": [-self.field_rate_limit/self.field_to_current, - self.field_rate_limit/self.field_to_current], + "range": [-self.field_rate_limit / self.field_to_current, + self.field_rate_limit / self.field_to_current], "return_type": ips120_float, } ) @@ -239,7 +239,7 @@ def initialize_properties(self): self.add_device_property( { - "name": "trip_field", # mA + "name": "trip_field", # mA "query_string": "R17", "return_type": ips120_float } @@ -314,7 +314,7 @@ def initialize_properties(self): { "name": "remote_control", "write_string": "$C{}", - "dict_values": {"local_locked":0, "remote_locked":1, "local_unlocked": 2, "remote_unlocked":3}, + "dict_values": {"local_locked": 0, "remote_locked": 1, "local_unlocked": 2, "remote_unlocked": 3}, "return_type": int } ) @@ -322,8 +322,8 @@ def initialize_properties(self): self.add_device_property( { "name": "communications_protocol", - "write_string": "Q{}", # no echo since this command clears output buffer - "dict_values": {"normal":0, "extended":4}, # do not allow switch to + "write_string": "Q{}", # no echo since this command clears output buffer + "dict_values": {"normal": 0, "extended": 4}, # do not allow switch to "return_type": int } ) @@ -332,7 +332,7 @@ def initialize_properties(self): { "name": "heater_control", "write_string": "$H{}", - "indexed_values": ["off","on", "force"], + "indexed_values": ["off", "on", "force"], "return_type": int } ) @@ -472,7 +472,7 @@ def field(self): ''' self._field = self.output_field return self._field - + @field.setter def field(self, new_value): self.hold() @@ -526,7 +526,7 @@ def sweeping_status(self): if status.M2 == 0: self._sweeping_status = False else: - self._sweeping_status = True + self._sweeping_status = True return self._sweeping_status @property @@ -556,6 +556,7 @@ def persistent_status(self): self._persistent_status = False return self._persistent_status + class Status: def __init__(self, status_string="X00A1C3H1M10P03"): """ @@ -585,19 +586,19 @@ def __init__(self, status_string="X00A1C3H1M10P03"): def __repr__(self): value = "\n" - value += f"{ self.X1_name } : {self.X1_value()} (X1={self.X1})" + value += f"{self.X1_name} : {self.X1_value()} (X1={self.X1})" value += "\n" - value += f"{ self.X2_name } : {self.X2_value()} (X2={self.X2})" + value += f"{self.X2_name} : {self.X2_value()} (X2={self.X2})" value += "\n" - value += f"{ self.A_name } ; {self.A_value()} (A={self.A})" + value += f"{self.A_name} ; {self.A_value()} (A={self.A})" value += "\n" - value += f"{ self.C_name } ; {self.C_value()} (C={self.C})" + value += f"{self.C_name} ; {self.C_value()} (C={self.C})" value += "\n" - value += f"{ self.H_name } ; {self.H_value()} (H={self.H})" + value += f"{self.H_name} ; {self.H_value()} (H={self.H})" value += "\n" - value += f"{ self.M1_name } ; {self.M1_value()} (M1={self.M1})" + value += f"{self.M1_name} ; {self.M1_value()} (M1={self.M1})" value += "\n" - value += f"{ self.M2_name } ; {self.M2_value()} (M2={self.M2})" + value += f"{self.M2_name} ; {self.M2_value()} (M2={self.M2})" return value @@ -686,10 +687,10 @@ def ips120_float(string): return float(string[1:]) + def ips120_int(string): ''' Helper function used with return_type to drop character and convert to a float. ''' return int(string[1:]) - From dba57f78d9bc375f5d01918f1981139940a4e562 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Thu, 23 May 2024 18:21:14 -0600 Subject: [PATCH 28/36] one more flake8 --- pyscan/drivers/oxfordips120.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 44f1e309..6c03dbb3 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -106,7 +106,7 @@ def __init__(self, instrument, debug=False): # magnet specific settings self.field_limit = 8 # oxford triton in 518/1112 self.field_rate_limit = 0.2 - self.field_to_current = 5/22.456 # Tesla/Amp + self.field_to_current = 5 / 22.456 # Tesla/Amp self.debug = debug self.initialize_properties() From 8d70a22785515e57ad1186c693260d0da49c6384 Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Mon, 10 Jun 2024 17:23:13 -0600 Subject: [PATCH 29/36] update IPS120 driver for auto-docstring, auto-update-properties and version --- pyscan/drivers/oxfordips120.py | 40 +++++++++++++--------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index a25c7a1a..7cac183c 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -18,12 +18,15 @@ class OxfordIPS120(InstrumentDriver): Properties: field: float + automated field sweep (blocking) current_rate: float + rate for changing magnet current current_set_point: float + set point for magnet current field_set_point: float - range defined by property field_limit (T): [-8, 8] + set point for magnet field; range defined by attribute field_limit: [-8, 8] field_rate: float - range defined by property field_rate_limit (T/min): [0, 0.2] + rate for changing magnet field; range defined by attribute field_rate_limit: [0, 0.2] Read-only properties: @@ -31,6 +34,8 @@ class OxfordIPS120(InstrumentDriver): current in magnet power supply voltage: float voltage across leads + measured_current: float + measured current in leads output_field: float field from current in magnet power supply (not actual field if persistent) software_voltage_limit: float @@ -39,7 +44,7 @@ class OxfordIPS120(InstrumentDriver): current in magnet where heater was turned off trip_field: float field where last magnet quench occurred - persistent_current: float + persistent_field: float field in magnet where heater was turned off switch_heater_current: float current in switch heater @@ -51,7 +56,7 @@ class OxfordIPS120(InstrumentDriver): resistance magnet_inductance: float inductance - version: str + firmware_version: str power supply model status_string: str @@ -112,6 +117,7 @@ def __init__(self, instrument, debug=False): self.debug = debug self.initialize_properties() + self.update_properties() def query(self, string, timeout=1): ''' @@ -190,6 +196,8 @@ def initialize_properties(self): } ) + # read-only properties + self.add_device_property( { "name": "output_current", @@ -297,7 +305,7 @@ def initialize_properties(self): self.add_device_property( { - "name": "version", + "name": "firmware_version", "query_string": "V", "return_type": str } @@ -311,7 +319,8 @@ def initialize_properties(self): } ) - # write-only property + # write-only properties + self.add_device_property( { "name": "remote_control", @@ -348,25 +357,6 @@ def initialize_properties(self): } ) - self.field - self.field_set_point - self.field_rate - self.current_set_point - self.current_rate - self.output_current - self.voltage - self.output_field - self.software_voltage_limit - self.persistent_current - self.trip_field - self.persistent_field - self.switch_heater_current - self.safe_current_limit_negative - self.safe_current_limit_positive - self.lead_resistance - self.magnet_inductance - self.version - def print_state(self): """ Print operating state of the IPS120 power supply From bab33504ab3585645ed3310a2a16f7548aa0bceb Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Tue, 11 Jun 2024 11:02:02 -0600 Subject: [PATCH 30/36] quick fix rounding error for changing from field to current values --- pyscan/drivers/oxfordips120.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 7cac183c..5c49bdb2 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from datetime import datetime from time import time, sleep +import numpy as np from .instrument_driver import InstrumentDriver @@ -79,6 +80,8 @@ class OxfordIPS120(InstrumentDriver): summarize state of the magnet print_status() not implemented yet + print_properties() + not_implemented yet hold() activity_control with checks to_zero() @@ -174,13 +177,16 @@ def initialize_properties(self): } ) + # TODO: fix all rounding issues related to T->A and precision self.add_device_property( { "name": "current_set_point", "write_string": "$I{}", "query_string": "R5", - "range": [-self.field_limit / self.field_to_current, - self.field_limit / self.field_to_current], + "range": [ + np.round(-self.field_limit / self.field_to_current,2), + np.round(self.field_limit / self.field_to_current,2) + ], "return_type": ips120_float, } ) @@ -190,8 +196,10 @@ def initialize_properties(self): "name": "current_rate", "write_string": "$S{}", "query_string": "R6", - "range": [-self.field_rate_limit / self.field_to_current, - self.field_rate_limit / self.field_to_current], + "range": [ + np.round(-self.field_rate_limit / self.field_to_current, 2), + np.round(self.field_rate_limit / self.field_to_current, 2) + ], "return_type": ips120_float, } ) @@ -202,7 +210,7 @@ def initialize_properties(self): { "name": "output_current", "query_string": "R0", - "readonly": float, + "read_only": float, "return_type": ips120_float } ) @@ -397,6 +405,7 @@ def print_status(self): """ pass + # TODO: combine hold(), to_set_point() and to_zero() into activity property def hold(self): if not self.remote_status: raise IPS120Error( @@ -418,6 +427,7 @@ def to_zero(self): ) self.activity_control = "to_zero" + # TODO: should be a property def heater(self, state): """ Set the state of the heater. Record the time when the heater was changed From 54b23170526b9a423c638d1913e299db784ecb0a Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Tue, 11 Jun 2024 11:03:52 -0600 Subject: [PATCH 31/36] fix bug where read_only properties cause an error in the set_function settings --- pyscan/drivers/instrument_driver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyscan/drivers/instrument_driver.py b/pyscan/drivers/instrument_driver.py index 95421f8e..5add29ce 100644 --- a/pyscan/drivers/instrument_driver.py +++ b/pyscan/drivers/instrument_driver.py @@ -141,6 +141,9 @@ def add_device_property(self, settings): set_function = self.set_indexed_values_property elif 'dict_values' in settings: set_function = self.set_dict_values_property + elif 'read_only' in settings: + # read_only does not require a set_function + pass else: assert False, "Key 'values', 'range', indexed_values', 'read_only', or 'dict_values' must be in settings." From 000557fb7592cf7caf02fedfdc1373defa8793bf Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Tue, 11 Jun 2024 11:11:41 -0600 Subject: [PATCH 32/36] Oxford IPS120 test notebook --- .../OxfordIPS120_test_notebook.ipynb | 238 +++++++++++++----- 1 file changed, 176 insertions(+), 62 deletions(-) diff --git a/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb b/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb index c17079fe..66494d49 100644 --- a/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb +++ b/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb @@ -64,10 +64,10 @@ "text": [ "Heater on\n", "At rest\n", - "activity: To Set Point\n", - "output_field = 0.0\n", - "field_set_point = 0.0\n", - "field_rate = 0.1\n" + "activity: Hold\n", + "output_field = -4.3469\n", + "field_set_point = -5.0\n", + "field_rate = 0.049\n" ] } ], @@ -77,62 +77,80 @@ }, { "cell_type": "markdown", - "id": "8184656e-8650-4f65-9b63-4d9b2f703165", + "id": "d478caef-dd58-4011-98a5-c98e0689658c", "metadata": {}, "source": [ - "## direct read and write commands" + "## Testing magnet driver" ] }, { "cell_type": "code", "execution_count": 4, - "id": "3a00c3ac-1ad2-49d2-a7cd-a12812a5cf58", + "id": "ef5d656f-7e6c-4aec-95cc-8929cf983ac4", + "metadata": {}, + "outputs": [], + "source": [ + "# don't sweep anywhere while testing\n", + "devices.magnet.remote()\n", + "devices.magnet.hold()" + ] + }, + { + "cell_type": "markdown", + "id": "cd14c8ac-9c16-4f1b-8d53-04679a783e76", + "metadata": {}, + "source": [ + "### magnet limits" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "88bc36e2-5c61-47a8-93a8-9a1f151e9fd2", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "X00A1C3H1M10P03\n" + "8\n", + "0.2\n", + "0.22265764161026008\n" ] } ], "source": [ - "result = devices.magnet.query('X')\n", - "print(result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "338da351-ca41-4488-94e6-2a229749ef6b", - "metadata": { - "scrolled": true - }, - "outputs": [], - "source": [ - "# extra read if necessary\n", - "result = instruments.ips120.read()\n", - "print(result)" + "print(devices.magnet.field_limit)\n", + "print(devices.magnet.field_rate_limit)\n", + "print(devices.magnet.field_to_current)" ] }, { "cell_type": "markdown", - "id": "d478caef-dd58-4011-98a5-c98e0689658c", + "id": "f8d98e0d-6d3d-459c-92a9-db6a867c80df", "metadata": {}, "source": [ - "## Testing magnet driver" + "**Fix this safe current discrepency for the Triton magnet**" ] }, { "cell_type": "code", "execution_count": 6, - "id": "ef5d656f-7e6c-4aec-95cc-8929cf983ac4", + "id": "5f49bd77-26c9-4605-80a0-8fd1be41dd46", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "40.42\n", + "8.999821873886713\n" + ] + } + ], "source": [ - "# don't sweep anywhere while testing\n", - "devices.magnet.hold()" + "print(devices.magnet.safe_current_limit_positive)\n", + "print(devices.magnet.safe_current_limit_positive*devices.magnet.field_to_current)" ] }, { @@ -153,7 +171,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.0\n" + "-5.0\n" ] } ], @@ -188,7 +206,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.1\n" + "0.049\n" ] } ], @@ -223,7 +241,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.0\n" + "-22.456\n" ] } ], @@ -258,7 +276,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.449\n" + "0.22\n" ] } ], @@ -301,7 +319,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.0\n" + "-19.523\n" ] } ], @@ -319,7 +337,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-0.02\n" + "-0.37\n" ] } ], @@ -337,7 +355,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.09\n" + "-19.35\n" ] } ], @@ -355,7 +373,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.0\n" + "-4.3469\n" ] } ], @@ -409,7 +427,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-32.8032\n" + "-32.803\n" ] } ], @@ -481,7 +499,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "15.7\n" + "18.7\n" ] } ], @@ -509,7 +527,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 29, "id": "51951497", "metadata": {}, "outputs": [ @@ -522,12 +540,12 @@ } ], "source": [ - "print(devices.magnet.version)" + "print(devices.magnet.firmware_version)" ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 30, "id": "cffbc249-92f7-4b6e-9b34-a190d058ed53", "metadata": {}, "outputs": [ @@ -535,7 +553,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "X00A0C3H1M10P03\n" + "X00A0C3H1M10P51\n" ] } ], @@ -545,7 +563,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 31, "id": "1d463b2e-5c90-4cac-b8d7-a1b6e2a5374c", "metadata": {}, "outputs": [ @@ -771,7 +789,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": null, "id": "eb302f8d-f3f9-443c-bba8-0cbd15918f10", "metadata": {}, "outputs": [], @@ -782,7 +800,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 47, "id": "e0aefdce", "metadata": {}, "outputs": [], @@ -792,7 +810,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 48, "id": "17af6062", "metadata": {}, "outputs": [], @@ -802,7 +820,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 49, "id": "5f06f175", "metadata": {}, "outputs": [], @@ -810,6 +828,52 @@ "devices.magnet.activity_control = \"to_set_point\"" ] }, + { + "cell_type": "markdown", + "id": "044bc85a-98a2-4b3d-9677-9d6e100df74f", + "metadata": {}, + "source": [ + "### list all properties" + ] + }, + { + "cell_type": "code", + "execution_count": 64, + "id": "aa92cb52-a30a-43cd-ae4a-7868cecc3e91", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "field_set_point: -5.0\n", + "field_rate: 0.2\n", + "current_set_point: -22.4562\n", + "current_rate: 0.898\n", + "output_current: -22.4562\n", + "voltage: -0.43\n", + "measured_current: -22.27\n", + "output_field: -5.0\n", + "software_voltage_limit: 2.5\n", + "persistent_current: -19.5229\n", + "trip_field: -32.8032\n", + "persistent_field: -4.34687\n", + "switch_heater_current: 20.0\n", + "safe_current_limit_negative: -40.42\n", + "safe_current_limit_positive: 40.42\n", + "lead_resistance: 18.6\n", + "magnet_inductance: 51.7\n", + "firmware_version: IPS120-10 Version 3.07 (c) OXFORD 1996\n", + "status_string: X00A1C3H1M10P71\n" + ] + } + ], + "source": [ + "for prop in devices.magnet.get_pyscan_properties():\n", + " if 'write_only' not in devices.magnet[f'_{prop}_settings']:\n", + " print(f'{prop}: {devices.magnet[prop]}')" + ] + }, { "cell_type": "markdown", "id": "eaebdb30-c0f3-417a-942b-8da07126992e", @@ -820,7 +884,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 50, "id": "9c303dc9-a9d9-43bf-bbd8-9ec87380c857", "metadata": {}, "outputs": [], @@ -830,7 +894,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 51, "id": "a935a8bb-2f23-45bc-a598-11e61ffca0d2", "metadata": {}, "outputs": [], @@ -840,7 +904,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": null, "id": "14712428-c581-478c-b572-b610c7bdbbe3", "metadata": { "scrolled": true @@ -852,7 +916,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": null, "id": "372d5931-36ca-4c8a-8a15-c7d0baa01164", "metadata": {}, "outputs": [], @@ -862,7 +926,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 52, "id": "851993a8-6326-479c-b479-c03eef087c7e", "metadata": {}, "outputs": [], @@ -872,7 +936,7 @@ }, { "cell_type": "code", - "execution_count": 60, + "execution_count": 55, "id": "e3c46264-dcef-44e9-a36f-44e61a9abe8c", "metadata": {}, "outputs": [], @@ -882,7 +946,7 @@ }, { "cell_type": "code", - "execution_count": 61, + "execution_count": 54, "id": "f5d09c36-fbf5-4b5b-a88f-d44c13e27cd3", "metadata": {}, "outputs": [], @@ -892,7 +956,7 @@ }, { "cell_type": "code", - "execution_count": 62, + "execution_count": 56, "id": "8d9547e5-422c-4d3b-bf9c-cb2478305bda", "metadata": {}, "outputs": [ @@ -903,7 +967,7 @@ "\n", "system status (operation) : Normal (X1=0)\n", "system status (voltage) : Normal (X2=0)\n", - "Activity ; To Zero (A=2)\n", + "Activity ; Hold (A=0)\n", "LOC/REM status ; Remote & Unlocked (C=3)\n", "Heater ; On (H=1)\n", "Mode (rate) ; Tesla, Immediate, Fast (M1=1)\n", @@ -926,7 +990,7 @@ }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 57, "id": "3c81d80e-aa14-4b31-89ee-dea52a10b00e", "metadata": {}, "outputs": [], @@ -947,7 +1011,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 58, "id": "f5e5be95-8cca-4161-a084-1a30bebf3e4f", "metadata": {}, "outputs": [], @@ -958,7 +1022,7 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 59, "id": "7bb77b13-3f67-4fb1-98db-4c75675dfa80", "metadata": {}, "outputs": [], @@ -968,13 +1032,63 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": null, "id": "b0624387-61c4-455c-b717-6a8c08b6796b", "metadata": {}, "outputs": [], "source": [ "devices.magnet.heater(\"off\")" ] + }, + { + "cell_type": "markdown", + "id": "bef62839-3949-4601-9a69-ad6418a80706", + "metadata": {}, + "source": [ + "### go to a field" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "1a2b561a-cc7d-4cc2-bba5-3f5c3249fd12", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.hold()\n", + "devices.magnet.heater('on')\n", + "devices.magnet.field_set_point = -5\n", + "devices.magnet.field_rate = 0.2\n", + "devices.magnet.to_set_point()" + ] + }, + { + "cell_type": "markdown", + "id": "36712e85-c835-4353-80e3-23558d278fa9", + "metadata": {}, + "source": [ + "## help" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24c3e4a0-405e-4d81-a11f-29774fe1e62a", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet?" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb1c7cf8-d943-4e2f-8a3b-6a948b297927", + "metadata": {}, + "outputs": [], + "source": [ + "devices.magnet.output_field?" + ] } ], "metadata": { From 8f144cbe06a5c63e6a389c05b48c4e901c2f875e Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Tue, 11 Jun 2024 11:29:28 -0600 Subject: [PATCH 33/36] flake8 fixes --- pyscan/drivers/oxfordips120.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 5c49bdb2..abf00307 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -184,8 +184,8 @@ def initialize_properties(self): "write_string": "$I{}", "query_string": "R5", "range": [ - np.round(-self.field_limit / self.field_to_current,2), - np.round(self.field_limit / self.field_to_current,2) + np.round(-self.field_limit / self.field_to_current, 2), + np.round(self.field_limit / self.field_to_current, 2) ], "return_type": ips120_float, } From 1786cb7cf610c44627ee1aaab88d039e6824020b Mon Sep 17 00:00:00 2001 From: Michael Lilly Date: Tue, 11 Jun 2024 16:44:33 -0600 Subject: [PATCH 34/36] addressing requested changes to documentation and initialization --- pyscan/drivers/oxfordips120.py | 282 ++++++++++++++++++--------------- 1 file changed, 158 insertions(+), 124 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index abf00307..26af20e7 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- from datetime import datetime -from time import time, sleep +from time import sleep, time + import numpy as np + from .instrument_driver import InstrumentDriver @@ -16,83 +18,97 @@ class OxfordIPS120(InstrumentDriver): Attributes ---------- - Properties: - - field: float - automated field sweep (blocking) - current_rate: float - rate for changing magnet current - current_set_point: float - set point for magnet current - field_set_point: float - set point for magnet field; range defined by attribute field_limit: [-8, 8] - field_rate: float - rate for changing magnet field; range defined by attribute field_rate_limit: [0, 0.2] - - Read-only properties: - - output_current: float - current in magnet power supply - voltage: float - voltage across leads - measured_current: float - measured current in leads - output_field: float - field from current in magnet power supply (not actual field if persistent) - software_voltage_limit: float - max voltage - persistent_current: float - current in magnet where heater was turned off - trip_field: float - field where last magnet quench occurred - persistent_field: float - field in magnet where heater was turned off - switch_heater_current: float - current in switch heater - safe_current_limit_negative: float - max negative current - safe_current_limit_positive: float - max positive current - lead_resistance: float - resistance - magnet_inductance: float - inductance - firmware_version: str - power supply model - status_string: str - - Write-only properties: - - remote_control - "local_locked", "remote_locked", "local_unlocked", "remote_unlocked" - communications_protocol - "normal", "extended" - heater_control - "off", "on", "force" - activity_control - "hold", "to_set_point", "to_zero", "clamp" + (Properties) + + field: float + Get/set target field sweep (blocking) + current_rate: float + Get/set rate for changing magnet current + current_set_point: float + Get/set set point for magnet current + field_set_point: float + Get/set set point for magnet field; range defined by attribute field_limit: [-8, 8] + field_rate: float + Get/set rate for changing magnet field; range defined by attribute field_rate_limit: [0, 0.2] + + (Read-only properties) + + output_current: read-only + Get current in magnet power supply + voltage: read-only + Get voltage across leads + measured_current: read-only + Get measured current in leads + output_field: read-only + Get field from current in magnet power supply (not actual field if persistent) + software_voltage_limit: read-only + Get max voltage + persistent_current: read-only + Get current in magnet where heater was turned off + trip_field: read-only + Get field where last magnet quench occurred + persistent_field: read-only + Get field in magnet where heater was turned off + switch_heater_current: read-only + Get current in switch heater + safe_current_limit_negative: read-only + Get max negative current + safe_current_limit_positive: read-only + Get max positive current + lead_resistance: read-only + Get resistance + magnet_inductance: read-only + Get inductance + firmware_version: read-only + Get power supply model + status_string: read-only + + (Write-only properties) + + remote_control: str + Set local/remote to "local_locked", "remote_locked", "local_unlocked", "remote_unlocked" + communications_protocol: str + Set rate and set point precision to "normal", "extended" + heater_control: str + Set heater to "off", "on", "force" + activity_control: str + Set sweep activity to "hold", "to_set_point", "to_zero", "clamp" Methods ------- - heater() - turn heater on/off and deal correctly with persistent mode - print_state() - summarize state of the magnet - print_status() - not implemented yet - print_properties() - not_implemented yet - hold() - activity_control with checks - to_zero() - activity_control with checks - to_set_point() - activity_control with checks - clamp() - activity_control with checks + heater() + turn heater on/off and deal correctly with persistent mode + print_state() + summarize state of the magnet + print_status() + not implemented yet + print_properties() + not_implemented yet + hold() + activity_control with checks + to_zero() + activity_control with checks + to_set_point() + activity_control with checks + clamp() + activity_control with checks """ - def __init__(self, instrument, debug=False): + def __init__( + self, + instrument, + *, + field_limit, + field_rate_limit, + field_to_current_ratio, + debug=False, + ): + """ + OxfordIPS120 initilization requires keyword arguments: + field_limit: maximum magnetic field (T) + field_rate_limit: maximum sweep rate (T/min) + field_to_current_ratio: constant to switch between field and current (T/A) + """ super().__init__(instrument, debug) self._version = "0.1.0" @@ -114,19 +130,21 @@ def __init__(self, instrument, debug=False): print(f"Message in buffer: {message}") # magnet specific settings - self.field_limit = 8 # oxford triton in 518/1112 - self.field_rate_limit = 0.2 - self.field_to_current = 5 / 22.456 # Tesla/Amp + self._field_limit = field_limit + self._field_rate_limit = field_rate_limit + self._field_to_current_ratio = field_to_current_ratio self.debug = debug self.initialize_properties() self.update_properties() + self.check_field_to_current_ratio() + def query(self, string, timeout=1): - ''' + """ Overload query for IPS120 Read until status indicates full message is received. - ''' + """ # message = self.query(string) self.write(string) stb = self.instrument.read_stb() @@ -148,6 +166,28 @@ def query(self, string, timeout=1): raise IPS120Error("IPS120 query Timeout") + def check_field_to_current_ratio(self): + """ + Compare safe_current_limit_positive to the field_limit + """ + + calculated_current_limit = self._field_limit / self._field_to_current_ratio + # IPS120 has 4 dig accuracy in ratio and 2 dig accuracy in safe_current limits + # we need to check the calculated current to tolerance of +- 0.01 A + tol = 0.01 + safe_current = self.safe_current_limit_positive + if (calculated_current_limit > safe_current - tol) and ( + calculated_current_limit < safe_current + tol + ): + + return + + assert ( + False + ), "field_limit and field_to_current_ratio ({}) not consistent with IPS120 safe_current ({})".format( + calculated_current_limit, safe_current + ) + def initialize_properties(self): """ The IPS120 does not have traditional get/set parameters. The control @@ -185,7 +225,7 @@ def initialize_properties(self): "query_string": "R5", "range": [ np.round(-self.field_limit / self.field_to_current, 2), - np.round(self.field_limit / self.field_to_current, 2) + np.round(self.field_limit / self.field_to_current, 2), ], "return_type": ips120_float, } @@ -198,7 +238,7 @@ def initialize_properties(self): "query_string": "R6", "range": [ np.round(-self.field_rate_limit / self.field_to_current, 2), - np.round(self.field_rate_limit / self.field_to_current, 2) + np.round(self.field_rate_limit / self.field_to_current, 2), ], "return_type": ips120_float, } @@ -211,39 +251,31 @@ def initialize_properties(self): "name": "output_current", "query_string": "R0", "read_only": float, - "return_type": ips120_float + "return_type": ips120_float, } ) self.add_device_property( - { - "name": "voltage", - "query_string": "R1", - "return_type": ips120_float - } + {"name": "voltage", "query_string": "R1", "return_type": ips120_float} ) self.add_device_property( { "name": "measured_current", "query_string": "R2", - "return_type": ips120_float + "return_type": ips120_float, } ) self.add_device_property( - { - "name": "output_field", - "query_string": "R7", - "return_type": ips120_float - } + {"name": "output_field", "query_string": "R7", "return_type": ips120_float} ) self.add_device_property( { "name": "software_voltage_limit", "query_string": "R15", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -251,7 +283,7 @@ def initialize_properties(self): { "name": "persistent_current", "query_string": "R16", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -259,7 +291,7 @@ def initialize_properties(self): { "name": "trip_field", # mA "query_string": "R17", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -267,7 +299,7 @@ def initialize_properties(self): { "name": "persistent_field", "query_string": "R18", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -275,7 +307,7 @@ def initialize_properties(self): { "name": "switch_heater_current", "query_string": "R20", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -283,7 +315,7 @@ def initialize_properties(self): { "name": "safe_current_limit_negative", "query_string": "R21", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -291,7 +323,7 @@ def initialize_properties(self): { "name": "safe_current_limit_positive", "query_string": "R22", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -299,7 +331,7 @@ def initialize_properties(self): { "name": "lead_resistance", "query_string": "R23", - "return_type": ips120_float + "return_type": ips120_float, } ) @@ -307,24 +339,16 @@ def initialize_properties(self): { "name": "magnet_inductance", "query_string": "R24", - "return_type": ips120_float + "return_type": ips120_float, } ) self.add_device_property( - { - "name": "firmware_version", - "query_string": "V", - "return_type": str - } + {"name": "firmware_version", "query_string": "V", "return_type": str} ) self.add_device_property( - { - "name": "status_string", - "query_string": "X", - "return_type": str - } + {"name": "status_string", "query_string": "X", "return_type": str} ) # write-only properties @@ -333,8 +357,13 @@ def initialize_properties(self): { "name": "remote_control", "write_string": "$C{}", - "dict_values": {"local_locked": 0, "remote_locked": 1, "local_unlocked": 2, "remote_unlocked": 3}, - "return_type": int + "dict_values": { + "local_locked": 0, + "remote_locked": 1, + "local_unlocked": 2, + "remote_unlocked": 3, + }, + "return_type": int, } ) @@ -342,8 +371,11 @@ def initialize_properties(self): { "name": "communications_protocol", "write_string": "Q{}", # no echo since this command clears output buffer - "dict_values": {"normal": 0, "extended": 4}, # do not allow switch to - "return_type": int + "dict_values": { + "normal": 0, + "extended": 4, + }, # do not allow switch to + "return_type": int, } ) @@ -352,7 +384,7 @@ def initialize_properties(self): "name": "heater_control", "write_string": "$H{}", "indexed_values": ["off", "on", "force"], - "return_type": int + "return_type": int, } ) @@ -361,7 +393,7 @@ def initialize_properties(self): "name": "activity_control", "write_string": "$A{}", "dict_values": {"hold": 0, "to_set_point": 1, "to_zero": 2, "clamp": 4}, - "return_type": int + "return_type": int, } ) @@ -440,7 +472,9 @@ def heater(self, state): # TODO: deal with persistent magnet if self.persistent_status: - raise IPS120Error("take magnet out of persistent mode before changing heater") + raise IPS120Error( + "take magnet out of persistent mode before changing heater" + ) if state == "on": if not self.heater_status: @@ -469,9 +503,9 @@ def local(self, locked=False): @property def field(self): - ''' + """ TODO add settings dictionary with range to make this consistent with pyscan property - ''' + """ self._field = self.output_field return self._field @@ -683,16 +717,16 @@ class IPS120Error(Exception): def ips120_float(string): - ''' + """ Helper function used with return_type to drop character and convert to a float. - ''' + """ return float(string[1:]) def ips120_int(string): - ''' + """ Helper function used with return_type to drop character and convert to a float. - ''' + """ return int(string[1:]) From 8ebc4b8eeb8ea530afb4ed84e6ad26c495f0c1c3 Mon Sep 17 00:00:00 2001 From: Mike Lilly Date: Fri, 14 Jun 2024 16:32:53 -0600 Subject: [PATCH 35/36] test changes to help, field_limit, field_rate_limit, field_to_current_ratio and jupyter test notebook --- .../OxfordIPS120_test_notebook.ipynb | 356 ++++++++++++------ pyscan/drivers/oxfordips120.py | 12 +- 2 files changed, 248 insertions(+), 120 deletions(-) diff --git a/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb b/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb index 66494d49..f04c9ab9 100644 --- a/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb +++ b/drivers_test_notebooks/OxfordIPS120_test_notebook.ipynb @@ -37,6 +37,48 @@ "from IPython.display import display" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "a3932781-6336-48fe-9166-eb7cbf5006e9", + "metadata": {}, + "outputs": [], + "source": [ + "ips120 = ps.new_instrument(gpib_address=25)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4f032e08-49b6-4ad7-8566-e442dd36519e", + "metadata": {}, + "outputs": [], + "source": [ + "ips120.read_termination = \"\\r\"\n", + "ips120.write_termination = \"\\r\"\n", + "print(ips120.read_stb())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86adfcb3-0009-4fb8-b9aa-a2ae19ea8fa5", + "metadata": {}, + "outputs": [], + "source": [ + "ips120.query('X')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d9c822d-3353-4087-a2d6-c840e510ead9", + "metadata": {}, + "outputs": [], + "source": [ + "ips120.read()" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -49,7 +91,7 @@ "instruments = ps.ItemAttribute()\n", "instruments.ips120 = ps.new_instrument(gpib_address=25)\n", "devices = ps.ItemAttribute()\n", - "devices.magnet = ps.OxfordIPS120(instruments.ips120, debug=False)" + "devices.magnet = ps.OxfordIPS120(instruments.ips120,field_limit=8, field_rate_limit=0.2, field_to_current_ratio=0.2227)" ] }, { @@ -62,12 +104,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "Heater on\n", + "Heater off\n", "At rest\n", - "activity: Hold\n", - "output_field = -4.3469\n", - "field_set_point = -5.0\n", - "field_rate = 0.049\n" + "activity: To Zero\n", + "output_field = 0.0\n", + "field_set_point = 5.0\n", + "field_rate = 0.2\n" ] } ], @@ -105,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "id": "88bc36e2-5c61-47a8-93a8-9a1f151e9fd2", "metadata": {}, "outputs": [ @@ -115,27 +157,19 @@ "text": [ "8\n", "0.2\n", - "0.22265764161026008\n" + "0.2227\n" ] } ], "source": [ - "print(devices.magnet.field_limit)\n", - "print(devices.magnet.field_rate_limit)\n", - "print(devices.magnet.field_to_current)" - ] - }, - { - "cell_type": "markdown", - "id": "f8d98e0d-6d3d-459c-92a9-db6a867c80df", - "metadata": {}, - "source": [ - "**Fix this safe current discrepency for the Triton magnet**" + "print(devices.magnet._field_limit)\n", + "print(devices.magnet._field_rate_limit)\n", + "print(devices.magnet._field_to_current_ratio)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "id": "5f49bd77-26c9-4605-80a0-8fd1be41dd46", "metadata": {}, "outputs": [ @@ -143,14 +177,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "40.42\n", - "8.999821873886713\n" + "35.93\n", + "8.001611\n" ] } ], "source": [ "print(devices.magnet.safe_current_limit_positive)\n", - "print(devices.magnet.safe_current_limit_positive*devices.magnet.field_to_current)" + "print(devices.magnet.safe_current_limit_positive*devices.magnet._field_to_current_ratio)" ] }, { @@ -163,7 +197,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "id": "c7a6bf3c-c19c-45e5-8dbd-baf701b954bf", "metadata": {}, "outputs": [ @@ -171,7 +205,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-5.0\n" + "5.0\n" ] } ], @@ -181,7 +215,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "id": "dccb8369-e95c-4c9c-8dd7-8bfa5e76b923", "metadata": {}, "outputs": [], @@ -198,7 +232,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "id": "f3940807", "metadata": {}, "outputs": [ @@ -206,7 +240,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.049\n" + "0.2\n" ] } ], @@ -216,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "id": "e5596e5f", "metadata": {}, "outputs": [], @@ -233,7 +267,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "id": "158473d0", "metadata": {}, "outputs": [ @@ -241,7 +275,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-22.456\n" + "22.456\n" ] } ], @@ -251,7 +285,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "id": "93df54cf", "metadata": {}, "outputs": [], @@ -268,7 +302,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "id": "622aaf10", "metadata": {}, "outputs": [ @@ -276,7 +310,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "0.22\n" + "0.9\n" ] } ], @@ -286,7 +320,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "id": "ece5e1d5", "metadata": {}, "outputs": [], @@ -311,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "id": "660c4640-ed68-44c1-af4a-5551a347e0e0", "metadata": {}, "outputs": [ @@ -319,7 +353,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-19.523\n" + "0.0\n" ] } ], @@ -329,7 +363,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "id": "45844641", "metadata": {}, "outputs": [ @@ -337,7 +371,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-0.37\n" + "-0.02\n" ] } ], @@ -347,7 +381,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "id": "7178f259", "metadata": {}, "outputs": [ @@ -355,7 +389,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-19.35\n" + "0.09\n" ] } ], @@ -365,7 +399,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "id": "4c4f458d", "metadata": {}, "outputs": [ @@ -373,7 +407,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-4.3469\n" + "0.0\n" ] } ], @@ -383,7 +417,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "id": "0492a897", "metadata": {}, "outputs": [ @@ -401,7 +435,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "id": "313cd9bc", "metadata": {}, "outputs": [ @@ -419,7 +453,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "id": "46a98f50", "metadata": {}, "outputs": [ @@ -427,7 +461,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-32.803\n" + "-0.078\n" ] } ], @@ -437,7 +471,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "id": "9cbfcec5-d623-4bc7-ab2e-7e9729bd7eef", "metadata": {}, "outputs": [ @@ -455,7 +489,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 25, "id": "9e498bd2", "metadata": {}, "outputs": [ @@ -463,7 +497,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "-40.42\n" + "-35.93\n" ] } ], @@ -473,7 +507,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 26, "id": "9270c945", "metadata": {}, "outputs": [ @@ -481,7 +515,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "40.42\n" + "35.93\n" ] } ], @@ -491,7 +525,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 27, "id": "67b5a56b", "metadata": {}, "outputs": [ @@ -499,7 +533,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "18.7\n" + "18.3\n" ] } ], @@ -509,7 +543,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 28, "id": "6f67ea45", "metadata": {}, "outputs": [ @@ -553,7 +587,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "X00A0C3H1M10P51\n" + "X00A0C3H0M10P03\n" ] } ], @@ -589,7 +623,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "heater_status = True\n" + "heater_status = False\n" ] } ], @@ -810,7 +844,7 @@ }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 50, "id": "17af6062", "metadata": {}, "outputs": [], @@ -838,7 +872,7 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 51, "id": "aa92cb52-a30a-43cd-ae4a-7868cecc3e91", "metadata": {}, "outputs": [ @@ -846,25 +880,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "field_set_point: -5.0\n", - "field_rate: 0.2\n", - "current_set_point: -22.4562\n", - "current_rate: 0.898\n", - "output_current: -22.4562\n", - "voltage: -0.43\n", - "measured_current: -22.27\n", - "output_field: -5.0\n", + "field_set_point: 4.99996\n", + "field_rate: 0.0495\n", + "current_set_point: 22.456\n", + "current_rate: 0.222\n", + "output_current: 0.0\n", + "voltage: -0.02\n", + "measured_current: 0.11\n", + "output_field: 0.0\n", "software_voltage_limit: 2.5\n", - "persistent_current: -19.5229\n", - "trip_field: -32.8032\n", - "persistent_field: -4.34687\n", + "persistent_current: 0.0\n", + "trip_field: -0.0776\n", + "persistent_field: 0.0\n", "switch_heater_current: 20.0\n", - "safe_current_limit_negative: -40.42\n", - "safe_current_limit_positive: 40.42\n", - "lead_resistance: 18.6\n", + "safe_current_limit_negative: -35.93\n", + "safe_current_limit_positive: 35.93\n", + "lead_resistance: 18.3\n", "magnet_inductance: 51.7\n", "firmware_version: IPS120-10 Version 3.07 (c) OXFORD 1996\n", - "status_string: X00A1C3H1M10P71\n" + "status_string: X00A2C3H1M10P03\n" ] } ], @@ -884,7 +918,7 @@ }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 52, "id": "9c303dc9-a9d9-43bf-bbd8-9ec87380c857", "metadata": {}, "outputs": [], @@ -894,7 +928,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 53, "id": "a935a8bb-2f23-45bc-a598-11e61ffca0d2", "metadata": {}, "outputs": [], @@ -904,7 +938,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 54, "id": "14712428-c581-478c-b572-b610c7bdbbe3", "metadata": { "scrolled": true @@ -916,7 +950,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 55, "id": "372d5931-36ca-4c8a-8a15-c7d0baa01164", "metadata": {}, "outputs": [], @@ -926,7 +960,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 56, "id": "851993a8-6326-479c-b479-c03eef087c7e", "metadata": {}, "outputs": [], @@ -936,7 +970,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 57, "id": "e3c46264-dcef-44e9-a36f-44e61a9abe8c", "metadata": {}, "outputs": [], @@ -946,7 +980,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 58, "id": "f5d09c36-fbf5-4b5b-a88f-d44c13e27cd3", "metadata": {}, "outputs": [], @@ -956,7 +990,7 @@ }, { "cell_type": "code", - "execution_count": 56, + "execution_count": 59, "id": "8d9547e5-422c-4d3b-bf9c-cb2478305bda", "metadata": {}, "outputs": [ @@ -967,7 +1001,7 @@ "\n", "system status (operation) : Normal (X1=0)\n", "system status (voltage) : Normal (X2=0)\n", - "Activity ; Hold (A=0)\n", + "Activity ; To Zero (A=2)\n", "LOC/REM status ; Remote & Unlocked (C=3)\n", "Heater ; On (H=1)\n", "Mode (rate) ; Tesla, Immediate, Fast (M1=1)\n", @@ -990,7 +1024,7 @@ }, { "cell_type": "code", - "execution_count": 57, + "execution_count": 61, "id": "3c81d80e-aa14-4b31-89ee-dea52a10b00e", "metadata": {}, "outputs": [], @@ -1003,7 +1037,7 @@ "value = devices.magnet.output_field\n", "db = 0.01\n", "new_value = value + db\n", - "if new_value > devices.magnet.field_limit:\n", + "if new_value > devices.magnet._field_limit:\n", " new_value = value - db\n", "devices.magnet.field = new_value\n", "assert devices.magnet.output_field == new_value" @@ -1011,7 +1045,7 @@ }, { "cell_type": "code", - "execution_count": 58, + "execution_count": 62, "id": "f5e5be95-8cca-4161-a084-1a30bebf3e4f", "metadata": {}, "outputs": [], @@ -1022,7 +1056,7 @@ }, { "cell_type": "code", - "execution_count": 59, + "execution_count": 63, "id": "7bb77b13-3f67-4fb1-98db-4c75675dfa80", "metadata": {}, "outputs": [], @@ -1032,7 +1066,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 64, "id": "b0624387-61c4-455c-b717-6a8c08b6796b", "metadata": {}, "outputs": [], @@ -1040,28 +1074,6 @@ "devices.magnet.heater(\"off\")" ] }, - { - "cell_type": "markdown", - "id": "bef62839-3949-4601-9a69-ad6418a80706", - "metadata": {}, - "source": [ - "### go to a field" - ] - }, - { - "cell_type": "code", - "execution_count": 60, - "id": "1a2b561a-cc7d-4cc2-bba5-3f5c3249fd12", - "metadata": {}, - "outputs": [], - "source": [ - "devices.magnet.hold()\n", - "devices.magnet.heater('on')\n", - "devices.magnet.field_set_point = -5\n", - "devices.magnet.field_rate = 0.2\n", - "devices.magnet.to_set_point()" - ] - }, { "cell_type": "markdown", "id": "36712e85-c835-4353-80e3-23558d278fa9", @@ -1072,20 +1084,136 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 65, "id": "24c3e4a0-405e-4d81-a11f-29774fe1e62a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[1;31mType:\u001b[0m OxfordIPS120\n", + "\u001b[1;31mString form:\u001b[0m \n", + "\u001b[1;31mFile:\u001b[0m c:\\snl\\projects\\pyscan\\pyscan\\drivers\\oxfordips120.py\n", + "\u001b[1;31mDocstring:\u001b[0m \n", + "Class to control Oxford Instruments Intelligent Power Supply IPS120 Superconducting Magnet Power Supply\n", + "\n", + "Parameters\n", + "----------\n", + "instrument :\n", + " Visa string or an instantiated instrument (return value from\n", + " :func:`.new_instrument`)\n", + "\n", + "Attributes\n", + "----------\n", + "(Properties)\n", + "\n", + "field: float\n", + " Get/set target field sweep (blocking)\n", + "current_rate: float\n", + " Get/set rate for changing magnet current\n", + "current_set_point: float\n", + " Get/set set point for magnet current\n", + "field_set_point: float\n", + " Get/set set point for magnet field; range defined by attribute field_limit: [-8, 8]\n", + "field_rate: float\n", + " Get/set rate for changing magnet field; range defined by attribute field_rate_limit: [0, 0.2]\n", + "\n", + "(Read-only properties)\n", + "\n", + "output_current: read-only\n", + " Get current in magnet power supply\n", + "voltage: read-only\n", + " Get voltage across leads\n", + "measured_current: read-only\n", + " Get measured current in leads\n", + "output_field: read-only\n", + " Get field from current in magnet power supply (not actual field if persistent)\n", + "software_voltage_limit: read-only\n", + " Get max voltage\n", + "persistent_current: read-only\n", + " Get current in magnet where heater was turned off\n", + "trip_field: read-only\n", + " Get field where last magnet quench occurred\n", + "persistent_field: read-only\n", + " Get field in magnet where heater was turned off\n", + "switch_heater_current: read-only\n", + " Get current in switch heater\n", + "safe_current_limit_negative: read-only\n", + " Get max negative current\n", + "safe_current_limit_positive: read-only\n", + " Get max positive current\n", + "lead_resistance: read-only\n", + " Get resistance\n", + "magnet_inductance: read-only\n", + " Get inductance\n", + "firmware_version: read-only\n", + " Get power supply model\n", + "status_string: read-only\n", + "\n", + "(Write-only properties)\n", + "\n", + "remote_control: str\n", + " Set local/remote to \"local_locked\", \"remote_locked\", \"local_unlocked\", \"remote_unlocked\"\n", + "communications_protocol: str\n", + " Set rate and set point precision to \"normal\", \"extended\"\n", + "heater_control: str\n", + " Set heater to \"off\", \"on\", \"force\"\n", + "activity_control: str\n", + " Set sweep activity to \"hold\", \"to_set_point\", \"to_zero\", \"clamp\"\n", + "\n", + "Methods\n", + "-------\n", + "heater()\n", + " turn heater on/off and deal correctly with persistent mode\n", + "print_state()\n", + " summarize state of the magnet\n", + "print_status()\n", + " not implemented yet\n", + "print_properties()\n", + " not_implemented yet\n", + "hold()\n", + " activity_control with checks\n", + "to_zero()\n", + " activity_control with checks\n", + "to_set_point()\n", + " activity_control with checks\n", + "clamp()\n", + " activity_control with checks\n", + "\u001b[1;31mInit docstring:\u001b[0m\n", + "OxfordIPS120 initilization requires keyword arguments:\n", + " field_limit: maximum magnetic field (T)\n", + " field_rate_limit: maximum sweep rate (T/min)\n", + " field_to_current_ratio: constant to switch between field and current (T/A)" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "devices.magnet?" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 66, "id": "cb1c7cf8-d943-4e2f-8a3b-6a948b297927", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "\u001b[1;31mType:\u001b[0m property\n", + "\u001b[1;31mString form:\u001b[0m \n", + "\u001b[1;31mDocstring:\u001b[0m \n", + "No doc string found for output_field.\n", + "Please update the drivers doc string to include this attribute." + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "devices.magnet.output_field?" ] diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 26af20e7..20fa84a4 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -202,7 +202,7 @@ def initialize_properties(self): "name": "field_set_point", "write_string": "$J{}", "query_string": "R8", - "range": [-self.field_limit, self.field_limit], + "range": [-self._field_limit, self._field_limit], "return_type": ips120_float, } ) @@ -212,7 +212,7 @@ def initialize_properties(self): "name": "field_rate", "write_string": "$T{}", "query_string": "R9", - "range": [-self.field_rate_limit, self.field_rate_limit], + "range": [-self._field_rate_limit, self._field_rate_limit], "return_type": ips120_float, } ) @@ -224,8 +224,8 @@ def initialize_properties(self): "write_string": "$I{}", "query_string": "R5", "range": [ - np.round(-self.field_limit / self.field_to_current, 2), - np.round(self.field_limit / self.field_to_current, 2), + np.round(-self._field_limit / self._field_to_current_ratio, 2), + np.round(self._field_limit / self._field_to_current_ratio, 2), ], "return_type": ips120_float, } @@ -237,8 +237,8 @@ def initialize_properties(self): "write_string": "$S{}", "query_string": "R6", "range": [ - np.round(-self.field_rate_limit / self.field_to_current, 2), - np.round(self.field_rate_limit / self.field_to_current, 2), + np.round(-self._field_rate_limit / self._field_to_current_ratio, 2), + np.round(self._field_rate_limit / self._field_to_current_ratio, 2), ], "return_type": ips120_float, } From aa04148adb58e207ec6683edc3f5be7f1bcc6638 Mon Sep 17 00:00:00 2001 From: i-am-mounce <146007668+i-am-mounce@users.noreply.github.com> Date: Tue, 18 Jun 2024 09:37:33 -0600 Subject: [PATCH 36/36] doc(driver): fixed double tabUpdate oxfordips120.py --- pyscan/drivers/oxfordips120.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyscan/drivers/oxfordips120.py b/pyscan/drivers/oxfordips120.py index 20fa84a4..82bd3ef9 100644 --- a/pyscan/drivers/oxfordips120.py +++ b/pyscan/drivers/oxfordips120.py @@ -66,13 +66,13 @@ class OxfordIPS120(InstrumentDriver): (Write-only properties) remote_control: str - Set local/remote to "local_locked", "remote_locked", "local_unlocked", "remote_unlocked" + Set local/remote to "local_locked", "remote_locked", "local_unlocked", "remote_unlocked" communications_protocol: str - Set rate and set point precision to "normal", "extended" + Set rate and set point precision to "normal", "extended" heater_control: str - Set heater to "off", "on", "force" + Set heater to "off", "on", "force" activity_control: str - Set sweep activity to "hold", "to_set_point", "to_zero", "clamp" + Set sweep activity to "hold", "to_set_point", "to_zero", "clamp" Methods -------