-
Notifications
You must be signed in to change notification settings - Fork 8
Added normalization logging #312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This is to track how quickly NodeNorm normalizes identifiers, and also which identifiers are being normalized. Also removed an unnecessary catch-everything block. As per #277 (comment)
gaurav
added a commit
to TranslatorSRI/babel-validation
that referenced
this pull request
Jul 3, 2025
diff --git c/log-analysis/NodeNorm_log_analysis.ipynb i/log-analysis/NodeNorm_log_analysis.ipynb
index a59421e..a84f196 100644
--- c/log-analysis/NodeNorm_log_analysis.ipynb
+++ i/log-analysis/NodeNorm_log_analysis.ipynb
@@ -1,532 +1,614 @@
{
"cells": [
{
- "cell_type": "markdown",
- "id": "ba1f42e6-f208-4511-8117-4d92d392bd84",
"metadata": {},
+ "cell_type": "raw",
"source": [
- "# NodeNorm Log Analysis\n",
- "\n",
- "As of [PR #312](https://github.com/TranslatorSRI/NodeNormalization/pull/312), NodeNorm produces logs in the format:\n",
- "\n",
- "```\n",
- "2025-06-18T03:26:30-04:00\t2025-06-18 07:26:30,635 | INFO | normalizer:get_normalized_nodes | Normalized 1 nodes in 1.21 ms with arguments (curies=['UMLS:C0132098'], conflate_gene_protein=True, conflate_chemical_drug=True, include_descriptions=False, include_individual_types=True)\n",
- "```\n",
- "\n",
- "This Jupyter Notebook is intended to be used in analysing these logs."
- ]
- },
- {
- "cell_type": "markdown",
- "id": "bc4248bb-1c4a-446e-95a3-54acc13e01de",
- "metadata": {},
- "source": [
- "## Install prerequisites"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 14,
- "id": "721be6fa-7f14-4979-bffb-5a32cb316444",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "Requirement already satisfied: pandas in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (2.3.0)\n",
- "Requirement already satisfied: matplotlib in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (3.10.3)\n",
- "Requirement already satisfied: numpy in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (2.3.0)\n",
- "Requirement already satisfied: python-dateutil>=2.8.2 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from pandas) (2.9.0.post0)\n",
- "Requirement already satisfied: pytz>=2020.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from pandas) (2025.2)\n",
- "Requirement already satisfied: tzdata>=2022.7 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from pandas) (2025.2)\n",
- "Requirement already satisfied: contourpy>=1.0.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (1.3.2)\n",
- "Requirement already satisfied: cycler>=0.10 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (0.12.1)\n",
- "Requirement already satisfied: fonttools>=4.22.0 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (4.58.4)\n",
- "Requirement already satisfied: kiwisolver>=1.3.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (1.4.8)\n",
- "Requirement already satisfied: packaging>=20.0 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (25.0)\n",
- "Requirement already satisfied: pillow>=8 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (11.2.1)\n",
- "Requirement already satisfied: pyparsing>=2.3.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (3.2.3)\n",
- "Requirement already satisfied: six>=1.5 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\n",
- "Note: you may need to restart the kernel to use updated packages.\n"
- ]
- }
+ "{\n",
+ " \"cells\": [\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"ba1f42e6-f208-4511-8117-4d92d392bd84\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"# NodeNorm Log Analysis\\n\",\n",
+ " \"\\n\",\n",
+ " \"As of [PR #312](https://github.com/TranslatorSRI/NodeNormalization/pull/312), NodeNorm produces logs in the format:\\n\",\n",
+ " \"\\n\",\n",
+ " \"```\\n\",\n",
+ " \"2025-06-18T03:26:30-04:00\\t2025-06-18 07:26:30,635 | INFO | normalizer:get_normalized_nodes | Normalized 1 nodes in 1.21 ms with arguments (curies=['UMLS:C0132098'], conflate_gene_protein=True, conflate_chemical_drug=True, include_descriptions=False, include_individual_types=True)\\n\",\n",
+ " \"```\\n\",\n",
+ " \"\\n\",\n",
+ " \"This Jupyter Notebook is intended to be used in analysing these logs.\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"bc4248bb-1c4a-446e-95a3-54acc13e01de\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"## Install prerequisites\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 14,\n",
+ " \"id\": \"721be6fa-7f14-4979-bffb-5a32cb316444\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"name\": \"stdout\",\n",
+ " \"output_type\": \"stream\",\n",
+ " \"text\": [\n",
+ " \"Requirement already satisfied: pandas in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (2.3.0)\\n\",\n",
+ " \"Requirement already satisfied: matplotlib in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (3.10.3)\\n\",\n",
+ " \"Requirement already satisfied: numpy in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (2.3.0)\\n\",\n",
+ " \"Requirement already satisfied: python-dateutil>=2.8.2 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from pandas) (2.9.0.post0)\\n\",\n",
+ " \"Requirement already satisfied: pytz>=2020.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from pandas) (2025.2)\\n\",\n",
+ " \"Requirement already satisfied: tzdata>=2022.7 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from pandas) (2025.2)\\n\",\n",
+ " \"Requirement already satisfied: contourpy>=1.0.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (1.3.2)\\n\",\n",
+ " \"Requirement already satisfied: cycler>=0.10 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (0.12.1)\\n\",\n",
+ " \"Requirement already satisfied: fonttools>=4.22.0 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (4.58.4)\\n\",\n",
+ " \"Requirement already satisfied: kiwisolver>=1.3.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (1.4.8)\\n\",\n",
+ " \"Requirement already satisfied: packaging>=20.0 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (25.0)\\n\",\n",
+ " \"Requirement already satisfied: pillow>=8 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (11.2.1)\\n\",\n",
+ " \"Requirement already satisfied: pyparsing>=2.3.1 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from matplotlib) (3.2.3)\\n\",\n",
+ " \"Requirement already satisfied: six>=1.5 in /opt/homebrew/Cellar/jupyterlab/4.4.3_2/libexec/lib/python3.13/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\\n\",\n",
+ " \"Note: you may need to restart the kernel to use updated packages.\\n\"\n",
+ " ]\n",
+ " }\n",
+ " ],\n",
+ " \"source\": [\n",
+ " \"%pip install pandas matplotlib numpy\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"3a6bab9f-897e-4c96-84c8-3e402676e753\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"## Loading files\\n\",\n",
+ " \"\\n\",\n",
+ " \"These files can be checked into the repository into the `logs/` subdirectory.\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 3,\n",
+ " \"id\": \"c6e9048c-6f68-4d67-b60f-f6e8fa13f4ea\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"logfile = \\\"logs/nodenorm-renci-logs-2025jun18.txt\\\"\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"67ca8f70-adaa-4883-ac51-1c0ec235bd13\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"We can use Python dataclasses to load the important information from the logfile.\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 4,\n",
+ " \"id\": \"42805620-22f8-4469-845a-a5fd40ae7a3d\",\n",
+ " \"metadata\": {\n",
+ " \"scrolled\": true\n",
+ " },\n",
+ " \"outputs\": [],\n",
+ " \"source\": [\n",
+ " \"from dataclasses import dataclass, field\\n\",\n",
+ " \"from datetime import datetime\\n\",\n",
+ " \"import logging\\n\",\n",
+ " \"import re\\n\",\n",
+ " \"import ast\\n\",\n",
+ " \"\\n\",\n",
+ " \"logging.basicConfig(level=logging.INFO)\\n\",\n",
+ " \"\\n\",\n",
+ " \"@dataclass\\n\",\n",
+ " \"class LogEntry:\\n\",\n",
+ " \" time: datetime\\n\",\n",
+ " \" curies: list[str]\\n\",\n",
+ " \" curie_count: int\\n\",\n",
+ " \" time_taken_ms: float\\n\",\n",
+ " \" time_taken_per_curie_ms: float\\n\",\n",
+ " \" arguments: dict[str, str]\\n\",\n",
+ " \" node: str = \\\"\\\"\\n\",\n",
+ " \"\\n\",\n",
+ " \"def convert_log_line_into_entry(line: str) -> LogEntry: \\n\",\n",
+ " \" # Depending on where the log file comes from, it might start with one of two types of timestamps:\\n\",\n",
+ " \" # - ISO 8601 date (e.g. \\\"2007-04-05T12:30−02:00\\\"), which will be separated from the rest of the log line with a tab character.\\n\",\n",
+ " \" # - Python log format date (e.g. \\\"2025-06-12 13:01:49,319\\\"), which should always be in UTC.\\n\",\n",
+ " \"\\n\",\n",
+ " \" # Entry variables.\\n\",\n",
+ " \" log_time = None\\n\",\n",
+ " \" curies = []\\n\",\n",
+ " \" curie_count = -1\\n\",\n",
+ " \" time_taken_ms = -1.0\\n\",\n",
+ " \" arguments = {}\\n\",\n",
+ " \"\\n\",\n",
+ " \" # Parse the datetime stamp.\\n\",\n",
+ " \" iso8601date_match = re.match(r'^(\\\\d{4}-\\\\d{2}-\\\\d{2}(?:[T ]\\\\d{2}:\\\\d{2}(?::\\\\d{2}(?:\\\\.\\\\d+)?(?:Z|[+-]\\\\d{2}:\\\\d{2})?)?)?)\\\\t', line)\\n\",\n",
+ " \" if iso8601date_match:\\n\",\n",
+ " \" log_time = datetime.fromisoformat(iso8601date_match.group(1))\\n\",\n",
+ " \" else:\\n\",\n",
+ " \" # TODO raise exception\\n\",\n",
+ " \" logging.error(f\\\"Could not identify the datetime for the line: {line}\\\")\\n\",\n",
+ " \"\\n\",\n",
+ " \" # Parse the log text.\\n\",\n",
+ " \" log_text_match = re.search(r'\\\\| INFO \\\\| normalizer:get_normalized_nodes \\\\| Normalized (\\\\d+) nodes in ([\\\\d\\\\.]+) ms with arguments \\\\((.*)\\\\)', line)\\n\",\n",
+ " \" if not log_text_match:\\n\",\n",
+ " \" raise ValueError(f\\\"Could not find NodeNorm log-line: {line}\\\")\\n\",\n",
+ " \" curie_count = int(log_text_match.group(1))\\n\",\n",
+ " \" time_taken_ms = float(log_text_match.group(2))\\n\",\n",
+ " \" argument_text = log_text_match.group(3)\\n\",\n",
+ " \"\\n\",\n",
+ " \" # To parse the argument_text, we can turn it into a function call and use Python's ast module to parse it.\\n\",\n",
+ " \" argument_fn_call = f'arguments({argument_text})'\\n\",\n",
+ " \" tree = ast.parse(argument_fn_call, mode=\\\"eval\\\")\\n\",\n",
+ " \" call_node = tree.body\\n\",\n",
+ " \" for kw in call_node.keywords:\\n\",\n",
+ " \" arguments[kw.arg] = ast.literal_eval(kw.value)\\n\",\n",
+ " \"\\n\",\n",
+ " \" # Some assertions.\\n\",\n",
+ " \" if 'curies' not in arguments:\\n\",\n",
+ " \" raise ValueError(f'No CURIEs found in arguments {argument_text} on line {line}, which was parsed into: {arguments}')\\n\",\n",
+ " \" curies = arguments['curies']\\n\",\n",
+ " \" if len(curies) != curie_count:\\n\",\n",
+ " \" raise ValueError(f'Found {len(curies)} CURIEs in arguments but expected {curie_count} CURIEs: {curies}')\\n\",\n",
+ " \" if len(curies) < 1:\\n\",\n",
+ " \" raise ValueError(f'Found no CURIEs in line: {line}')\\n\",\n",
+ " \" \\n\",\n",
+ " \" # Emit the LogEntry.\\n\",\n",
+ " \" return LogEntry(\\n\",\n",
+ " \" time=log_time,\\n\",\n",
+ " \" curies=curies,\\n\",\n",
+ " \" curie_count=curie_count,\\n\",\n",
+ " \" time_taken_ms=time_taken_ms,\\n\",\n",
+ " \" time_taken_per_curie_ms=time_taken_ms/curie_count,\\n\",\n",
+ " \" arguments=arguments\\n\",\n",
+ " \" )\\n\",\n",
+ " \"\\n\",\n",
+ " \"logs = []\\n\",\n",
+ " \"with open(logfile, 'r') as logf:\\n\",\n",
+ " \" for line in logf:\\n\",\n",
+ " \" # We're only interested in log-lines -- these will all contain `normalizer:get_normalized_nodes`\\n\",\n",
+ " \" if \\\"normalizer:get_normalized_nodes\\\" not in line:\\n\",\n",
+ " \" continue\\n\",\n",
+ " \" \\n\",\n",
+ " \" logs.append(convert_log_line_into_entry(line))\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 5,\n",
+ " \"id\": \"227a6bd5-1ca5-4c5b-8cde-4821b7efa7cc\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"data\": {\n",
+ " \"text/plain\": [\n",
+ " \"[LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['PMID:17553787'], curie_count=1, time_taken_ms=2.1, time_taken_per_curie_ms=2.1, arguments={'curies': ['PMID:17553787'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['GO:0008285'], curie_count=1, time_taken_ms=1.5, time_taken_per_curie_ms=1.5, arguments={'curies': ['GO:0008285'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['PMID:16943770'], curie_count=1, time_taken_ms=3.21, time_taken_per_curie_ms=3.21, arguments={'curies': ['PMID:16943770'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['PMID:17553787'], curie_count=1, time_taken_ms=1.97, time_taken_per_curie_ms=1.97, arguments={'curies': ['PMID:17553787'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['KEGG:hsa05200'], curie_count=1, time_taken_ms=2.13, time_taken_per_curie_ms=2.13, arguments={'curies': ['KEGG:hsa05200'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['GO:0031670'], curie_count=1, time_taken_ms=3.58, time_taken_per_curie_ms=3.58, arguments={'curies': ['GO:0031670'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['GO:0050680'], curie_count=1, time_taken_ms=3.47, time_taken_per_curie_ms=3.47, arguments={'curies': ['GO:0050680'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['GO:0070316'], curie_count=1, time_taken_ms=4.5, time_taken_per_curie_ms=4.5, arguments={'curies': ['GO:0070316'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['PMID:10812241'], curie_count=1, time_taken_ms=2.85, time_taken_per_curie_ms=2.85, arguments={'curies': ['PMID:10812241'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node=''),\\n\",\n",
+ " \" LogEntry(time=datetime.datetime(2025, 6, 18, 14, 23, 48, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=72000))), curies=['KEGG:hsa04110'], curie_count=1, time_taken_ms=2.26, time_taken_per_curie_ms=2.26, arguments={'curies': ['KEGG:hsa04110'], 'conflate_gene_protein': True, 'conflate_chemical_drug': False, 'include_descriptions': False, 'include_individual_types': False}, node='')]\"\n",
+ " ]\n",
+ " },\n",
+ " \"execution_count\": 5,\n",
+ " \"metadata\": {},\n",
+ " \"output_type\": \"execute_result\"\n",
+ " }\n",
+ " ],\n",
+ " \"execution_count\": 50\n",
+ " },\n",
+ " {\n",
+ " \"metadata\": {},\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"source\": \"# Some overall measures\",\n",
+ " \"id\": \"a13af441dd8d87d\"\n",
+ " },\n",
+ " {\n",
+ " \"metadata\": {},\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"source\": \"\",\n",
+ " \"id\": \"2ee4b13bab99da17\"\n",
+ " },\n",
+ " {\n",
+ " \"metadata\": {\n",
+ " \"ExecuteTime\": {\n",
+ " \"end_time\": \"2025-07-03T14:54:04.252739Z\",\n",
+ " \"start_time\": \"2025-07-03T14:54:04.246303Z\"\n",
+ " }\n",
+ " },\n",
+ " \"cell_type\": \"code\",\n",
+ " \"source\": [\n",
+ " \"times = sorted(list(set(map(lambda x: x.time, logs))))\\n\",\n",
+ " \"count_requests = len(logs)\\n\",\n",
+ " \"\\n\",\n",
+ " \"print(f\\\"Time range: {times[0]} to {times[-1]} ({times[-1] - times[0]})\\\")\\n\",\n",
+ " \"print(f\\\"Total number of requests: {count_requests}\\\")\\n\",\n",
+ " \"print(f\\\"Total number of CURIEs: {sum(map(lambda x: x.curie_count, logs))}\\\")\\n\",\n",
+ " \"print(f\\\"Total time taken: {sum(map(lambda x: x.time_taken_ms, logs))} ms\\\")\\n\",\n",
+ " \"print(f\\\"Average time per CURIE: {sum(map(lambda x: x.time_taken_per_curie_ms, logs))/count_requests} ms\\\")\\n\",\n",
+ " \"print(f\\\"Average throughput: {len(logs) / sum(map(lambda x: x.time_taken_ms, logs))} nodes/sec\\\")\\n\",\n",
+ " \"#print(f\\\"Average throughput per CURIE: {len(logs) / sum(map(lambda x: x.time_taken_ms, logs))} nodes/sec per CURIE\\\")\\n\",\n",
+ " \"#print(f\\\"Total number of unique CURIEs: {len(set(sum(map(lambda x: x.curies, logs), [])))}\\\")\"\n",
+ " ],\n",
+ " \"id\": \"702b88dac738feb0\",\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"name\": \"stdout\",\n",
+ " \"output_type\": \"stream\",\n",
+ " \"text\": [\n",
+ " \"Time range: 2025-06-30 15:19:44.142000 to 2025-07-03 14:01:04.186000 (2 days, 22:41:20.044000)\\n\",\n",
+ " \"Total number of requests: 9992\\n\",\n",
+ " \"Total number of CURIEs: 1300164\\n\",\n",
+ " \"Total time taken: 4278872.9 ms\\n\",\n",
+ " \"Average time per CURIE: 5.692698317139622 ms\\n\",\n",
+ " \"Average throughput: 0.0023351943919624253 nodes/sec\\n\"\n",
+ " ]\n",
+ " }\n",
+ " ],\n",
+ " \"execution_count\": 55\n",
+ " \"logs[0:10]\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"dfc3b8e7-be80-44a2-b142-943c0c3c2dbb\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": [\n",
+ " \"## Visualizing the logs\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"markdown\",\n",
+ " \"id\": \"9650b40f-4ddf-4157-84c3-cb8dd9466491\",\n",
+ " \"metadata\": {},\n",
+ " \"source\": []\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 15,\n",
+ " \"id\": \"7a52c4d7-21da-42f5-94cc-e5957ec9bcb6\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"data\": {\n",
+ " \"text/html\": [\n",
+ " \"<div>\\n\",\n",
+ " \"<style scoped>\\n\",\n",
+ " \" .dataframe tbody tr th:only-of-type {\\n\",\n",
+ " \" vertical-align: middle;\\n\",\n",
+ " \" }\\n\",\n",
+ " \"\\n\",\n",
+ " \" .dataframe tbody tr th {\\n\",\n",
+ " \" vertical-align: top;\\n\",\n",
+ " \" }\\n\",\n",
+ " \"\\n\",\n",
+ " \" .dataframe thead th {\\n\",\n",
+ " \" text-align: right;\\n\",\n",
+ " \" }\\n\",\n",
+ " \"</style>\\n\",\n",
+ " \"<table border=\\\"1\\\" class=\\\"dataframe\\\">\\n\",\n",
+ " \" <thead>\\n\",\n",
+ " \" <tr style=\\\"text-align: right;\\\">\\n\",\n",
+ " \" <th></th>\\n\",\n",
+ " \" <th>time</th>\\n\",\n",
+ " \" <th>curies</th>\\n\",\n",
+ " \" <th>curie_count</th>\\n\",\n",
+ " \" <th>time_taken_ms</th>\\n\",\n",
+ " \" <th>time_taken_per_curie_ms</th>\\n\",\n",
+ " \" <th>arguments</th>\\n\",\n",
+ " \" <th>node</th>\\n\",\n",
+ " \" <th>throughput_cps</th>\\n\",\n",
+ " \" </tr>\\n\",\n",
+ " \" </thead>\\n\",\n",
+ " \" <tbody>\\n\",\n",
+ " \" <tr>\\n\",\n",
+ " \" <th>0</th>\\n\",\n",
+ " \" <td>2025-06-18 14:23:48-04:00</td>\\n\",\n",
+ " \" <td>[PMID:17553787]</td>\\n\",\n",
+ " \" <td>1</td>\\n\",\n",
+ " \" <td>2.10</td>\\n\",\n",
+ " \" <td>2.10</td>\\n\",\n",
+ " \" <td>{'curies': ['PMID:17553787'], 'conflate_gene_p...</td>\\n\",\n",
+ " \" <td></td>\\n\",\n",
+ " \" <td>476.190476</td>\\n\",\n",
+ " \" </tr>\\n\",\n",
+ " \" <tr>\\n\",\n",
+ " \" <th>1</th>\\n\",\n",
+ " \" <td>2025-06-18 14:23:48-04:00</td>\\n\",\n",
+ " \" <td>[GO:0008285]</td>\\n\",\n",
+ " \" <td>1</td>\\n\",\n",
+ " \" <td>1.50</td>\\n\",\n",
+ " \" <td>1.50</td>\\n\",\n",
+ " \" <td>{'curies': ['GO:0008285'], 'conflate_gene_prot...</td>\\n\",\n",
+ " \" <td></td>\\n\",\n",
+ " \" <td>666.666667</td>\\n\",\n",
+ " \" </tr>\\n\",\n",
+ " \" <tr>\\n\",\n",
+ " \" <th>2</th>\\n\",\n",
+ " \" <td>2025-06-18 14:23:48-04:00</td>\\n\",\n",
+ " \" <td>[PMID:16943770]</td>\\n\",\n",
+ " \" <td>1</td>\\n\",\n",
+ " \" <td>3.21</td>\\n\",\n",
+ " \" <td>3.21</td>\\n\",\n",
+ " \" <td>{'curies': ['PMID:16943770'], 'conflate_gene_p...</td>\\n\",\n",
+ " \" <td></td>\\n\",\n",
+ " \" <td>311.526480</td>\\n\",\n",
+ " \" </tr>\\n\",\n",
+ " \" <tr>\\n\",\n",
+ " \" <th>3</th>\\n\",\n",
+ " \" <td>2025-06-18 14:23:48-04:00</td>\\n\",\n",
+ " \" <td>[PMID:17553787]</td>\\n\",\n",
+ " \" <td>1</td>\\n\",\n",
+ " \" <td>1.97</td>\\n\",\n",
+ " \" <td>1.97</td>\\n\",\n",
+ " \" <td>{'curies': ['PMID:17553787'], 'conflate_gene_p...</td>\\n\",\n",
+ " \" <td></td>\\n\",\n",
+ " \" <td>507.614213</td>\\n\",\n",
+ " \" </tr>\\n\",\n",
+ " \" <tr>\\n\",\n",
+ " \" <th>4</th>\\n\",\n",
+ " \" <td>2025-06-18 14:23:48-04:00</td>\\n\",\n",
+ " \" <td>[KEGG:hsa05200]</td>\\n\",\n",
+ " \" <td>1</td>\\n\",\n",
+ " \" <td>2.13</td>\\n\",\n",
+ " \" <td>2.13</td>\\n\",\n",
+ " \" <td>{'curies': ['KEGG:hsa05200'], 'conflate_gene_p...</td>\\n\",\n",
+ " \" <td></td>\\n\",\n",
+ " \" <td>469.483568</td>\\n\",\n",
+ " \" </tr>\\n\",\n",
+ " \" </tbody>\\n\",\n",
+ " \"</table>\\n\",\n",
+ " \"</div>\"\n",
+ " ],\n",
+ " \"text/plain\": [\n",
+ " \" time curies curie_count time_taken_ms \\\\\\n\",\n",
+ " \"0 2025-06-18 14:23:48-04:00 [PMID:17553787] 1 2.10 \\n\",\n",
+ " \"1 2025-06-18 14:23:48-04:00 [GO:0008285] 1 1.50 \\n\",\n",
+ " \"2 2025-06-18 14:23:48-04:00 [PMID:16943770] 1 3.21 \\n\",\n",
+ " \"3 2025-06-18 14:23:48-04:00 [PMID:17553787] 1 1.97 \\n\",\n",
+ " \"4 2025-06-18 14:23:48-04:00 [KEGG:hsa05200] 1 2.13 \\n\",\n",
+ " \"\\n\",\n",
+ " \" time_taken_per_curie_ms arguments \\\\\\n\",\n",
+ " \"0 2.10 {'curies': ['PMID:17553787'], 'conflate_gene_p... \\n\",\n",
+ " \"1 1.50 {'curies': ['GO:0008285'], 'conflate_gene_prot... \\n\",\n",
+ " \"2 3.21 {'curies': ['PMID:16943770'], 'conflate_gene_p... \\n\",\n",
+ " \"3 1.97 {'curies': ['PMID:17553787'], 'conflate_gene_p... \\n\",\n",
+ " \"4 2.13 {'curies': ['KEGG:hsa05200'], 'conflate_gene_p... \\n\",\n",
+ " \"\\n\",\n",
+ " \" node throughput_cps \\n\",\n",
+ " \"0 476.190476 \\n\",\n",
+ " \"1 666.666667 \\n\",\n",
+ " \"2 311.526480 \\n\",\n",
+ " \"3 507.614213 \\n\",\n",
+ " \"4 469.483568 \"\n",
+ " ]\n",
+ " },\n",
+ " \"execution_count\": 15,\n",
+ " \"metadata\": {},\n",
+ " \"output_type\": \"execute_result\"\n",
+ " }\n",
+ " ],\n",
+ " \"source\": [\n",
+ " \"import pandas as pd\\n\",\n",
+ " \"import numpy as np\\n\",\n",
+ " \"import matplotlib.pyplot as plt\\n\",\n",
+ " \"from dataclasses import asdict\\n\",\n",
+ " \"\\n\",\n",
+ " \"# Assume `records` is your list of dataclass instances\\n\",\n",
+ " \"# Convert to DataFrame\\n\",\n",
+ " \"df = pd.DataFrame([asdict(r) for r in logs])\\n\",\n",
+ " \"df['time'] = pd.to_datetime(df['time'])\\n\",\n",
+ " \"df['throughput_cps'] = df['curie_count'] / df['time_taken_ms'] * 1000\\n\",\n",
+ " \"\\n\",\n",
+ " \"df.head()\\n\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 10,\n",
+ " \"id\": \"3f0f62a4-fe2f-4e9c-8236-6e93785e1588\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"data\": {\n",
+ " \"image/png\": \"iVBORw0KGgoAAAANSUhEUgAAA90AAAJOCAYAAACqS2TfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAVgZJREFUeJzt3QeYlNX5N+CDohQFLBgFIWIBwQ5oLAR7iRpLjF0iGjXF3pLYa+wlGpMYo0lIgrHEQhKT2BJUrFGwoSBYsCtWFgQb7Hc95/vPZnddYFf2ZXZ27/u6ht15d8qZmXeG+b3nnOe0q66urk4AAABAs1uk+W8SAAAACEI3AAAAFEToBgAAgIII3QAAAFAQoRsAAAAKInQDAABAQYRuAAAAKIjQDQAAAAURugEAAKAgQjdAAc4444zUrl27hXJfm2++eT6V3HPPPfm+b7rppoVy/wcccEDq06dPaslmzJiRDj744LTCCivk5+boo49eoNubMmVKvp0RI0Y0Wxtp2Uqv+cUXX5zamtjP47E/9thjhd9X3E98fgK0JkI3QCO/cJZOHTt2TD179kzbbbdd+vnPf56mT5/eLPfzxhtv5C+bTzzxRGppWnLbGuPcc8/Nr+MPf/jD9Kc//Sl95zvfmeuBkvmdah/g4Itmz56dfv/73+fnaZlllkkdOnTIB2UOPPDAOqGt9Hy/++67Dd7OWmutVee5LoXe0mmRRRbJt7/99tunhx566AvXb+j24wDR3F7XeF+3xM+bOH3lK19JW2yxRfrXv/61QO+BUaNGpXK5//7782u14oor5uf6q1/9atppp53Sn//857K1CWBhab/Q7gmgwp111llp5ZVXTp999ll66623co9y9Jheeuml6W9/+1taZ511ai57yimnpBNOOKHJwfbMM8/MAWW99dZr9PXuvPPOVLR5te3qq69Oc+bMSS3Zf/7zn7TRRhul008/fa6X2W233dJqq61Wp3c8Qvq3vvWt/LeS5ZdfPq200kpp1qxZabHFFiu87ZUknpN4rm6//fa06aabppNOOikH4wjMN954Y/rDH/6QXnnlldSrV68vfR/77LNP2mGHHXK4nzRpUvrVr36VA+mjjz6a1l577flePw4CXHPNNV/Yvuiii6aW+HlTXV2d3n777RzG43H//e9/T9/85je/VOjefffd06677poWtr/85S9pr732yp8dRx11VFp66aXTSy+9lO677778+bHvvvvW2Yfat/f1FGhdfKoBNFL00qy//vo150888cQc5uIL8M4775wmTJiQOnXqlP8WXxqL/uI4c+bM1Llz57T44ouncqqE4Dl16tS0xhprzPMycdCk9oGT6CGN0B3bhg0b9oXLt5Se0YXp888/zwdY5rbP/ehHP8qB+2c/+9kXhvDHAY/YvqAGDRpU5/UYOnRofm9eeeWVOYDPT7wvG3o9W/rnzUEHHZQP+Fx33XVfKnSXU4w6iPffww8//IV9J96bbf19BbR+hpcDLIAtt9wynXrqqenll19OI0eOnOec7rvuuit9/etfT0sttVRacskl0+qrr557AkP0mm+wwQb59xiGWxpWWpozHMNsY7jt2LFjcw9ihO3SdevP6S6JnsC4TMxjXmKJJfKBgVdffbXOZaLnOobc1lf7NufXtobmdH/00UfpuOOOS7179849i/FYYy5s9NrVFrdz+OGH52Gv8fjismuuuWYObo0RX9hLYSS+rK+77rq5N7X+/PboVfvHP/5R0/boeW3uOd3xPMTrGj25EYri9xhK+8tf/jL//emnn877S7wW0VPe0LDaDz/8MIfV0vMWPe8XXHBBo0YSxGsQ9xsjH6JHMZ6PCDq33HLLl7qf2nOYL7vssrTqqqvmyz777LMN3v9rr72WrrrqqrTNNts0OGc+epKPP/74BerlbkiE7vDCCy+khSUOHsRrGAfZNttsszR+/Piav8XQ+njeHn/88QZ7m+N5eP3115t8n/G5EfdX/2BevD6bbLJJWnbZZfPfBw8e/IV6DtGeeE/Ge6P0Hqj9vo/2xPsops3Eaxw97HHA6dNPP61zO5988kk69thj03LLLZf34xgF8s4778y37fHaxGdIQwdrYuj83OZ0159SUP9U2yOPPJK+8Y1vpG7duuXPx3hdHnjggfm2DWBh0NMNsIBifnCE2wg7hxxySIOXeeaZZ3Igil7TGDYaX2yff/75mi+FAwYMyNtPO+209L3vfa8mSMSX6ZL33nsv937tvffeuacugua8nHPOOfmL6U9+8pMcTiM4bb311nledqlHvjEa07baIlhHwB89enT+Ih8B8I477si9oPHlvn5vZ8z1jGB46KGHpi5duuR58t/+9rdzeI0gMTcxDDUODMTzGME9gkIMY40wEaEyhrFG22MO9zHHHJPDXhwICBEaihAHOuI1igMjF154Ybr22mtz2yKgnHzyyWm//fbLw69//etfp/333z9tvPHGud2lkQsRFOI5+v73v5/nvD744IN5RMWbb76ZX7/5mTx5ch7G+4Mf/CANHz48B8A99tgjH8SIMPxl7idu4+OPP86vfey3MVy8ITHfOHrCG5ovX6TSAZQYstxYDc0jj0DYtWvX+V73j3/8Y67jcNhhh+Xn5fLLL88HU+KgSrwnYwh3/C1e+4EDB9a5bmyLfTYOxszPtGnTcjvj/RTv3yuuuCJPeajfSx/3H++32LciJF9//fX5Nb/tttvSjjvumC8T74EoJPi1r30tv44hDqKUpo7E9njPxN/69++f940I7rGv1A7KRxxxRH6eY9RCPO+xr8T+fcMNN8zzscQBin//+9/5wExTDrrE+zTaXltM74n3c+12xYijeN/FAYdoW8z3j/02XpcxY8bkxwdQVtUAzNPvf//76J6tfvTRR+d6mW7dulUPHDiw5vzpp5+er1Pys5/9LJ9/55135nobcftxmbi/+jbbbLP8t1//+tcN/i1OJaNHj86XXXHFFaurqqpqtt944415++WXX16zbaWVVqoePnz4fG9zXm2L68ftlIwaNSpf9qc//Wmdy+2+++7V7dq1q37++edrtsXlFl988Trbnnzyybz9iiuuqJ6Xyy67LF9u5MiRNds+/fTT6o033rh6ySWXrPPYo3077rhjdVPEaxW3H69lfS+99NIXno94HmLbueeeW7Ptgw8+qO7UqVN+3Ndff33N9okTJ37hts8+++zqJZZYonrSpEl17uuEE06oXnTRRatfeeWVebY3HmPc5s0331yzbdq0adU9evSos2829n5Kj7Fr167VU6dOne/zdcwxx+TLP/7449WNUXqPzO09seaaa9bZB0vtOfPMM/N13nrrreoxY8ZUb7DBBnn7X/7yl/nefuk1aui03XbbzbO9pfuP1/O1116r2f7II4/k7fH4S/bZZ5/qnj17Vs+ePbtm27hx4+b6Hmro86b+qUOHDtUjRoz4wuVnzpxZ53y8B9Zaa63qLbfcss72eM0beq/vv//+1YssskiDn29z5syp06att966ZluIxxz7zIcffjjPx/Tb3/625r2+xRZbVJ966qn5tav9/JTM7T1Xcuihh+b7/M9//lPTxr59++bXr3bb4nlZeeWV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NwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIK0L+qGaX1mz56dxowZk958883Uo0ePNHTo0LTooouWu1kAAAAtlp5uGuWWW25Jq622Wtpiiy3Svvvum3/G+dgOAABAw4Ru5iuC9e67757WXnvt9NBDD6Xp06fnn3E+tgveAAAADWtXXV1dnSpUVVVV6tatW5o2bVrq2rVruZvTaoeUR492BOxRo0alRRb533GaOXPmpF133TWNHz8+TZ482VBzAACgzahqZB7V0808xRzuKVOmpJNOOqlO4A5x/sQTT0wvvfRSvhwAAAB1Cd3MUxRNC2uttVaDfy9tL10OAACA/xG6maeoUh5iCHlDSttLlwMAAOB/hG7mKZYF69OnTzr33HPzHO7a4vx5552XVl555Xw5AAAA6hK6macojnbJJZek2267LRdNq129PM7H9osvvlgRNQAAgAa0b2gj1Lbbbrulm266KR133HFpk002qdkePdyxPf4OAADAF1kyjCYtHxZVyqNoWszhjiHlergBAIC2qKqReVRPN40WAXvzzTcvdzMAAAAqhjndAAAAUBChGwAAAAoidAMAAEBBhG4AAAAoiNANAAAABRG6AQAAoCBCNwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIII3QAAAFAQoRsAAAAKInQDAABAQYRuAAAAKIjQDQAAAAURugEAAKAgQjcAAAAUROgGAACAggjdAAAAUBChGwAAAAoidAMAAEBBhG4AAAAoiNANAAAABRG6AQAAoCBCNwAAABRE6AYAAICCCN0AAABQEKEbAAAAWmPoPuOMM1K7du3qnPr371/OJgEAAECzaZ/KbM0110x33313zfn27cveJAAAAGgWZU+4EbJXWGGFcjcDAAAAWt+c7smTJ6eePXumVVZZJe23337plVdemetlP/nkk1RVVVXnBAAAAC1VWUP3hhtumEaMGJFuv/32dOWVV6aXXnopDR06NE2fPr3By5933nmpW7duNafevXsv9DYDAABAY7Wrrq6uTi3Ehx9+mFZaaaV06aWXpoMOOqjBnu44lURPdwTvadOmpa5duy7k1gIAANBWVVVV5c7g+eXRss/prm2ppZZK/fr1S88//3yDf+/QoUM+AQAAQCUo+5zu2mbMmJFeeOGF1KNHj3I3BQAAACo7dB9//PHp3nvvTVOmTEkPPvhg+ta3vpUWXXTRtM8++5SzWQAAANAsyjq8/LXXXssB+7333kvLLbdc+vrXv54efvjh/DsAAABUurKG7uuvv76cdw8AAABtZ043AAAAtCZCNwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIII3QAAAFAQoRsAAAAKInQDAABAQYRuAAAAKIjQDQAAAAURugEAAKAgQjcAAAAUROgGAACAggjdAAAAUBChGwAAAAoidAMAAEBBhG4AAAAoiNANAAAABRG6AQAAoCBCNwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIII3QAAAFAQoRsAAAAKInQDAABAQYRuAAAAKIjQDQAAAAURugEAAKAgQjcAAAAUpH1RN0zrM3v27DRmzJj05ptvph49eqShQ4emRRddtNzNAgAAaLH0dNMot9xyS1pttdXSFltskfbdd9/8M87HdgAAABomdDNfEax33333tPbaa6eHHnooTZ8+Pf+M87Fd8AYAAGhYu+rq6upUoaqqqlK3bt3StGnTUteuXcvdnFY7pDx6tCNg33zzzemBBx6oGV4+ZMiQ9O1vfzuNHz8+TZ482VBzAACgzahqZB7V0808xRzuKVOmpE022ST169evzvDyOL/xxhunl156KV8OAACAuoRu5il6tcNJJ53U4PDyk08+uc7lAAAA+B/Vy5mnr3zlK/lnDCUfNWpUWmSR/3+cZqONNsrnN9tss3T//ffXXA4AAID/0dPNAqngkgAAAACFE7qZp6lTp+af0Zu966671hleHuejsFrtywEAAPA/QjfzFFXKw3nnnZeefvrpXFAtKvPFz6hafu6559a5HAAAAP9jTjfzNHTo0NSnT5/04IMPpkmTJjW4ZNjKK6+cLwcAAEBderqZp1h7+5JLLkm33XZbDtgdOnRI3/zmN/PPOB/bL774Ymt0AwAANEBPN/O12267pZtuuikdd9xxeVh5SfRwx/b4OwAAAF/UrrqCy09XVVWlbt26pWnTpuV5xhRr9uzZacyYMTXDy2NIuR5uAACgLapqZB7V002jRcDefPPNy90MAACAimFONwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIII3QAAAFCQ9kXdMOUzc+bMNHHixEJue9asWWnKlCmpT58+qVOnTqko/fv3T507dy7s9gEAABYGobsVisA9ePDgVMnGjh2bBg0aVO5mAAAALBChuxWKXuIIrUWYMGFCGjZsWBo5cmQaMGBAKvIxAAAAVDqhuxWKYdlF9xJH4NYTDQAAUCGF1M4///zUrl27dPTRR5e7KQAAANB6Qvejjz6arrrqqrTOOuuUuykAAADQekL3jBkz0n777ZeuvvrqtPTSS5e7OQAAANB6Qvdhhx2Wdtxxx7T11lvP97KffPJJqqqqqnMCAACAlqqshdSuv/76NG7cuDy8vDHOO++8dOaZZxbeLgAAAKjonu5XX301HXXUUenaa69NHTt2bNR1TjzxxDRt2rSaU9wGAAAAtFRl6+mOdaSnTp1aZ9mp2bNnp/vuuy/94he/yEPJF1100TrX6dChQz4BAABAJShb6N5qq63S008/XWfbgQcemPr3759+8pOffCFwAwAAQKUpW+ju0qVLWmuttepsW2KJJdKyyy77he0AAABQicpevRwAAABaq7JWL6/vnnvuKXcTAAAAoNno6QYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIII3QAAAFAQoRsAAAAKInQDAABAQdo35cIffvhhuvXWW9OYMWPSyy+/nGbOnJmWW265NHDgwLTddtulTTbZpKh2AgAAQOvs6X7jjTfSwQcfnHr06JF++tOfplmzZqX11lsvbbXVVqlXr15p9OjRaZtttklrrLFGuuGGG4pvNQAAALSWnu7oyR4+fHgaO3ZsDtYNiSA+atSodNlll6VXX301HX/88c3dVgAAAGh9ofvZZ59Nyy677Dwv06lTp7TPPvvk03vvvddc7QMAAIDWPbx8foF7QS8PAAAArVGTq5f/4Q9/SP/4xz9qzv/4xz9OSy21VC6iFsXVAAAAgC8Zus8999w8lDw89NBD6Ze//GW68MILU/fu3dMxxxxTRBsBAACg9S8ZFqJI2mqrrZZ/j8Jp3/72t9P3vve9NGTIkLT55psX0UYAAABoGz3dSy65ZE2htDvvvDMvFRY6duyYK5gDAAAAX7KnO0J2rNkdy4hNmjQp7bDDDnn7M888k/r06dPUmwMAAIBWq8k93TGHe+ONN07vvPNOuvnmm2sqlcca3rFcGAAAAPAle7qjUvkvfvGLL2w/88wzm3pTAAAA0Ko1OXSHjz/+OD311FNp6tSpac6cOTXb27Vrl3baaafmbB8AAAC0ndB9++23p+985zs1xdRqi9A9e/bs5mobAAAAtK053UcccUTac88905tvvpl7uWufBG4AAABYgND99ttvp2OPPTYtv/zyTb0qAAAAtClNDt277757uueee4ppDQAAALTlOd1RuXyPPfZIY8aMSWuvvXZabLHF6vz9yCOPbM72AQAAQNsJ3dddd1268847U8eOHXOPdxRPK4nfhW4AAAD4kqH75JNPzmtyn3DCCWmRRZo8Oh0AAADajCan5k8//TTttddeAjcAAADMR5OT8/Dhw9MNN9zQ1KsBAABAm9Pk4eWxFveFF16Y7rjjjrTOOut8oZDapZde2pztAwAAgLYTup9++uk0cODA/Pv48ePr/K12UTUAAABo65ocukePHl1MSwAAAKCVUQ0NAAAAyhm6f/CDH6TXXnutUTcYRdauvfbaBW0XAAAAtI3h5cstt1xac80105AhQ9JOO+2U1l9//dSzZ8/UsWPH9MEHH6Rnn3023X///en666/P23/zm98U33IAAABoDaH77LPPTocffni65ppr0q9+9ascsmvr0qVL2nrrrXPY/sY3vlFUWwEAAKB1FlJbfvnl08knn5xP0bv9yiuvpFmzZqXu3bunVVddVeVyAAAAWNDq5WHppZfOJwAAAGDuVC8HAACAggjdAAAAUBChGwAAAAoidAMAAEC5Q/fUqVPn+ffPP/88/fe//22ONgEAAEDbCt09evSoE7zXXnvt9Oqrr9acf++999LGG2/c/C0EAACA1h66q6ur65yfMmVK+uyzz+Z5GQAAAGjLmnVOd7t27Zrz5gAAAKCiKaQGAAAABWnflF7s6dOnp44dO+Zh5HF+xowZqaqqKv+99BMAAABoYuiOoN2vX7865wcOHFjnvOHlAAAA8CVC9+jRoxt7UQAAAKApoXuzzTYrtiUAAADQVkN3Y+dsd+3adUHaAwAAAG0vdC+11FLznLNdmtM9e/bs5mobAAAAVDRzugEAAKAg5nQDAABAQRZprhsaN25c+uY3v9lcNwcAAABtK3Tfcccd6fjjj08nnXRSevHFF/O2iRMnpl133TVtsMEGac6cOUW1EwAAAFrv8PLf/va36ZBDDknLLLNM+uCDD9I111yTLr300nTEEUekvfbaK40fPz4NGDCg2NYCAABAa+zpvvzyy9MFF1yQ3n333XTjjTfmn7/61a/S008/nX79618L3AAAAPBlQ/cLL7yQ9thjj/z7brvtltq3b58uuuii1KtXr8beBAAAALQpjQ7ds2bNSp07d86/x3rcHTp0SD169CiybQAAANA25nSHmMe95JJL5t8///zzNGLEiNS9e/c6lznyyCObt4UAAADQ2kP3V7/61XT11VfXnF9hhRXSn/70pzqXiR5woRsAAACaGLqnTJnS2IsCAAAATV2nGwAAACigp/vYY49tcHu3bt1Sv379ckXzKK4GAAAANDF0P/744w1u//DDD9Pzzz+fTj311PSf//wnz/1urCuvvDKfSkPX11xzzXTaaael7bffvtG3AQAAABUfukePHj3Xv1VVVaX99tsvnXDCCenPf/5zo+881vg+//zzU9++fVN1dXX6wx/+kHbZZZcc8COAAwAAQGrrc7q7du2ae7ofeOCBJl1vp512SjvssEMO3TFE/ZxzzslLkj388MPN0SwAAAConHW65yXW637//fe/9PVnz56d/vKXv6SPPvoobbzxxs3VLAAAAKj80B2906uuumqTr/f000/nkP3xxx/nXu5bb701rbHGGg1e9pNPPsmn2sPaAQAAoOJD91NPPdXg9mnTpqWxY8emc889N51++ulNbsDqq6+ennjiiXw7N910Uxo+fHi69957Gwze5513XjrzzDObfB8AAABQDu2qo4JZIyyyyCKpXbt2ueBZQ0PLY0mxn/zkJ/kyC2LrrbfOPeZXXXVVo3q6e/funQN7zCuneOPGjUuDBw/OB1oGDRpU7uYAAACUReTRWEJ7fnm00T3dL730UoPb48aXXnrp1FzmzJlTJ1jXFuuAWwscAACAStHo0L3SSis1+52feOKJeU3uWNt7+vTpebmxe+65J91xxx3Nfl8AAADQYpcMi+HEW2yxRYPFy6I7Pf725JNPNunOp06dmvbff/88r3urrbZKjz76aA7c22yzTZNuBwAAACq6p/uSSy5JW265ZYNj1WMcewTliy66KI0cObLRd/7b3/628S0FAACA1trT/cgjj6Rddtllrn/faaed0oMPPthc7QIAAIC2E7pff/311KVLl7n+PdbYfvPNN5urXQAAANB2Qvdyyy2Xnnvuubn+feLEiXnpMAAAAKCJoTvWzz7nnHMa/Fus3R1/i8sAAAAATSykdsopp6TBgwenDTfcMB133HG54niphzuKrE2aNCmNGDGisTcHAAAArV6jQ/eqq66a7r777nTAAQekvffeO7Vr166ml3uNNdZId911V1pttdWKbCsAAAC0ztAd1l9//TR+/Pj0xBNPpMmTJ+fA3a9fv7TeeusV10IAAABoC6G7JEK2oA0AAADNVEgNAAAAaBqhGwAAAAoidAMAAEBLCN2ff/55Ouuss9Jrr71WVHsAAACgbYbu9u3bp4suuiiHbwAAAKCZh5dvueWW6d57723q1QAAAKDNafKSYdtvv3064YQT0tNPP50GDx6cllhiiTp/33nnnZuzfQAAANB2Qvehhx6af1566aVf+Fu7du3S7Nmzm6dlAAAA0NZC95w5c4ppCQAAALQyC7Rk2Mcff9x8LQEAAIC2Hrpj+PjZZ5+dVlxxxbTkkkumF198MW8/9dRT029/+9si2ggAAABtI3Sfc845acSIEenCCy9Miy++eM32tdZaK11zzTXN3T4AAABoO6H7j3/8Y/rNb36T9ttvv7TooovWbF933XXTxIkTm7t9AAAA0HZC9+uvv55WW221BgusffbZZ83VLgAAAGh7oXuNNdZIY8aM+cL2m266KQ0cOLC52gUAAABtb8mw0047LQ0fPjz3eEfv9i233JKee+65POz8tttuK6aVrdTkyZPT9OnTUyWZMGFCnZ+VpkuXLqlv377lbgYAANBGtKuurq5u6pWip/uss85KTz75ZJoxY0YaNGhQDuPbbrttWpiqqqpSt27d0rRp01LXrl1TpQXufv36lbsZbdKkSZMEbwAAYKHk0Sb3dIehQ4emu+66a0Ha1+aVerhHjhyZBgwYkCrFrFmz0pQpU1KfPn1Sp06dUiWJ3vlhw4ZV3OgCAACgcn2p0B0ee+yxmiHGMc978ODBzdmuNiMCd4wUqCRDhgwpdxMAAABaZ+h+7bXX0j777JMeeOCBtNRSS+VtH374Ydpkk03S9ddfn3r16lVEOwEAAKD1Vy8/+OCD89Jg0cv9/vvv51P8HkXV4m8AAADAl+zpvvfee9ODDz6YVl999Zpt8fsVV1yR53oDAAAAX7Knu3fv3rmnu77Zs2ennj17NvXmAAAAoNVqcui+6KKL0hFHHJELqZXE70cddVS6+OKLm7t9AAAA0HaGlx9wwAFp5syZacMNN0zt2///q3/++ef59+9+97v5VBLzvQEAAKCtanLovuyyy4ppCQAAALT10D18+PBiWgIAAABtfU43AAAA0DhCNwAAABRE6AYAAICCCN0AAADQ0kL3888/n+644440a9asfL66uro52wUAAABtL3S/9957aeutt079+vVLO+ywQ3rzzTfz9oMOOigdd9xxRbQRAAAA2kboPuaYY1L79u3TK6+8kjp37lyzfa+99kq33357c7cPAAAA2s463XfeeWceVt6rV6862/v27Ztefvnl5mwbAAAAtK2e7o8++qhOD3fJ+++/nzp06NBc7QIAAIC219M9dOjQ9Mc//jGdffbZ+Xy7du3SnDlz0oUXXpi22GKLItoI8AUzZ85MEydOLOz2o0jklClTUp8+fVKnTp0KuY/+/fs3eBATAIA2HLojXG+11VbpscceS59++mn68Y9/nJ555pnc0/3AAw8U00qAeiJwDx48OFWysWPHpkGDBpW7GQAAtKTQvdZaa6VJkyalX/ziF6lLly5pxowZabfddkuHHXZY6tGjRzGtBGiglzhCa1EmTJiQhg0blkaOHJkGDBhQ2GMAAKB1a3LoDt26dUsnn3xy87cGoJFiWPbC6CWOwK03GgCAhRq6P/744/TUU0+lqVOn5vncte28885fujEAAADQpkN3rMW9//77p3ffffcLf4uiarNnz26utgEAAEDbWjLsiCOOSHvssUd68803cy937ZPADQAAAAsQut9+++107LHHpuWXX76pVwUAAIA2pcmhe/fdd0/33HNPMa0BAACAtjynO5YKi+HlY8aMSWuvvXZabLHF6vz9yCOPbM72AQAAQNsJ3dddd1268847U8eOHXOPdxRPK4nfhW4AAAD4kqE71uc+88wz0wknnJAWWaTJo9MBAACgzWhyav7000/TXnvtJXADAADAfDQ5OQ8fPjzdcMMNTb0aAAAAtDlNHl4ea3FfeOGF6Y477kjrrLPOFwqpXXrppc3ZPgAAAGg7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p3uh93Rv9ZM1Be6FaObMmfnnuHHjUiWZNWtWmjJlSurTp0/q1KlTqiQTJkwodxOAZiB0A8AC6N69e9rv+8eWuxlQuIkTJ+afhxxySLmb0uZ06dKl3E0AFoDQDQBAo0Z1hBhd0Llz52bv0R02bFiqZCNHjkwDBgwoJHD37du32W8XWHjaVVdXV6cKVVVVlbp165amTZuWunbtWu7mAADwJYeul3rSK3V4eREHI4DWkUeFbgAAACgoj1qnGwAAAAoidAMAAEBBhG4AAAAoiNANAAAABRG6AQAAoCBCNwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAAFEToBgAAgIII3QAAAFAQoRsAAAAKInQDAABAQdqnClZdXZ1/VlVVlbspAAAAtCFV/5dDS7m0VYbu6dOn55+9e/cud1MAAABog6ZPn566des217+3q55fLG/B5syZk954443UpUuX1K5du3I3p80czYmDHK+++mrq2rVruZsDhbGv0xbYz2kL7Oe0Ffb1hS+idATunj17pkUWWaR19nTHA+vVq1e5m9EmxRvZm5m2wL5OW2A/py2wn9NW2NcXrnn1cJcopAYAAAAFEboBAACgIEI3TdKhQ4d0+umn55/QmtnXaQvs57QF9nPaCvt6y1XRhdQAAACgJdPTDQAAAAURugEAAKAgQjcAAAAUROgGAACAggjdNIm6ewAAUPnmzJlT7ia0GUI38/XRRx+l6dOnp6qqqtSuXbtyNwcK8/7776eJEyemyZMnp08//bTczYEWY+bMmeVuAhTu888/T5999lm5mwGFiu84N998c/59kUUWEbwXEqGbeXr22WfTbrvtljbbbLM0YMCAdO211+bterxpbcaPH5+23nrrtOeee6a11147XXjhhWn27NnlbhaU3dixY9Nqq62WXn755XI3BQr9vrP33nvn/wf23XffdNNNNzn4Sqvz4YcfpvXWWy/tv//+6Te/+U3eJngvHEI38/wPaNNNN01rrrlmOv744/N/RgceeGB64okn9HjT6vb1zTffPG211Vbp+uuvT+ecc0467bTT0htvvFHupkFZPfnkk2mLLbZIu+++e1pppZXK3RworOdvk002SZ06dUrbbbddPsB03nnnpcMOOyzNmjWr3M2DZrPYYoulVVZZJe21117piiuuSFdeeWVN8NahVqx21Z5h5jLMdp999kn9+/dPl19+ec32+PIVvYA///nP85tT+KbSvfvuu+nb3/52GjhwYLrsssvytti3d9hhhxy840vYsssum3r37l3upsJC9dRTT6UhQ4akH/7wh3nkR7wvopckpht95StfSR07dsyX838Ble6ss87KHQq33HJLzTDz+J5z3XXXpb59+6bf/e53Nfs7VLrtt98+7bjjjumll15Kf/vb39IJJ5yQDjrooPweWGONNdLiiy9e7ia2Su3L3QBappjTFF+uoncjxLCTOAq28sor50AefMmiNYj9+Bvf+EbNvh5++tOfpjvuuCO99dZbOZTHaI9TTjklff3rXy9rW2FhiWAdUy169OiRA3f8HxBDbqMHcNy4cflLWxys+s53vuP/Aire1KlT06uvvlpzvn379rmXOw66/uEPf0jnnntuOv3009Oiiy5a1nbCgoiDSbFvd+vWLfXs2TP3dsfn96WXXpo7HZZccsl09913595wn+vNz/ByGrT88sunkSNHpqFDh+bzpbmtK664Yg7ftc2YMaMsbYTmEL3Yhx9+eO7NCDG8PL5cxc9///vfuY5BHGiK36GtiC9mxx13XHr99dfTiSeemHbeeef8Poj3ym9/+9v85SzC+F//+tdyNxW+tNI81pjjGiEjRneUBoB26NAhz3vdeOON0z/+8Y80bdq0MrcWmq72XO34XA8bbrhhGjNmTFpuueXSj3/847zPxxSLmFK6xBJL5PeCgdDNT+hmrkohJN6wcdQrxJswjgiXxJynKMQQR8+gUnXp0qXm9/iC9dhjj+VevmWWWSb/JxRDaaOYFLQV0cM3bNiw9Itf/CJdfPHF6YMPPsgHoPbbb7+8PUZ+LLXUUvmLG1SaUqAodSLEdKKo4XHmmWfmUX6ly0QAiYOwUdtg9OjRZW0zNNULL7yQP8PfeeedOts7d+6cnn766fz7SSedlN577708cumf//xnzTQ7Pd3Nz/By5qtUXKH0Biz9JxXzXWMY7uOPP15z9AwqXRSLKhWMigNOUb02evXWWWedcjcNChWhI5bMiykVcdApgvcee+yRunfvnt8LMSqk9H9B1PuIEVFRhBAqyXPPPZcPIL3yyit5ytBGG22U1lprrTRq1Khct+aII45IF110UZ5aEaJTIT7/4yAsVIoYtbHlllum4cOH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5JJL0kMPPWTViQqgejkAVOAQxCOPPDIPMYxCaYcffng6/vjj87JfEbzriy9kET6efPLJ9MADD+QvddDSvf/++2mfffbJQTp6tEu22GKLtPbaa+cDSCUxJPenP/1pvk4UCIxllSKYQ2vYzyOulQ6o3njjjXk4+VJLLZVHedSu3k/L5fAfAFSY6MGOL1rLLrts2muvvXLQji9hoXbwjjl+06ZNSy+++GJefibWMI4vcVAJYrh41B7Yfffd8/k4yBT7/sorr5yDSogwEqeo8nzBBRfUuRy0lv289gim+OyPCuaxjJjP88ohdANAhYlKzcOHD8/DasOee+6Zg0f0lsTPWKs4Ann8Hl/gbrjhhtSrV688VBEqxfLLL59GjhxZswxSHESKMLLiiivmdYpLYSROUXiqVLegdkCB1rCfl8TB01VXXTX961//qvn8pzII3QBQgUpfuEpf0KLHO0J2VHKO0HH00Ueniy++OE2ZMiV/oYviO1BpSkEkDh7FevMh9vMolFYSFfujcnlMuYhpFEI3rXU/j3oF8dkucFceoRsAKlipUFp8WYsh5hE4vvOd76S//e1vudjaY489JnBT8eLAUu15raXh46eddlqeyx1TJxRNoy3s56qUVyYTXgCgwpWG2MaXtejxHjp0aHrnnXfyF7T6lW+hUpVq/0a47t27dx7JceGFF+YDS+uuu265mwfNwn7eOjkkCACtQITuGGr+ox/9KI0ePTo98cQTiuzQqpR6/WL47dVXX53ncN9///2WS6JVsZ+3Tnq6AaAViWJp48aNy2sWQ2u03Xbb5Z8PPvig5e9oteznrYt1ugGgFak9HxBa81r1iknR2tnPWw+hGwAAAApieDkAAAAUROgGAACAggjdAAAAUBChGwAAAAoidAMAAEBBhG4AAAAoiNANAP9nypQpeY3rJ554IrUUEydOTBtttFHq2LFjWm+99VJbEq/FqFGjUqU64IAD0q677lruZgBQZkI3AC0qpETQOv/88+tsj+AV29ui008/PS2xxBLpueeeS//+97/nerm33norHXHEEWmVVVZJHTp0SL1790477bRTnevMLcTWD4ebb755vmycIuz369cvnXfeeam6unquByhK5xs6Pfzww1/qsb/55ptp++23TwuiT58+Ne1YdNFFU8+ePdNBBx2UPvjggybdTjwnRx999AK1BYC2SegGoEWJkHfBBRc0ORS1ZJ9++umXvu4LL7yQvv71r6eVVlopLbvssg1eJgLv4MGD03/+85900UUXpaeffjrdfvvtaYsttkiHHXbYl7rfQw45JIfeCPsnnnhiOu2009Kvf/3r+V7v7rvvzterfYq2fRkrrLBCPoCwoM4666zcjldeeSVde+216b777ktHHnnkAt8uADSG0A1Ai7L11lvnsBU9q3NzxhlnfGGo9WWXXZZ7Nev33p577rlp+eWXT0sttVQOX59//nn60Y9+lJZZZpnUq1ev9Pvf/77BId2bbLJJPgCw1lprpXvvvbfO38ePH597YJdccsl829/5znfSu+++W6dX9PDDD889o927d0/bbbddg49jzpw5uU3RjgiX8ZgiLJdE7+zYsWPzZeL3eNwNOfTQQ/Pf//vf/6Zvf/vbuWd6zTXXTMcee+yX7mXu3Llzfh0i7B944IFpnXXWSXfdddd8rxcHBuJ6tU+LLbZY/tuTTz6ZDwR06dIlde3aNYfxxx57bK63VbtnvtSTfsstt+TbiPatu+666aGHHppvm+L+oh0rrrhivu7w4cPTuHHjav7+3nvvpX322Sf/PW537bXXTtddd12dfSn2gcsvv7ym1zzaE5555pn0zW9+Mz+euJ+hQ4fmAyW1XXzxxalHjx75uYmDIJ999tl82wxA6yF0A9CixBDgCMpXXHFFeu211xbotqLn94033sg9m5deemkeqh0Baemll06PPPJI+sEPfpC+//3vf+F+IpQfd9xx6fHHH08bb7xxHqYdwSx8+OGHacstt0wDBw7MgTFC8ttvv5323HPPOrfxhz/8IS2++OLpgQcemGsPcYS4Sy65JIeyp556KofznXfeOU2ePDn/PXpnIzxHW+L3448//gu38f777+c2RJiLYej1xcGGBRFDyseMGZMPRMTjWRD77bdfPsDw6KOP5oMJJ5xwQk0gb6yTTz45Pw8xrD0OLkRYjgMpjfX666+nv//972nDDTes2fbxxx/nAwD/+Mc/8gGV733ve/lAShzEKL1OsR+Uev/jFMP347Y23XTTfMAk9rV4TN/97nfrtGf06NE5hMfP2CdGjBiRTwC0IdUA0EIMHz68epdddsm/b7TRRtXf/e538++33nprTCauudzpp59eve6669a57s9+9rPqlVZaqc5txfnZs2fXbFt99dWrhw4dWnP+888/r15iiSWqr7vuunz+pZdeyvdz/vnn11zms88+q+7Vq1f1BRdckM+fffbZ1dtuu22d+3711Vfz9Z577rl8frPNNqseOHDgfB9vz549q88555w62zbYYIPqQw89tOZ8PM54vHPzyCOP5Pu+5ZZb5nt/cbl4Luf1vJfav9hii+XnJn7G9Tp27Fj9wAMP1Fym9Fw9/vjjdc536tQpX6/2qaRLly7VI0aMmG87G2pv6favueaamr8/88wzeduECRPmehuxDyy++OK5HfEY4vIbbrhh9QcffDDP+95xxx2rjzvuuDrPyVFHHVXnMieeeGL1yiuvXP3pp582eBulfTD2s5I99tijeq+99mrEowegtdDTDUCLFPO6o2dwwoQJX/o2opd4kUX+919dDAWPocO1e9VjyO/UqVPrXC96NUvat2+f1l9//Zp2xBDp6LWMoeWlU//+/fPfag8rnt885qqqqtwLP2TIkDrb43xTHnPt4mbNKXqlozc5eupjKH30MMeQ+/m54YYb8vVqn0piuPvBBx+cpxBEsbz6w7AbI4a5l8SQ7VD/9asvRi5EO2I0Qamw3I477phmz56df4+fZ599dt43YtpBvKZ33HFHngM+L3GbMZx8Xr31sQ/Gfla7zfNrLwCtS/tyNwAAGhLDdmO4dRTxijm1tUWQrh82G5onWz8MxVzchrbF3OrGmjFjRh5uHgcF6iuFwNDQUO8i9O3bNz+GGP49PzHneNq0aV/YHkPmu3XrVmdbnF9ttdXy7zfeeGP+PZYui8A8LzHsunS9+mJO+r777puHcf/rX//Kw/2vv/769K1vfSs1Vu3Xr1TRfn6vX8yrL7Upnq+Y/x8HVuLgSTyeKD4XQ8hjewTveO1iPv78CuB16tSpSe39MvsbAJVPTzcALVb0hsb82/rFspZbbrm8RFbt4N2ca2vXLj4W83Njru6AAQPy+UGDBuXiWVG0LYJc7VNTgnYU3orlq6InubY4v8YaazT6dqJnNg5O/PKXv0wfffRRg4G6ZPXVV8+Ppbbo5Y3e+5gfPTfR83vUUUfludQL2rMe93PMMcekO++8M+22224NFrIrWqnnedasWTXP+S677JKGDRuWi7PFsmuTJk2qc52Yz17qGa/d6x7z3RVGA2BehG4AWqzodYxhzj//+c/rbI/q4O+880668MIL8xDlCJzRc9pc4vZuvfXW3HscBcpi+bIokBXifBQviwJeURAs7j+GIkeF7/qhbH5i2HP0mMeQ7FiaKwqLxcGDCLhNbW/c99e+9rV0880350JsMUQ9nrfaQ+VjePc111yTfvWrX+XLxH1F0bB4fDHse16i4FwE0bj9eYmCc3FApPYpCpVFwI2K7vfcc096+eWXc9CN5690MKNI06dPz+2IAmhRHC2e9zhwUxouH73fUZn9wQcfzM9bPNYojldbHGSJ4ntRtTwq1UdvdTyemCaw995756J68Zz+6U9/yq8lAJQI3QC0aLFcVv3huBHUIjhG2IyeyQhSDVX2XpAe9jjFbd9///3pb3/7Wx6iHEq90xFyt91223xgIIYiR5Xw2vPHGyPWio4gHNXJ43aiCnncV4TApoie2VgCK5bDituKZc622WabPH/5yiuvrLlcHCiI0P273/0uzzn/xje+kcNoVHeP+e7z61Hff//98xDxeQ2PjuHaMcy+9imW/Yre5QjkcRvR2x3V3mOu+JlnnpmKFmuMRzvitYvq9TEiIXraS+uen3LKKXkEQ4wYiAM6sbxYLDdXW+xf8RhiFEIE9pjvHdePquUx5WCzzTbLz+nVV1/d5IrsALRu7aKaWrkbAQAAAK2Rnm4AAAAoiNANAAAABRG6AQAAoCBCNwAAABRE6AYAAICCCN0AAABQEKEbAAAACiJ0AwAAQEGEbgAAACiI0A0AAAAFEboBAACgIEI3AAAApGL8P2DPln7ItiwOAAAAAElFTkSuQmCC\",\n",
+ " \"text/plain\": [\n",
+ " \"<Figure size 1000x600 with 1 Axes>\"\n",
+ " ]\n",
+ " },\n",
+ " \"metadata\": {},\n",
+ " \"output_type\": \"display_data\"\n",
+ " }\n",
+ " ],\n",
+ " \"source\": [\n",
+ " \"# Graph 1. CURIE count vs time taken.\\n\",\n",
+ " \"\\n\",\n",
+ " \"# —————————————————————\\n\",\n",
+ " \"# 2) Group by batch size\\n\",\n",
+ " \"# —————————————————————\\n\",\n",
+ " \"groups = [grp['time_taken_per_curie_ms'].values\\n\",\n",
+ " \" for _, grp in df.groupby('curie_count')]\\n\",\n",
+ " \"\\n\",\n",
+ " \"labels = [str(size) for size, _ in df.groupby('curie_count')]\\n\",\n",
+ " \"\\n\",\n",
+ " \"del groups[0]\\n\",\n",
+ " \"del labels[0]\\n\",\n",
+ " \"\\n\",\n",
+ " \"# —————————————————————\\n\",\n",
+ " \"# 3) Boxplot of per‑CURIE time by batch size\\n\",\n",
+ " \"# —————————————————————\\n\",\n",
+ " \"plt.figure(figsize=(10,6))\\n\",\n",
+ " \"plt.boxplot(groups, tick_labels=labels, showfliers=True)\\n\",\n",
+ " \"plt.xlabel(\\\"Number of CURIEs in Batch\\\")\\n\",\n",
+ " \"plt.ylabel(\\\"Time per CURIE (ms)\\\")\\n\",\n",
+ " \"plt.title(\\\"Distribution of Time per CURIE by Batch Size\\\")\\n\",\n",
+ " \"plt.xticks(rotation=45)\\n\",\n",
+ " \"plt.tight_layout()\\n\",\n",
+ " \"plt.show()\"\n",
+ " ]\n",
+ " },\n",
+ " {\n",
+ " \"cell_type\": \"code\",\n",
+ " \"execution_count\": 16,\n",
+ " \"id\": \"ebb8cd6e-4162-4ba5-b66e-27b7aa9076b4\",\n",
+ " \"metadata\": {},\n",
+ " \"outputs\": [\n",
+ " {\n",
+ " \"data\": {\n",
+ " \"image/png\": \"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…
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is to track how quickly NodeNorm normalizes identifiers, and also which identifiers are being normalized as per #277 (comment).
Also removed some unnecessary catch-everything blocks as per #180.