From 17cb9bbc0bccbee86bea5b45cbc0e479f77e936e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas.kluyver@xfel.eu> Date: Tue, 7 Mar 2023 18:11:38 +0100 Subject: [PATCH] Initial work on LPD Mini correction NB --- notebooks/LPD/LPD_Mini_Correct.ipynb | 628 +++++++++++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 notebooks/LPD/LPD_Mini_Correct.ipynb diff --git a/notebooks/LPD/LPD_Mini_Correct.ipynb b/notebooks/LPD/LPD_Mini_Correct.ipynb new file mode 100644 index 000000000..da1fc2825 --- /dev/null +++ b/notebooks/LPD/LPD_Mini_Correct.ipynb @@ -0,0 +1,628 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# LPD Mini Offline Correction #\n", + "\n", + "Author: European XFEL Data Analysis Group" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2018-12-03T15:19:56.056417Z", + "start_time": "2018-12-03T15:19:56.003012Z" + } + }, + "outputs": [], + "source": [ + "# Input parameters\n", + "in_folder = \"/gpfs/exfel/exp/FXE/202321/p004576/raw/\" # the folder to read data from, required\n", + "out_folder = \"/gpfs/exfel/data/scratch/kluyvert/correct-lpdmini-p4576-r48\" # the folder to output to, required\n", + "metadata_folder = '' # Directory containing calibration_metadata.yml when run by xfel-calibrate.\n", + "sequences = [-1] # Sequences to correct, use [-1] for all\n", + "modules = [-1] # Modules indices to correct, use [-1] for all, only used when karabo_da is empty\n", + "karabo_da = [''] # Data aggregators names to correct, use [''] for all\n", + "run = 48 # run to process, required\n", + "\n", + "# Source parameters\n", + "karabo_id = 'FXE_DET_LPD_MINI' # Karabo domain for detector.\n", + "input_source = '{karabo_id}/DET/0CH0:xtdf' # Input fast data source.\n", + "output_source = '{karabo_id}/CORR/0CH0:output' # Output fast data source, empty to use same as input.\n", + "\n", + "# CalCat parameters\n", + "creation_time = \"\" # The timestamp to use with Calibration DB. Required Format: \"YYYY-MM-DD hh:mm:ss\" e.g. 2019-07-04 11:02:41\n", + "cal_db_interface = '' # Not needed, compatibility with current webservice.\n", + "cal_db_timeout = 0 # Not needed, compatbility with current webservice.\n", + "cal_db_root = '/gpfs/exfel/d/cal/caldb_store'\n", + "\n", + "# Operating conditions\n", + "bias_voltage = 250.0 # Detector bias voltage.\n", + "capacitor = '5pF' # Capacitor setting: 5pF or 50pF\n", + "photon_energy = 9.2 # Photon energy in keV.\n", + "use_cell_order = False # Whether to use memory cell order as a detector condition (not stored for older constants)\n", + "\n", + "# Correction parameters\n", + "offset_corr = True # Offset correction.\n", + "rel_gain = True # Gain correction based on RelativeGain constant.\n", + "ff_map = True # Gain correction based on FFMap constant.\n", + "gain_amp_map = True # Gain correction based on GainAmpMap constant.\n", + "\n", + "# Output options\n", + "overwrite = True # set to True if existing data should be overwritten\n", + "chunks_data = 1 # HDF chunk size for pixel data in number of frames.\n", + "chunks_ids = 32 # HDF chunk size for cellId and pulseId datasets.\n", + "create_virtual_cxi_in = '' # Folder to create virtual CXI files in (for each sequence).\n", + "\n", + "# Parallelization options\n", + "sequences_per_node = 1 # Sequence files to process per node\n", + "max_nodes = 8 # Maximum number of SLURM jobs to split correction work into\n", + "num_workers = 8 # Worker processes per node, 8 is safe on 768G nodes but won't work on 512G.\n", + "num_threads_per_worker = 32 # Number of threads per worker.\n", + "\n", + "def balance_sequences(in_folder, run, sequences, sequences_per_node, karabo_da, max_nodes):\n", + " from xfel_calibrate.calibrate import balance_sequences as bs\n", + " return bs(in_folder, run, sequences, sequences_per_node, karabo_da, max_nodes=max_nodes)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2018-12-03T15:19:56.990566Z", + "start_time": "2018-12-03T15:19:56.058378Z" + } + }, + "outputs": [], + "source": [ + "from collections import OrderedDict\n", + "from pathlib import Path\n", + "from time import perf_counter\n", + "import gc\n", + "import re\n", + "import warnings\n", + "\n", + "import numpy as np\n", + "import h5py\n", + "\n", + "import matplotlib\n", + "matplotlib.use('agg')\n", + "import matplotlib.pyplot as plt\n", + "%matplotlib inline\n", + "\n", + "from calibration_client import CalibrationClient\n", + "from calibration_client.modules import CalibrationConstantVersion\n", + "import extra_data as xd\n", + "import extra_geom as xg\n", + "import pasha as psh\n", + "\n", + "from extra_data.components import LPD1M\n", + "\n", + "from cal_tools.lpdalgs import correct_lpd_frames\n", + "from cal_tools.lpdlib import get_mem_cell_order\n", + "from cal_tools.tools import CalibrationMetadata, calcat_creation_time\n", + "from cal_tools.files import DataFile\n", + "from cal_tools.restful_config import restful_config" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Prepare environment" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "file_re = re.compile(r'^RAW-R(\\d{4})-(\\w+\\d+)-S(\\d{5})$') # This should probably move to cal_tools\n", + "\n", + "run_folder = Path(in_folder) / f'r{run:04d}'\n", + "out_folder = Path(out_folder)\n", + "out_folder.mkdir(exist_ok=True)\n", + "\n", + "output_source = output_source or input_source\n", + "\n", + "cal_db_root = Path(cal_db_root)\n", + "\n", + "metadata = CalibrationMetadata(metadata_folder or out_folder)\n", + "\n", + "creation_time = calcat_creation_time(in_folder, run, creation_time)\n", + "print(f'Using {creation_time.isoformat()} as creation time')\n", + "\n", + "# Pick all modules/aggregators or those selected.\n", + "if not karabo_da or karabo_da == ['']:\n", + " if not modules or modules == [-1]:\n", + " modules = list(range(16))\n", + "\n", + " karabo_da = [f'LPD{i:02d}' for i in modules]\n", + " \n", + "# Pick all sequences or those selected.\n", + "if not sequences or sequences == [-1]:\n", + " do_sequence = lambda seq: True\n", + "else:\n", + " do_sequence = [int(x) for x in sequences].__contains__ \n", + " \n", + "# List of detector sources.\n", + "det_inp_sources = [input_source.format(karabo_id=karabo_id)]\n", + "\n", + "mem_cells = 512" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Select data to process" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data_to_process = []\n", + "\n", + "for inp_path in run_folder.glob('RAW-*.h5'):\n", + " match = file_re.match(inp_path.stem)\n", + " \n", + " if match[2] not in karabo_da or not do_sequence(int(match[3])):\n", + " continue\n", + " \n", + " outp_path = out_folder / 'CORR-R{run:04d}-{aggregator}-S{seq:05d}.h5'.format(\n", + " run=int(match[1]), aggregator=match[2], seq=int(match[3]))\n", + "\n", + " data_to_process.append((match[2], inp_path, outp_path))\n", + "\n", + "print('Files to process:')\n", + "for data_descr in sorted(data_to_process, key=lambda x: f'{x[0]}{x[1]}'):\n", + " print(f'{data_descr[0]}\\t{data_descr[1]}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Obtain and prepare calibration constants" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Connect to CalCat.\n", + "calcat_config = restful_config['calcat']\n", + "client = CalibrationClient(\n", + " base_api_url=calcat_config['base-api-url'],\n", + " use_oauth2=calcat_config['use-oauth2'],\n", + " client_id=calcat_config['user-id'],\n", + " client_secret=calcat_config['user-secret'],\n", + " user_email=calcat_config['user-email'],\n", + " token_url=calcat_config['token-url'],\n", + " refresh_url=calcat_config['refresh-url'],\n", + " auth_url=calcat_config['auth-url'],\n", + " scope='')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "metadata = CalibrationMetadata(metadata_folder or out_folder)\n", + "# Constant paths & timestamps are saved under retrieved-constants in calibration_metadata.yml\n", + "const_yaml = metadata.setdefault(\"retrieved-constants\", {})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "const_data = {}\n", + "const_load_mp = psh.ProcessContext(num_workers=24)\n", + "\n", + "module_const_shape = (mem_cells, 32, 256, 3) # cells, slow_scan, fast_scan, gain\n", + "\n", + "if const_yaml: # Read constants from YAML file.\n", + " start = perf_counter()\n", + " for da, ccvs in const_yaml.items():\n", + "\n", + " for calibration_name, ccv in ccvs['constants'].items():\n", + " if ccv['file-path'] is None:\n", + " warnings.warn(f\"Missing {calibration_name} for {da}\")\n", + " continue\n", + "\n", + " dtype = np.uint32 if calibration_name.startswith('BadPixels') else np.float32\n", + "\n", + " const_data[(da, calibration_name)] = dict(\n", + " path=Path(ccv['file-path']),\n", + " dataset=ccv['dataset-name'],\n", + " data=const_load_mp.alloc(shape=module_const_shape, dtype=dtype)\n", + " )\n", + "else: # Retrieve constants from CALCAT.\n", + " dark_calibrations = {\n", + " 1: 'Offset', # np.float32\n", + " 14: 'BadPixelsDark' # should be np.uint32, but is np.float64\n", + " }\n", + "\n", + " base_condition = [\n", + " dict(parameter_id=1, value=bias_voltage), # Sensor bias voltage\n", + " dict(parameter_id=15, value=capacitor), # Feedback capacitor\n", + " ]\n", + " if use_cell_order:\n", + " # Read the order of memory cells used\n", + " raw_data = xd.DataCollection.from_paths([e[1] for e in data_to_process])\n", + " cell_ids_pattern_s = get_mem_cell_order(raw_data, det_inp_sources)\n", + " print(\"Memory cells order:\", cell_ids_pattern_s)\n", + "\n", + " dark_condition = base_condition + [\n", + " dict(parameter_id=30, value=cell_ids_pattern_s), # Memory cell order\n", + " ]\n", + " else:\n", + " dark_condition = base_condition.copy()\n", + "\n", + " illuminated_calibrations = {\n", + " 20: 'BadPixelsFF', # np.uint32\n", + " 42: 'GainAmpMap', # np.float32\n", + " 43: 'FFMap', # np.float32\n", + " 44: 'RelativeGain' # np.float32\n", + " }\n", + "\n", + " illuminated_condition = base_condition + [\n", + " dict(parameter_id=3, value=photon_energy), # Source energy\n", + " ]\n", + "\n", + " print('Querying calibration database', end='', flush=True)\n", + " start = perf_counter()\n", + " for calibrations, condition in [\n", + " (dark_calibrations, dark_condition),\n", + " (illuminated_calibrations, illuminated_condition)\n", + " ]:\n", + " resp = CalibrationConstantVersion.get_closest_by_time_by_detector_conditions(\n", + " client, karabo_id, list(calibrations.keys()),\n", + " {'parameters_conditions_attributes': condition},\n", + " karabo_da='', event_at=creation_time.isoformat()\n", + " )\n", + "\n", + " if not resp['success']:\n", + " raise RuntimeError(resp)\n", + "\n", + " for ccv in resp['data']:\n", + " cc = ccv['calibration_constant']\n", + " module_num = int(ccv['physical_detector_unit']['karabo_da'].split('/')[1])\n", + " calibration_name = calibrations[cc['calibration_id']]\n", + " \n", + " dtype = np.uint32 if calibration_name.startswith('BadPixels') else np.float32\n", + " \n", + " const_data[(module_num, calibration_name)] = dict(\n", + " path=Path(ccv['path_to_file']) / ccv['file_name'],\n", + " dataset=ccv['data_set_name'],\n", + " data=const_load_mp.alloc(shape=module_const_shape, dtype=dtype)\n", + " )\n", + " print('.', end='', flush=True)\n", + " \n", + "total_time = perf_counter() - start\n", + "print(f'{total_time:.1f}s')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def load_constant_dataset(wid, index, const_descr):\n", + " ccv_entry = const_data[const_descr]\n", + " \n", + " with h5py.File(cal_db_root / ccv_entry['path'], 'r') as fp:\n", + " fp[ccv_entry['dataset'] + '/data'].read_direct(ccv_entry['data'])\n", + " \n", + " print('.', end='', flush=True)\n", + "\n", + "print('Loading calibration data', end='', flush=True)\n", + "start = perf_counter()\n", + "const_load_mp.map(load_constant_dataset, list(const_data.keys()))\n", + "total_time = perf_counter() - start\n", + "\n", + "print(f'{total_time:.1f}s')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "module_nums = sorted({n for n, _ in const_data})\n", + "nmods = len(module_nums)\n", + "const_type_names = {t for _, t in const_data}\n", + "\n", + "const_shape = (mem_cells, 32 * len(module_nums), 256, 3) # cells, slow_scan, fast_scan, gain\n", + "const_slices = [slice(i * 32, (i+1) * 32) for i in range(len(module_nums))]\n", + "raw_data_slices = [slice((n-1) * 32, n * 32) for n in module_nums]\n", + "\n", + "def _assemble_constant(arr, calibration_name):\n", + " for mod_num, sl in zip(module_nums, const_slices):\n", + " arr[:, sl] = const_data[mod_num, calibration_name]\n", + "\n", + "offset_const = np.zeros(const_shape, dtype=np.float32)\n", + "if offset_corr:\n", + " _assemble_constant(offset_const, 'Offset')\n", + "\n", + "mask_const = np.zeros(const_shape, dtype=np.uint32)\n", + "_assemble_constant(mask_const, 'BadPixelsDark')\n", + "\n", + "gain_const = np.ones(const_shape, dtype=np.float32)\n", + "if rel_gain:\n", + " _assemble_constant(gain_const, 'RelativeGain')\n", + "\n", + "if ff_map:\n", + " ff_map_gain = np.ones(const_shape, dtype=np.float32)\n", + " _assemble_constant(ff_map_gain, 'FFMap')\n", + " gain_const *= ff_map_gain\n", + "\n", + " if 'BadPixelsFF' in const_type_names:\n", + " badpix_ff = np.zeros(const_shape, dtype=np.uint32)\n", + " _assemble_constant(badpix_ff, 'BadPixelsFF')\n", + " mask_const |= badpix_ff\n", + "\n", + "if gain_amp_map:\n", + " gain_amp_map = np.zeros(const_shape, dtype=np.float32)\n", + " _assemble_constant(gain_amp_map, 'GainAmpMap')\n", + " gain_const *= gain_amp_map" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def correct_file(wid, index, work):\n", + " aggregator, inp_path, outp_path = work\n", + " module_index = int(aggregator[-2:])\n", + " \n", + " start = perf_counter()\n", + " dc = xd.H5File(inp_path, inc_suspect_trains=False).select('*', 'image.*', require_all=True)\n", + " inp_source = dc[input_source.format(karabo_id=karabo_id)]\n", + " open_time = perf_counter() - start\n", + " \n", + " # Load raw data for this file.\n", + " # Reshaping gets rid of the extra 1-len dimensions without\n", + " # mangling the frame axis for an actual frame count of 1.\n", + " start = perf_counter()\n", + " in_raw = inp_source['image.data'].ndarray()\n", + " in_cell = inp_source['image.cellId'].ndarray().reshape(-1)\n", + " in_pulse = inp_source['image.pulseId'].ndarray().reshape(-1)\n", + " read_time = perf_counter() - start\n", + " \n", + " # Slice modules from input data\n", + " data_shape = (in_raw.shape[0], nmods * 32, 256)\n", + " in_sliced = np.zeros(data_shape, dtype=in_raw.dtype)\n", + " for i, sl in enumerate(raw_data_slices):\n", + " in_sliced[:, i*32:(i+1)*32] = in_raw[..., sl, :]\n", + " \n", + " # Allocate output arrays.\n", + " out_data = np.zeros(in_sliced.shape, dtype=np.float32)\n", + " out_gain = np.zeros(in_sliced.shape, dtype=np.uint8)\n", + " out_mask = np.zeros(in_sliced.shape, dtype=np.uint32)\n", + " \n", + " start = perf_counter()\n", + " correct_lpd_frames(in_sliced, in_cell,\n", + " out_data, out_gain, out_mask,\n", + " ccv_offsets[aggregator], ccv_gains[aggregator], ccv_masks[aggregator],\n", + " num_threads=num_threads_per_worker)\n", + " correct_time = perf_counter() - start\n", + " \n", + " image_counts = inp_source['image.data'].data_counts(labelled=False)\n", + " \n", + " start = perf_counter()\n", + " if (not outp_path.exists() or overwrite) and image_counts.sum() > 0:\n", + " outp_source_name = output_source.format(karabo_id=karabo_id, module_index=module_index)\n", + "\n", + " with DataFile(outp_path, 'w') as outp_file: \n", + " outp_file.create_index(dc.train_ids, from_file=dc.files[0])\n", + " outp_file.create_metadata(like=dc, instrument_channels=(f'{outp_source_name}/image',))\n", + " \n", + " outp_source = outp_file.create_instrument_source(outp_source_name)\n", + " \n", + " outp_source.create_index(image=image_counts)\n", + " outp_source.create_key('image.cellId', data=in_cell,\n", + " chunks=(min(chunks_ids, in_cell.shape[0]),))\n", + " outp_source.create_key('image.pulseId', data=in_pulse,\n", + " chunks=(min(chunks_ids, in_pulse.shape[0]),))\n", + " outp_source.create_key('image.data',\n", + " data=out_data.reshape(data_shape[0], nmods, 32, 256),\n", + " chunks=(min(chunks_data, out_data.shape[0]), nmods, 32, 256))\n", + " outp_source.create_compressed_key('image.gain', data=out_gain)\n", + " outp_source.create_compressed_key('image.mask', data=out_mask)\n", + " write_time = perf_counter() - start\n", + " \n", + " total_time = open_time + read_time + correct_time + write_time\n", + " frame_rate = in_raw.shape[0] / total_time\n", + " \n", + " print('{}\\t{}\\t{:.3f}\\t{:.3f}\\t{:.3f}\\t{:.3f}\\t{:.3f}\\t{}\\t{:.1f}'.format(\n", + " wid, aggregator, open_time, read_time, correct_time, write_time, total_time,\n", + " in_raw.shape[0], frame_rate))\n", + " \n", + " in_raw = None\n", + " in_cell = None\n", + " in_pulse = None\n", + " out_data = None\n", + " out_gain = None\n", + " out_mask = None\n", + " gc.collect()\n", + "\n", + "print('worker\\tDA\\topen\\tread\\tcorrect\\twrite\\ttotal\\tframes\\trate')\n", + "start = perf_counter()\n", + "psh.ProcessContext(num_workers=num_workers).map(correct_file, data_to_process)\n", + "total_time = perf_counter() - start\n", + "print(f'Total time: {total_time:.1f}s')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Data preview for first train" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "geom = xg.LPD_MiniGeometry.from_module_positions(\n", + " [(0, i * 40) for i in range(nmods)]\n", + ")\n", + "\n", + "output_paths = [outp_path for _, _, outp_path in data_to_process if outp_path.exists()]\n", + "dc = xd.DataCollection.from_paths(output_paths).select_trains(np.s_[0])\n", + "\n", + "det = LPDMini(dc, detector_name=karabo_id)\n", + "data = det.get_array('image.data')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Intensity histogram across all cells" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "left_edge_ratio = 0.01\n", + "right_edge_ratio = 0.99\n", + "\n", + "fig, ax = plt.subplots(num=1, clear=True, figsize=(15, 6))\n", + "values, bins, _ = ax.hist(np.ravel(data.data), bins=2000, range=(-1500, 2000))\n", + "\n", + "def find_nearest_index(array, value):\n", + " return (np.abs(array - value)).argmin()\n", + "\n", + "cum_values = np.cumsum(values)\n", + "vmin = bins[find_nearest_index(cum_values, cum_values[-1]*left_edge_ratio)]\n", + "vmax = bins[find_nearest_index(cum_values, cum_values[-1]*right_edge_ratio)]\n", + "\n", + "max_value = values.max()\n", + "ax.vlines([vmin, vmax], 0, max_value, color='red', linewidth=5, alpha=0.2)\n", + "ax.text(vmin, max_value, f'{left_edge_ratio*100:.0f}%',\n", + " color='red', ha='center', va='bottom', size='large')\n", + "ax.text(vmax, max_value, f'{right_edge_ratio*100:.0f}%',\n", + " color='red', ha='center', va='bottom', size='large')\n", + "ax.text(vmax+(vmax-vmin)*0.01, max_value/2, 'Colormap interval',\n", + " color='red', rotation=90, ha='left', va='center', size='x-large')\n", + "\n", + "ax.set_xlim(vmin-(vmax-vmin)*0.1, vmax+(vmax-vmin)*0.1)\n", + "ax.set_ylim(0, max_value*1.1)\n", + "pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### First memory cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(num=2, figsize=(15, 15), clear=True, nrows=1, ncols=1)\n", + "geom.plot_data_fast(data[:, 0, 0], ax=ax, vmin=vmin, vmax=vmax)\n", + "pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Train average" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "ExecuteTime": { + "end_time": "2018-11-13T18:24:57.547563Z", + "start_time": "2018-11-13T18:24:56.995005Z" + }, + "scrolled": false + }, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(num=3, figsize=(15, 15), clear=True, nrows=1, ncols=1)\n", + "geom.plot_data_fast(data[:, 0].mean(axis=1), ax=ax, vmin=vmin, vmax=vmax)\n", + "pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Lowest gain stage per pixel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "highest_gain_stage = det.get_array('image.gain', pulses=np.s_[:]).max(axis=(1, 2))\n", + "\n", + "fig, ax = plt.subplots(num=4, figsize=(15, 15), clear=True, nrows=1, ncols=1)\n", + "p = geom.plot_data_fast(highest_gain_stage, ax=ax, vmin=0, vmax=2);\n", + "\n", + "cb = ax.images[0].colorbar\n", + "cb.set_ticks([0, 1, 2])\n", + "cb.set_ticklabels(['High gain', 'Medium gain', 'Low gain'])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Offline Cal", + "language": "python", + "name": "offline-cal" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} -- GitLab