diff --git a/notebooks_tsqr/TEMPLATE_logrep.yaml b/notebooks_tsqr/TEMPLATE_logrep.yaml index c6eb836..de6c99f 100644 --- a/notebooks_tsqr/TEMPLATE_logrep.yaml +++ b/notebooks_tsqr/TEMPLATE_logrep.yaml @@ -1,6 +1,9 @@ # For use with a Times Square notebook title: TEMPLATE for LR -description: Prototype 1 +description: > + Copy and rename this ipynb and yaml sidecar into a new + pair of files (.ipynb, .yaml). + The TEMPLATE_* files will eventually be hidden in Times Square. authors: - name: Steve Pothier slack: Steve Pothier diff --git a/notebooks_tsqr/efd.ipynb b/notebooks_tsqr/efd.ipynb new file mode 100644 index 0000000..a65f418 --- /dev/null +++ b/notebooks_tsqr/efd.ipynb @@ -0,0 +1,356 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "0", + "metadata": {}, + "outputs": [], + "source": [ + "# Parameters. Set defaults here.\n", + "# Times Square replaces this cell with the user's parameters.\n", + "record_limit = '999'" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "\n", + "## Imports and General Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "# Only use packages available in the Rubin Science Platform\n", + "import requests\n", + "from collections import defaultdict\n", + "import pandas as pd\n", + "from pprint import pp, pformat\n", + "from urllib.parse import urlencode\n", + "from IPython.display import FileLink, display_markdown\n", + "from matplotlib import pyplot as plt\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "env = 'usdf_dev' # usdf-dev, tucson, slac, summit\n", + "log_name = 'narrativelog'\n", + "log = log_name\n", + "limit = int(record_limit)\n", + "response_timeout = 3.05 # seconds, how long to wait for connection\n", + "read_timeout = 20 # seconds\n", + "\n", + "timeout = (float(response_timeout), float(read_timeout))\n", + "\n", + "# RUNNING_INSIDE_JUPYTERLAB is True when running under Times Square\n", + "server = os.environ.get('EXTERNAL_INSTANCE_URL', \n", + " 'https://tucson-teststand.lsst.codes')\n", + "service = f'{server}/{log}'\n", + "service" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "\n", + "## Setup Source" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "md = f'### Will retrieve from {service}'\n", + "display_markdown(md, raw=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "recs = None\n", + "ok = True\n", + "\n", + "# is_human=either&is_valid=either&offset=0&limit=50' \n", + "# site_ids=tucson&message_text=wubba&min_level=0&max_level=999&user_ids=spothier&user_agents=LOVE\n", + "# tags=love&exclude_tags=ignore_message\n", + "qparams = dict(is_human='either',\n", + " is_valid='either',\n", + " limit=limit,\n", + " )\n", + "qstr = urlencode(qparams)\n", + "url = f'{service}/messages?{qstr}'\n", + "\n", + "ignore_fields = set(['tags', 'urls', 'message_text', 'id', 'date_added', \n", + " 'obs_id', 'day_obs', 'seq_num', 'parent_id', 'user_id',\n", + " 'date_invalidated', 'date_begin', 'date_end',\n", + " 'time_lost', # float\n", + " #'systems','subsystems','cscs', # values are lists, special handling\n", + " ])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "display_markdown(f'## Get (up to {limit}) Records', raw=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO Often fails on first request. Find out why!\n", + "try:\n", + " response = requests.get(url, timeout=timeout)\n", + "except:\n", + " pass \n", + " \n", + "try:\n", + " print(f'Attempt to get logs from {url=}')\n", + " response = requests.get(url, timeout=timeout)\n", + " response.raise_for_status()\n", + " recs = response.json()\n", + " flds = set(recs[0].keys())\n", + " facflds = flds - ignore_fields\n", + " # facets(field) = set(value-1, value-2, ...)\n", + " facets = {fld: set([str(r[fld])\n", + " for r in recs if not isinstance(r[fld], list)]) \n", + " for fld in facflds}\n", + "except Exception as err:\n", + " ok = False\n", + " print(f'ERROR getting {log} from {env=} using {url=}: {err=}')\n", + "numf = len(flds) if ok else 0\n", + "numr = len(recs) if ok else 0\n", + "print(f'Retrieved {numr} records, each with {numf} fields.')" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "\n", + "## Tables of (mostly raw) results" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "### Fields names provided in records from log." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "pd.DataFrame(flds, columns=['Field Name'])" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "### Facets from log records.\n", + "A *facet* is the set all of values found for a field in the retrieved records. Facets are only calculated for some fields." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "display(pd.DataFrame.from_dict(facets, orient='index'))\n", + "display(facets)" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "### Table of selected log record fields.\n", + "Table can be retrieved as CSV file for local use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": { + "jupyter": { + "source_hidden": true + } + }, + "outputs": [], + "source": [ + "cols = ['date_added', 'time_lost']\n", + "df = pd.DataFrame(recs)[cols]\n", + "\n", + "# Allow download of CSV version of DataFrame\n", + "csvfile = 'tl.csv'\n", + "df.to_csv(csvfile)\n", + "myfile = FileLink(csvfile)\n", + "print('Table available as CSV file: ')\n", + "display(myfile)\n", + "df" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame(recs)\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "\n", + "## Plots from log" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "x = [r['date_added'] for r in recs]\n", + "y = [r['time_lost'] for r in recs]\n", + "plt.plot(x, y) \n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "\n", + "## Raw Content Analysis" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### Example of one record" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "rec = recs[-1]\n", + "\n", + "msg = rec[\"message_text\"]\n", + "md = f'Message text from log:\\n> {msg}'\n", + "display_markdown(md, raw=True)\n", + "\n", + "display(rec)" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "\n", + "## Stakeholder Elicitation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "#EXTERNAL_INSTANCE_URL\n", + "ed = dict(os.environ.items())\n", + "with pd.option_context('display.max_rows', None,):\n", + " print(pd.DataFrame(ed.values(), index=ed.keys()))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks_tsqr/efd.yaml b/notebooks_tsqr/efd.yaml new file mode 100644 index 0000000..de6c99f --- /dev/null +++ b/notebooks_tsqr/efd.yaml @@ -0,0 +1,19 @@ +# For use with a Times Square notebook +title: TEMPLATE for LR +description: > + Copy and rename this ipynb and yaml sidecar into a new + pair of files (.ipynb, .yaml). + The TEMPLATE_* files will eventually be hidden in Times Square. +authors: + - name: Steve Pothier + slack: Steve Pothier +tags: + - reporting + - prototype +parameters: + record_limit: + type: integer + description: Max number of records to output + default: 99 + minimum: 1 + maximum: 9999 diff --git a/notebooks_tsqr/exposurelog.ipynb b/notebooks_tsqr/exposurelog.ipynb index b8ff30b..a8dc90c 100644 --- a/notebooks_tsqr/exposurelog.ipynb +++ b/notebooks_tsqr/exposurelog.ipynb @@ -9,7 +9,7 @@ "source": [ "# Parameters. Set defaults here.\n", "# Times Square replaces this cell with the user's parameters.\n", - "record_limit = '999'" + "record_limit = '9999'" ] }, { @@ -52,8 +52,10 @@ "read_timeout = 20 # seconds\n", "timeout = (float(response_timeout), float(read_timeout))\n", "\n", - "server = os.environ.get('EXTERNAL_INSTANCE_URL', \n", - " 'https://tucson-teststand.lsst.codes')\n", + "summit = 'https://summit-lsp.lsst.codes'\n", + "usdf = 'https://usdf-rsp-dev.slac.stanford.edu'\n", + "tucson = 'https://tucson-teststand.lsst.codes'\n", + "server = os.environ.get('EXTERNAL_INSTANCE_URL', tucson)\n", "log = 'exposurelog'\n", "service = f'{server}/{log}'\n", "service" @@ -78,16 +80,6 @@ "recs = None\n", "ok = True\n", "\n", - "# is_human=either&is_valid=either&offset=0&limit=50' \n", - "# site_ids=tucson&message_text=wubba&min_level=0&max_level=999&user_ids=spothier&user_agents=LOVE\n", - "# tags=love&exclude_tags=ignore_message\n", - "qparams = dict(is_human='either',\n", - " is_valid='either',\n", - " limit=limit,\n", - " )\n", - "qstr = urlencode(qparams)\n", - "url = f'{service}/messages?{qstr}'\n", - "\n", "ignore_fields = set(['tags', 'urls', 'message_text', 'id', 'date_added', \n", " 'obs_id', 'day_obs', 'seq_num', 'parent_id', 'user_id',\n", " 'date_invalidated', 'date_begin', 'date_end',\n", @@ -112,6 +104,14 @@ "metadata": {}, "outputs": [], "source": [ + "# Endpoint: messages\n", + "\n", + "# is_human=either&is_valid=either&offset=0&limit=50' \n", + "# site_ids=tucson&message_text=wubba&min_level=0&max_level=999&user_ids=spothier&user_agents=LOVE\n", + "# tags=love&exclude_tags=ignore_message\n", + "qstr = urlencode(dict(is_human='either',is_valid='either', limit=limit))\n", + "url = f'{service}/messages?{qstr}'\n", + "\n", "try:\n", " print(f'Attempt to get logs from {url=}')\n", " response = requests.get(url, timeout=timeout)\n", @@ -125,7 +125,7 @@ " for fld in facflds}\n", "except Exception as err:\n", " ok = False\n", - " print(f'ERROR getting {log} from {env=} using {url=}: {err=}')\n", + " print(f'ERROR getting {log} from {url=}: {err=}')\n", "numf = len(flds) if ok else 0\n", "numr = len(recs) if ok else 0\n", "print(f'Retrieved {numr} records, each with {numf=} fields.')" @@ -174,7 +174,8 @@ "metadata": {}, "outputs": [], "source": [ - "pd.DataFrame.from_dict(facets, orient='index')" + "display(pd.DataFrame.from_dict(facets, orient='index'))\n", + "facets" ] }, { diff --git a/notebooks_tsqr/narrativelog.ipynb b/notebooks_tsqr/narrativelog.ipynb index 62c0bdc..37d9e65 100644 --- a/notebooks_tsqr/narrativelog.ipynb +++ b/notebooks_tsqr/narrativelog.ipynb @@ -9,7 +9,7 @@ "source": [ "# Parameters. Set defaults here.\n", "# Times Square replaces this cell with the user's parameters.\n", - "record_limit = '999'" + "record_limit = '9999'" ] }, { @@ -74,11 +74,7 @@ "cell_type": "code", "execution_count": null, "id": "5", - "metadata": { - "jupyter": { - "source_hidden": true - } - }, + "metadata": {}, "outputs": [], "source": [ "recs = None\n", @@ -137,7 +133,7 @@ " for fld in facflds}\n", "except Exception as err:\n", " ok = False\n", - " print(f'ERROR getting {log} from {env=} using {url=}: {err=}')\n", + " print(f'ERROR getting {log} from {url=}: {err=}')\n", "numf = len(flds) if ok else 0\n", "numr = len(recs) if ok else 0\n", "print(f'Retrieved {numr} records, each with {numf=} fields.')" @@ -186,7 +182,8 @@ "metadata": {}, "outputs": [], "source": [ - "pd.DataFrame.from_dict(facets, orient='index')" + "display(pd.DataFrame.from_dict(facets, orient='index'))\n", + "facets" ] }, { diff --git a/notebooks_tsqr/scaffolding.org b/notebooks_tsqr/scaffolding.org index 6c1c85a..5033b08 100644 --- a/notebooks_tsqr/scaffolding.org +++ b/notebooks_tsqr/scaffolding.org @@ -14,7 +14,7 @@ + num_recs_in_example + min_date + max_date - + + : # Parameters. Set defaults here. diff --git a/notebooks_tsqr/sources_dashboard.ipynb b/notebooks_tsqr/sources_dashboard.ipynb new file mode 100644 index 0000000..4036bdf --- /dev/null +++ b/notebooks_tsqr/sources_dashboard.ipynb @@ -0,0 +1,327 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Logging and Reporting" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "## Table of contents\n", + "* [Parameters](#params)\n", + "* [Imports and setup](#imports)\n", + "* [Try every server](#every-server)\n", + "* [Report](#report)" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "\n", + "## Parameters\n", + "The first code cell must contain parameters with string values for compatibility with Times Square.\n", + "\n", + "See: https://rsp.lsst.io/v/usdfdev/guides/times-square/index.html" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "#Parameters\n", + "env = 'tucson' # usdf-dev, tucson, slac, summit\n", + "record_limit = '9999'\n", + "response_timeout = '3.05' # seconds, how long to wait for connection\n", + "read_timeout = '20' # seconds" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "\n", + "## Imports and General Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "from collections import defaultdict\n", + "import pandas as pd\n", + "from pprint import pp" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "limit = int(record_limit)\n", + "timeout = (float(response_timeout), float(read_timeout))\n", + "\n", + "# Env list comes from drop-down menu top of:\n", + "# https://rsp.lsst.io/v/usdfdev/guides/times-square/\n", + "envs = dict(\n", + " #rubin_usdf_dev = '',\n", + " #data_lsst_cloud = '',\n", + " #usdf = '',\n", + " #base_data_facility = '',\n", + " summit = 'https://summit-lsp.lsst.codes',\n", + " usdf_dev = 'https://usdf-rsp-dev.slac.stanford.edu',\n", + " #rubin_idf_int = '',\n", + " tucson = 'https://tucson-teststand.lsst.codes',\n", + ")\n", + "envs" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "\n", + "## Try to access every Server, every Log in our list\n", + "We call the combination of a specific Server and specific Log a \"service\".\n", + "This is a First Look. As such, we don't try to get a useful list of records. \n", + "Instead, we save a few pieces of data from each service. A more tailored web-service call should be done to get useful records. For each service, we save:\n", + "1. The number of records retrieved\n", + "1. The list of fields found in a record (we assume all records from a service have the same fields)\n", + "1. An example of 1-2 records.\n", + "1. The [Facets](https://en.wikipedia.org/wiki/Faceted_search) of the service for all service fields that are not explictly excluded." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "verbose=False\n", + "fields = defaultdict(set) # fields[(env,log)] = {field1, field2, ...}\n", + "examples = defaultdict(list) # examples[(env,log)] = [rec1, rec2]\n", + "results = defaultdict(dict) # results[(env,log)] = dict(server,url, ok, numfields, numrecs)\n", + "facets = defaultdict(dict) # facets[(env,log)] = dict(field) = set(value-1, value-2, ...)\n", + "\n", + "# Dumb! Using same ignore set for all LOGS.\n", + "ignore_fields = set(['tags', 'urls', 'message_text', 'id', 'date_added', \n", + " 'obs_id', 'day_obs', 'seq_num', 'parent_id', 'user_id',\n", + " 'date_invalidated', 'date_begin', 'date_end',\n", + " 'time_lost', # float\n", + " #'systems','subsystems','cscs', # values are lists, special handling\n", + " ])\n", + "for env,server in envs.items():\n", + " ok = True\n", + " try:\n", + " recs = None\n", + " log = 'exposurelog'\n", + " #!url = f'{server}/{log}/messages?is_human=either&is_valid=either&offset=0&{limit=}'\n", + " url = f'{server}/{log}/messages?is_human=either&is_valid=either&{limit=}'\n", + " print(f'\\nAttempt to get logs from {url=}')\n", + " response = requests.get(url, timeout=timeout)\n", + " response.raise_for_status()\n", + " recs = response.json()\n", + " flds = set(recs[0].keys())\n", + " if verbose:\n", + " print(f'Number of {log} records: {len(recs):,}')\n", + " print(f'Got {log} fields: {flds}')\n", + " print(f'Example record: {recs[0]}') \n", + " fields[(env,log)] = flds\n", + " examples[(env,log)] = recs[:2] \n", + "\n", + " facflds = flds - ignore_fields\n", + " # Fails when r[fld] is a LIST instead of singleton\n", + " # I think when that happens occasionaly, its a BUG in the data! It happens.\n", + " facets[(env,log)] = {fld: set([str(r[fld])\n", + " for r in recs if not isinstance(r[fld], list)]) \n", + " for fld in facflds}\n", + " except Exception as err:\n", + " ok = False\n", + " print(f'ERROR getting {log} from {env=} using {url=}: {err=}')\n", + " numf = len(flds) if ok else 0\n", + " numr = len(recs) if ok else 0\n", + " results[(env,log)] = dict(ok=ok, server=server, url=url,numfields=numf, numrecs=numr)\n", + "\n", + " print()\n", + " try:\n", + " recs = None\n", + " log = 'narrativelog'\n", + " #! url = f'{server}/{log}/messages?is_human=either&is_valid=true&offset=0&{limit=}'\n", + " url = f'{server}/{log}/messages?is_human=either&is_valid=either&{limit=}'\n", + " print(f'\\nAttempt to get logs from {url=}')\n", + " response = requests.get(url, timeout=timeout)\n", + " response.raise_for_status()\n", + " recs = response.json()\n", + " flds = set(recs[0].keys())\n", + " if verbose:\n", + " print(f'Number of {log} records: {len(recs):,}')\n", + " print(f'Got {log} fields: {flds}')\n", + " print(f'Example record: {recs[0]}')\n", + " fields[(env,log)] = flds \n", + " examples[(env,log)] = recs[:2] \n", + "\n", + " facflds = flds - ignore_fields\n", + " # Fails when r[fld] is a LIST instead of singleton\n", + " # I think when that happens occasionaly, its a BUG in the data! It happens.\n", + " # Look for BAD facet values like: {'None', None}\n", + " facets[(env,log)] = {fld: set([r[fld] \n", + " for r in recs if not isinstance(r[fld], list)]) \n", + " for fld in facflds}\n", + " except Exception as err:\n", + " ok = False\n", + " print(f'ERROR getting {log} from {env=} using {url=}: {err=}')\n", + " numf = len(flds) if ok else 0\n", + " numr = len(recs) if ok else 0\n", + " results[(env,log)] = dict(ok=ok, server=server, url=url,numfields=numf, numrecs=numr)" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "\n", + "## Report\n", + "This is a silly report that may be useful for developers. Not so much for astronomers." + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "\n", + "### Success/Failure table" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "show_columns = ['ok', 'server', 'numfields', 'numrecs']\n", + "df = pd.DataFrame(data=dict(results)).T.loc[:,show_columns]\n", + "print(f'Got results from {df[\"ok\"].values.sum()} of {len(df)} env/logs')\n", + "df" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "\n", + "### Field Names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "print('Field names for each Environment/Log source:')\n", + "for (env,log),flds in fields.items():\n", + " field_names = ', '.join(flds)\n", + " print(f'\\n{env}/{log}: {field_names}')\n", + "#!dict(fields)" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "\n", + "### Facets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "dict(facets)\n", + "for (env,log),flds in facets.items():\n", + " print(f'{env}/{log}:')\n", + " for fld,vals in flds.items():\n", + " print(f' {fld}: \\t{vals}')" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "\n", + "### Example Records" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "for (env,log),recs in examples.items():\n", + " print(f'\\n{env=}, {log=}: ')\n", + " print(' Example records: ')\n", + " pp(recs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}