Full Code of google-research/proteinfer for AI

master 0acc7b16eb93 cached
49 files
38.3 MB
1.5M tokens
272 symbols
1 requests
Download .txt
Showing preview only (6,017K chars total). Download the full file or copy to clipboard to get everything.
Repository: google-research/proteinfer
Branch: master
Commit: 0acc7b16eb93
Files: 49
Total size: 38.3 MB

Directory structure:
gitextract_3w94dd4w/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── baseline_utils.py
├── baseline_utils_test.py
├── colab_evaluation.py
├── colab_evaluation_test.py
├── colabs/
│   ├── Class_Activation_Mapping.ipynb
│   ├── Embeddings.ipynb
│   ├── Price.ipynb
│   ├── README.md
│   ├── Random_EC.ipynb
│   └── Random_GO.ipynb
├── evaluation.py
├── evaluation_test.py
├── hparams_sets.py
├── inference.py
├── inference_test.py
├── install_models.py
├── misc/
│   └── price_et_al_table_s12.csv
├── parenthood_bin.py
├── parenthood_lib.py
├── parenthood_lib_test.py
├── protein_dataset.py
├── protein_dataset_test.py
├── protein_model.py
├── protein_model_test.py
├── proteinfer.py
├── proteinfer_test.py
├── requirements.txt
├── test_util.py
├── test_util_test.py
├── testdata/
│   ├── blast.tsv
│   ├── dev-00000-of-00001.tfrecord
│   ├── enzclass.txt
│   ├── go.obo
│   ├── label_vocab.tsv
│   ├── saved_model/
│   │   ├── saved_model.pb
│   │   └── variables/
│   │       ├── variables.data-00000-of-00001
│   │       └── variables.index
│   ├── test-00000-of-00001.tfrecord
│   ├── test_hemoglobin.fasta
│   └── train-00000-of-00001.tfrecord
├── train.py
├── train_test.py
├── utils.py
└── utils_test.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/test.yml
================================================
name: Testing

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [ 3.7]

    steps:
    - uses: actions/checkout@v2
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v2
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        sed 's/tensorflow-gpu/tensorflow/' requirements.txt > requirements.txt.modified
        pip install -r requirements.txt.modified
    - name: Test
      run: |
        bash -c 'for f in *_test.py; do python3 $f || exit 1; done'


================================================
FILE: .gitignore
================================================
cached_models/**
__pycache__/**

================================================
FILE: CONTRIBUTING.md
================================================
This repository exists to provide a record of published work, and so we do not
expect to receive pull-requests and do not currently have infrastructure to
support these.

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Community Guidelines

This project follows
[Google's Open Source Community Guidelines](https://opensource.google.com/conduct/).


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

================================================
FILE: README.md
================================================
[![GitHub license](https://img.shields.io/badge/license-Apache2-blue.svg)](https://github.com/google-research/nisaba/blob/main/LICENSE)
[![Paper](https://img.shields.io/badge/paper-eLife-blue.svg)](https://elifesciences.org/articles/80942)
# ProteInfer

ProteInfer is an approach for predicting the functional properties of protein
sequences using deep neural networks.

#### 📝 Read about the method in [our interactive paper](https://google-research.github.io/proteinfer/) (or [in the static version](https://elifesciences.org/articles/80942), published in eLife).

## Usage instructions

Go to https://google-research.github.io/proteinfer/ to use an interactive demo in your
browser, and read the related paper.

Or if you're interested in the command line interface instead, see below.

You do not need a Google Cloud machine - these instructions were made as
such just to demonstrate what it would take if you had to install
everything from scratch.

### Install gcloud on your local machine if you don't have it installed
```
sudo apt install -y google-cloud-sdk
gcloud auth login
```

### Create GCP instance with a GPU
```
gcloud compute instances create proteinfer-gpu --machine-type n1-standard-8 --zone us-west1-b --accelerator type=nvidia-tesla-v100,count=1  --image-family ubuntu-2004-lts --image-project ubuntu-os-cloud --maintenance-policy TERMINATE --boot-disk-size 250
```

### ssh into the machine
```
# You may need to wait ~30 seconds for the machine to boot up first.
gcloud compute ssh proteinfer-gpu
```

### Install cuda dependencies for GPU support
```
sudo apt update
sudo add-apt-repository ppa:graphics-drivers -y

wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-keyring_1.0-1_all.deb -O /tmp/cuda-keyring_1.0-1_all.deb
sudo dpkg -i /tmp/cuda-keyring_1.0-1_all.deb

sudo bash -c 'echo "deb http://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64 /" > /etc/apt/sources.list.d/cuda_learn.list'
sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/machine-learning/repos/ubuntu1804/x86_64/7fa2af80.pub

sudo apt update
sudo apt install -y cuda-10-0 libcudnn7
```

### Install local python virtual environment
```
sudo apt update
sudo add-apt-repository ppa:deadsnakes/ppa -y
sudo apt install -y python3-venv python3.7 python3-pip python3.7-venv 
mkdir ~/python_venv
cd ~/python_venv
python3.7 -m venv proteinfer
source ~/python_venv/proteinfer/bin/activate
cd ~
```

### Get our code from github and install python dependencies (e.g. numpy)
```
git clone https://github.com/google-research/proteinfer
cd ~/proteinfer
pip3 install -r requirements.txt
```

### Run our code on test sequences
```
cd ~/proteinfer
python3 install_models.py
python3 proteinfer.py -i testdata/test_hemoglobin.fasta -o ~/hemoglobin_predictions.tsv

# View your predictions.
cat ~/hemoglobin_predictions.tsv
```

### Copy your sequences as a fasta file to the GCP instance
```
# exit the ssh session by typing ctrl D
gcloud compute scp <YOUR_FASTA_FILE_HERE> proteinfer-gpu:~/
# Then ssh back in again
gcloud compute ssh proteinfer-gpu
# Then run your inference
python3 proteinfer.py -i ~/<YOUR_FASTA_FILE_HERE> -o ~/predictions.tsv
```

### Delete the instance when you're done to save money
```
gcloud compute instances delete 'proteinfer-gpu'
```


## Running unit tests
```
bash -c 'for f in *_test.py; do python3 $f || exit 1; done'
```


================================================
FILE: baseline_utils.py
================================================
# coding=utf-8
# Copyright 2020 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Lint as: python2, python3
"""Utilities for evaluating baseline implementations of function annotation."""

from __future__ import absolute_import
from __future__ import division

from __future__ import print_function

import re
import typing
from typing import Any, Dict, FrozenSet, List, Set, Text

import numpy as np
import pandas as pd
import six
from six.moves import zip
import tensorflow.compat.v1 as tf
import tqdm

_BLAST_OUTPUT_COLUMNS = [
    'query_id',
    'subject_id',
    'percent_seq_identity',
    'alignment_length',
    'n_mismatches',
    'n_gap_opens',
    'query_alignment_start',
    'query_alignment_end',
    'subject_alignment_start',
    'subject_alignment_end',
    'expected_value',
    'bit_score',
]

_MERGE_COL_NAME = 'merge_col'

# e.g. >accession="A0A0PX123"	labels="PF00001,PF00002"
# Notice optional fasta header character '>' at the beginning.
_SEQUENCE_HEADER_RE = re.compile(r'^>?accession="(\w+)"\tlabels="(.*)"')

# Format of query sequence field for interproscan.
_INTERPROSCAN_ACCESSION_REGEX = re.compile(r'accession="(\w+)"')


def _get_sequence_name_from_sequence_header(s):
  """Returns sequence name from fasta header.

  Args:
    s: Fasta header input string. Format of sequence headers is
      >accession_"label1,label2". The initial '>' character is optional.
  """
  return _SEQUENCE_HEADER_RE.findall(s)[0][0]  # 0th match, 0th capture group.


def _get_labels_from_sequence_header(s):
  r"""Returns labels from fasta header.

  Args:
    s: Fasta header input string. Format of sequence headers is
      >accession_"label1,label2". The initial '>' character is optional.
  """
  # 0th match, 1th capture group.
  regex_match = _SEQUENCE_HEADER_RE.findall(s)[0][1]
  if regex_match == '':  # pylint: disable=g-explicit-bool-comparison
    # If the regex match is the empty string, we just want an empty set, as
    # there are no labels.
    return set()
  return set(six.ensure_str(regex_match).split(','))


class BlastResult(
    typing.NamedTuple('BlastResult', (
        ('sequence_name', Text),
        ('closest_sequence', Text),
        ('predicted_label', Set[Text]),
        ('percent_seq_identity', float),
        ('e_value', float),
        ('bit_score', float),
    ))):
  """Result of parsing BLAST output."""

  @staticmethod
  def from_string(s, ground_truth_lookup):
    """Constructs a BlastResult from a line in blast tsv output."""
    tab_split = six.ensure_str(s).split('\t')
    query_sequence = six.ensure_str(tab_split[0]).split('"')[1]
    closest_sequence = six.ensure_str(tab_split[1]).split('"')[1]

    percent_seq_identity = float(tab_split[2])
    e_value = float(tab_split[10])
    bit_score = float(tab_split[11])

    predicted_label = ground_truth_lookup[closest_sequence]

    return BlastResult(
        sequence_name=query_sequence,
        closest_sequence=closest_sequence,
        predicted_label=predicted_label,
        percent_seq_identity=percent_seq_identity,
        e_value=e_value,
        bit_score=bit_score)


class InterproScanResult(
    typing.NamedTuple('InterproScanResult', (('sequence_name', Text),
                                             ('predicted_label', Set[Text])))):
  """Result of parsing InterproScan output."""

  @staticmethod
  def from_string(s):
    tab_split = six.ensure_str(s).rstrip('\n').split('\t')
    if len(tab_split) < 14 or tab_split[13] == '':  # pylint: disable=g-explicit-bool-comparison
      go_labels = set()
    else:
      go_labels = set(six.ensure_str(tab_split[13]).split('|'))

    return InterproScanResult(
        sequence_name=_INTERPROSCAN_ACCESSION_REGEX.findall(tab_split[0])[0],
        predicted_label=go_labels)


def load_ground_truth(filename):
  """Returns dataframe of ground truth from FASTA file.

  Documentation on how to produce this FASTA file is available at
  https://docs.google.com/document/d/1uB8J_a1cURIeVNqa5SOJRBBtimI1nOHbU1NbN43Xr0s

  Args:
    filename: str. Path to FASTA file. Fasta header input string. Format of
      sequence headers is like >accession="A0A0PX123"   labels="PF00001,PF00002"

  Returns:
    pd.DataFrame with columns sequence_name (str), sequence (str),
    true_label(Set[str]).
  """
  dict_for_making_df = {'sequence_name': [], 'true_label': [], 'sequence': []}

  with tf.io.gfile.GFile(filename) as f:
    for line in tqdm.tqdm(f, position=0):
      line = six.ensure_str(line)
      if line.startswith('>'):
        dict_for_making_df['sequence_name'].append(
            _get_sequence_name_from_sequence_header(line))
        dict_for_making_df['true_label'].append(
            _get_labels_from_sequence_header(line))
      else:
        dict_for_making_df['sequence'].append(line.rstrip())

  all_test_seqs = pd.DataFrame(dict_for_making_df)

  return all_test_seqs


def _set_predicted_labels_missing(row):
  """Adds empty set as predicted labels for a DataFrame row."""
  if row[_MERGE_COL_NAME] == 'right_only':
    return set()
  else:
    return row['predicted_labels']


def _fillna(df, column_name, value):
  """Replacement for df.fillna that works with non-indexable values.

  https://github.com/pandas-dev/pandas/issues/21329

  Args:
    df: pd.DataFrame with column `column_name`.
    column_name: str.
    value: value to set when column's value is NaN.
  """
  df[column_name] = [
      (x if isinstance(x, (set, frozenset)) else value) for x in df[column_name]
  ]


def _blast_row_to_confidence_array(
    row,
    label_vocab,
    label_to_vocab_index,
):
  """Returns a confidence array of preds (predicted labels get bit_score)."""
  arr = np.zeros_like(label_vocab, np.float32)
  indexes_to_update = np.array(
      [label_to_vocab_index[l] for l in row.predicted_label], dtype=np.int32)
  arr[indexes_to_update] = row.bit_score
  return arr


def load_blast_output(filename, label_vocab,
                      training_data_ground_truth,
                      test_data_ground_truth):
  """Load a file containing tsv-separated blast output.

  Args:
    filename: str. Path to output of blast.
    label_vocab: np.array of string (labels). Used to compute the output column
      `predictions`.
    training_data_ground_truth: pd.DataFrame. The output of `load_ground_truth`.
      Used to compute predicted labels, by using the labels on the training data
      for the best match, given by blast.
    test_data_ground_truth: pd.DataFrame. The output of `load_ground_truth`.
      Used to compute the true labels.

  Returns:
    pd.DataFrame with columns
    sequence_name (str);
    true_label (Set[str]);
    predicted_label (Set[str]);
    predictions (np.array of length len(label_vocab), filled with the bit score
      of the closest match). A value of 0 is used for sequences with no blast
      calls;
    percent_seq_identity (float, between 0 and 100);
    e_value (float, a measure of confidence. Lower is more confident.).
      A value of NaN is used for missing blast calls;
    bit_score (float, a measure of confidence. Higher is more confident.).
      A value of 0 is used for sequences with no blast calls.

    For more information on e-value and bit score, see
    http://www.metagenomics.wiki/tools/blast/evalue
  """
  training_data_ground_truth_lookup = dict(
      list(
          zip(training_data_ground_truth.sequence_name,
              training_data_ground_truth.true_label)))

  blast_output_namedtuples = []
  with tf.io.gfile.GFile(filename) as f:
    for line in tqdm.tqdm(f, position=0):
      blast_output_namedtuples.append(
          BlastResult.from_string(
              line, training_data_ground_truth_lookup))

  blast_output = pd.DataFrame.from_records(
      blast_output_namedtuples, columns=BlastResult._fields)

  # Add in ground truth to get all sequences (including those missed by BLAST).
  blast_output = blast_output.merge(
      test_data_ground_truth,
      on='sequence_name',
      how='right',
      indicator=_MERGE_COL_NAME)

  label_to_vocab_index = {l: i for i, l in enumerate(label_vocab)}
  # For all sequences for which we don't have predictions, set the predictions
  # to the empty set.
  _fillna(blast_output, 'predicted_label', frozenset())

  # A bit score of 0 is a "zero confidence" score.
  blast_output['bit_score'].fillna(0., inplace=True)

  blast_output['predictions'] = blast_output.apply(
      axis='columns',
      func=lambda row: _blast_row_to_confidence_array(  # pylint: disable=g-long-lambda
          row, label_vocab, label_to_vocab_index))

  # Clean up output columns.
  blast_output.drop(inplace=True, columns=[_MERGE_COL_NAME, 'sequence'])

  return blast_output


def _pad_ec_label_with_hyphens(label):
  """E.g. Given 'EC:1', returns 'EC:1.-.-.-'."""
  to_return = label
  while to_return.count('.') != 3:
    to_return += '.-'
  return to_return


def limit_set_of_labels(df, acceptable_labels,
                        column_to_limit):
  """Limits the set of things in df[column_to_limit to acceptable_labels."""
  working_df = df.copy()

  working_df[column_to_limit] = working_df[column_to_limit].apply(
      acceptable_labels.intersection)
  return working_df


def limit_labels_for_label_normalizer(
    label_normalizer,
    acceptable_labels):
  """Limits keys and values in label_normalizer to acceptable labels."""
  limited_label_normalizer = {}
  for k, v in label_normalizer.items():
    if k in acceptable_labels:
      limited_label_normalizer[k] = sorted(
          acceptable_labels.intersection(set(v)))

  return limited_label_normalizer


def load_interproscan_output(
    interproscan_output_filename,
    test_data_ground_truth,
):
  """Loads tab-separated output from interproscan.

  Args:
    interproscan_output_filename: file path to tsv interproscan output.
    test_data_ground_truth: pd.DataFrame. The output of `load_ground_truth`.
      Used to compute the true labels.

  Returns:
    pd.DataFrame with output columns 'sequence_name' (Text), predicted_label
    (frozenset).
  """
  interproscan_output_namedtuples = []
  with tf.io.gfile.GFile(interproscan_output_filename) as f:
    for line in tqdm.tqdm(f, position=0):
      interproscan_output_namedtuples.append(
          InterproScanResult.from_string(line))

  interproscan_output = pd.DataFrame.from_records(
      interproscan_output_namedtuples, columns=InterproScanResult._fields)

  collapsed_interproscan_output_dict = {
      'sequence_name': [],
      'predicted_label': []
  }

  # There is more than one output per accession because interproscan supports
  # many analyses of each sequence, and so all the go labels need to be
  # collapsed.
  for grouping_key, group in tqdm.tqdm(
      interproscan_output.groupby('sequence_name'), position=0):
    collapsed_interproscan_output_dict['sequence_name'].append(grouping_key)

    collapsed_interproscan_output_dict['predicted_label'].append(
        frozenset().union(*group.predicted_label.values.tolist()))

  collapsed_interproscan_output = pd.DataFrame(
      collapsed_interproscan_output_dict)

  # Add in ground truth to get all sequences (including those missed by BLAST).
  interproscan_output = collapsed_interproscan_output.merge(
      test_data_ground_truth,
      on='sequence_name',
      how='right',
      indicator=_MERGE_COL_NAME)

  # For all sequences for which we don't have predictions, set the predictions
  # to the empty set.
  _fillna(interproscan_output, 'predicted_label', frozenset())

  # Clean up output columns.
  interproscan_output.drop(inplace=True, columns=[_MERGE_COL_NAME, 'sequence'])

  return interproscan_output


================================================
FILE: baseline_utils_test.py
================================================
# coding=utf-8
# Copyright 2020 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Lint as: python2, python3
# pylint: disable=line-too-long
"""Tests for module model_performance_analysis.py."""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import tempfile

from absl.testing import absltest
from absl.testing import parameterized
import numpy as np
import pandas as pd

import baseline_utils
import test_util
import tensorflow.compat.v1 as tf


def _write_to_file(contents):
  tmpfile_name = tempfile.mktemp()
  with tf.io.gfile.GFile(tmpfile_name, "w") as f:
    f.write(contents.encode("utf-8"))
  return tmpfile_name


class BaselineUtilsTest(parameterized.TestCase):

  @parameterized.named_parameters(
      dict(
          testcase_name="has fasta character >",
          header='>accession="ACCESSION"\tlabels="label1,label2"',
          expected="ACCESSION",
      ),
      dict(
          testcase_name="does not have character >",
          header='accession="ACCESSION"\tlabels="label1,label2"',
          expected="ACCESSION",
      ),
  )
  def test_get_sequence_name_from_sequence_header(self, header, expected):
    actual = baseline_utils._get_sequence_name_from_sequence_header(header)
    self.assertEqual(actual, expected)

  @parameterized.named_parameters(
      dict(
          testcase_name="two labels",
          header='>accession="ACCESSION"\tlabels="label1,label2"',
          expected={"label1", "label2"},
      ),
      dict(
          testcase_name="zero labels",
          header='>accession="ACCESSION"\tlabels=""',
          expected=set(),
      ),
  )
  def test_get_labels_from_sequence_header(self, header, expected):
    actual = baseline_utils._get_labels_from_sequence_header(header)
    self.assertEqual(actual, expected)

  def test_load_ground_truth(self):
    input_fasta = ('>accession="ACCESSION"\tlabels="GO:101010,EC:9.9.9.9"\n'
                   "ADE\n"
                   '>accession="ACCESSION2"\tlabels="EC:1.2.-.-"\n'
                   "WWWW\n")
    tmpfile_name = _write_to_file(input_fasta)
    actual = baseline_utils.load_ground_truth(tmpfile_name)

    expected = pd.DataFrame({
        "sequence_name": ["ACCESSION", "ACCESSION2"],
        "true_label": [{"GO:101010", "EC:9.9.9.9"}, {"EC:1.2.-.-"}],
        "sequence": ["ADE", "WWWW"]
    })

    test_util.assert_dataframes_equal(self, actual, expected)

  @parameterized.named_parameters(
      dict(
          testcase_name="no inputs, one thing in vocab",
          input_row=pd.Series({
              "predicted_label": frozenset([]),
              "bit_score": 99.
          }),
          input_label_vocab=np.array(["PF00001"]),
          expected=[0.],
      ),
      dict(
          testcase_name="one input, one thing in vocab",
          input_row=pd.Series({
              "predicted_label": frozenset(["PF00001"]),
              "bit_score": 99.
          }),
          input_label_vocab=np.array(["PF00001"]),
          expected=[99.],
      ),
      dict(
          testcase_name="one input, two things in vocab",
          input_row=pd.Series({
              "predicted_label": frozenset(["PF00001"]),
              "bit_score": 99.
          }),
          input_label_vocab=np.array(["PF00001", "PF99999"]),
          expected=[99., 0.],
      ),
      dict(
          testcase_name="two inputs, two things in vocab",
          input_row=pd.Series({
              "predicted_label": frozenset(["PF00001", "PF99999"]),
              "bit_score": 99.
          }),
          input_label_vocab=np.array(["PF00001", "PF99999"]),
          expected=[99., 99.],
      ),
  )
  def test_blast_row_to_confidence_array(self, input_row, input_label_vocab,
                                         expected):
    lookup = {k: i for i, k in enumerate(input_label_vocab)}

    actual = baseline_utils._blast_row_to_confidence_array(
        input_row, input_label_vocab, lookup)
    np.testing.assert_allclose(actual, expected)

  def test_load_blast_output(self):
    input_test_fasta = (
        '>accession="ACCESSION"\tlabels="GO:101010,EC:9.9.9.9"\n'
        "ADE\n"
        '>accession="ACCESSION2"\tlabels="EC:1.2.-.-"\n'
        "WWWW\n")
    test_fasta_filename = _write_to_file(input_test_fasta)
    ground_truth_test = baseline_utils.load_ground_truth(test_fasta_filename)

    input_train_fasta = (
        '>accession="MATCHACCESSION"\tlabels="GO:101010,EC:9.9.9.9,Pfam:PF12345"\n'
        "ADE\n")
    train_fasta_filename = _write_to_file(input_train_fasta)
    ground_truth_train = baseline_utils.load_ground_truth(train_fasta_filename)

    # Missing second sequence in ground truth.
    input_blast = (
        'accession="ACCESSION"\taccession="MATCHACCESSION"\t82.456\t57\t10\t0\t1\t57\t1\t57\t6.92e-21\t79.3\n'
    )
    input_label_vocab = np.array(
        ["EC:1.2.-.-", "EC:9.9.9.9", "GO:101010", "Pfam:PF12345"])
    blast_filename = _write_to_file(input_blast)
    actual = baseline_utils.load_blast_output(
        filename=blast_filename,
        label_vocab=input_label_vocab,
        test_data_ground_truth=ground_truth_test,
        training_data_ground_truth=ground_truth_train)

    expected = pd.DataFrame({
        "sequence_name": ["ACCESSION", "ACCESSION2"],
        "closest_sequence": ["MATCHACCESSION", float("nan")],
        "true_label": [{"GO:101010", "EC:9.9.9.9"}, {"EC:1.2.-.-"}],
        "predicted_label": [{"GO:101010", "EC:9.9.9.9", "Pfam:PF12345"},
                            frozenset()],
        "percent_seq_identity": [82.456, float("nan")],
        "e_value": [6.92e-21, float("nan")],
        "bit_score": [79.3, 0.0],
    })

    test_util.assert_dataframes_equal(
        self,
        # Assert dataframes equal except for predictions column.
        # Rely on unit testing for predictions column instead to increase
        # test clarity. See test_blast_row_to_confidence_array above.
        actual.drop(columns=["predictions"]),
        expected,
        nan_equals_nan=True)

  def test_limit_set_of_labels(self):
    # Set up input data.
    input_df = pd.DataFrame(
        {"labels": [frozenset(["a"]), frozenset(["a", "b"])]})
    acceptable_labels = frozenset(["a"])
    column_to_limit = "labels"

    # Assert input dataframe was not modified later on, so save a copy.
    input_df_copy = input_df.copy()

    # Compute actual.
    actual = baseline_utils.limit_set_of_labels(input_df, acceptable_labels,
                                                column_to_limit)
    expected = pd.DataFrame({"labels": [frozenset(["a"]), frozenset(["a"])]})

    # Test assertions.
    test_util.assert_dataframes_equal(self, actual, expected)

    # Assert input dataframe was not modified.
    test_util.assert_dataframes_equal(self, input_df, input_df_copy)

  def test_limit_labels_for_label_normalizer(self):
    input_label_normalizer = {
        "a": ["a", "b", "c"],
        "DDDD": ["XXXX"],
        "b": ["YYYY", "b"]
    }
    input_acceptable_labels = frozenset(["a", "b"])

    actual = baseline_utils.limit_labels_for_label_normalizer(
        input_label_normalizer, input_acceptable_labels)
    expected = {"a": ["a", "b"], "b": ["b"]}

    self.assertDictEqual(actual, expected)

  @parameterized.named_parameters(
      dict(
          testcase_name="one sequence, one label row, no extra nonlabel entries",
          interproscan_output="""accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	TIGRFAM	TIGR00506	ribB: 3,4-dihydroxy-2-butanone-4-phosphate synthase	13	209	1.5E-86	T	21-10-2019	IPR000422	3,4-dihydroxy-2-butanone 4-phosphate synthase, RibB	GO:0008686|GO:0009231""",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={
              "B7UIV5": {"GO:0008686", "GO:0009231"}
          },
      ),
      dict(
          testcase_name="one sequence, one label row, extra nonlabel entries",
          interproscan_output="""accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	TIGRFAM	TIGR00506	ribB: 3,4-dihydroxy-2-butanone-4-phosphate synthase	13	209	1.5E-86	T	21-10-2019	IPR000422	3,4-dihydroxy-2-butanone 4-phosphate synthase, RibB	GO:0008686|GO:0009231
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	Gene3D	G3DSA:3.90.870.10		1	217	5.1E-95T21-10-2019""",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={"B7UIV5": {"GO:0008686", "GO:0009231"}},
      ),
      dict(
          testcase_name="one sequence, no labels",
          interproscan_output="""accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	Gene3D	G3DSA:3.90.870.10		1	217	5.1E-95T21-10-2019""",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={"B7UIV5": set()},
      ),
      dict(
          testcase_name="one sequence, multiple label rows, extra nonlabel entries",
          interproscan_output="""accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	PANTHER	PTHR21327:SF38		1	217	8.2E-126	T21-10-2019
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	TIGRFAM	TIGR00506	ribB: 3,4-dihydroxy-2-butanone-4-phosphate synthase	13	209	1.5E-86	T	21-10-2019	IPR000422	3,4-dihydroxy-2-butanone 4-phosphate synthase, RibB	GO:0008686|GO:0009231
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	Hamap	MF_00180	3,4-dihydroxy-2-butanone 4-phosphate synthase [ribB].	11	213	43.238	T	21-10-2019	IPR000422	3,4-dihydroxy-2-butanone 4-phosphate synthase, RibB	GO:0008686|GO:0009231
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	Gene3D	G3DSA:3.90.870.10		1	217	5.1E-95T21-10-2019
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	SUPERFAMILY	SSF55821		7	213	5.95E-86T	21-10-2019	IPR017945	DHBP synthase RibB-like alpha/beta domain superfamily
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	Pfam	PF00926	3,4-dihydroxy-2-butanone 4-phosphate synthase	17	208	1.7E-82	T	21-10-2019	IPR000422	3,4-dihydroxy-2-butanone 4-phosphate synthase, RibB	GO:0008686|GO:0009231
accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	PANTHER	PTHR21327		1	217	8.2E-126	T21-10-2019""",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={"B7UIV5": {"GO:0008686", "GO:0009231"}},
      ),
      dict(
          testcase_name="two sequences, one has labels",
          interproscan_output="""accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	PANTHER	PTHR21327		1	217	8.2E-126	T21-10-2019
    accession="Q5SMK6"	e9a286a263b71156fcf0cfebc12caec6	360	CDD	cd00143	PP2Cc	64	325	6.91138E-87	T	21-10-2019	IPR001932	PPM-type phosphatase domain	GO:0003824""",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED
>accession="Q5SMK6"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={
              "B7UIV5": set(),
              "Q5SMK6": {"GO:0003824"},
          },
      ),
      dict(
          testcase_name="two sequences, both have labels",
          interproscan_output="""accession="B7UIV5"	74c763abf8567dfb6f4f83a4e0a31454	217	TIGRFAM	TIGR00506	ribB: 3,4-dihydroxy-2-butanone-4-phosphate synthase	13	209	1.5E-86	T	21-10-2019	IPR000422	3,4-dihydroxy-2-butanone 4-phosphate synthase, RibB	GO:0008686|GO:0009231
    accession="Q5SMK6"	e9a286a263b71156fcf0cfebc12caec6	360	CDD	cd00143	PP2Cc	64	325	6.91138E-87	T	21-10-2019	IPR001932	PPM-type phosphatase domain	GO:0003824""",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED
>accession="Q5SMK6"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={
              "B7UIV5": {"GO:0008686", "GO:0009231"},
              "Q5SMK6": {"GO:0003824"},
          },
      ),
      dict(
          testcase_name="sequence in ground truth that is missing in interproscan output",
          interproscan_output="",
          input_ground_truth_test_fasta=""">accession="B7UIV5"\tlabels="GO:101010"
NOT_USED""",
          expected_predicted_labels_per_seq={
              "B7UIV5": set(),
          },
      ),
  )
  def test_load_interproscan_output(self, interproscan_output,
                                    input_ground_truth_test_fasta,
                                    expected_predicted_labels_per_seq):
    # Set up inputs.
    input_file = _write_to_file(interproscan_output)

    input_test_fasta_filename = _write_to_file(input_ground_truth_test_fasta)

    input_ground_truth_test = baseline_utils.load_ground_truth(
        input_test_fasta_filename)

    # Compute actual results.
    actual_interproscan_output = baseline_utils.load_interproscan_output(
        test_data_ground_truth=input_ground_truth_test,
        interproscan_output_filename=input_file)

    # Assertions.
    expected_df_length = len(
        set(
            list(expected_predicted_labels_per_seq.keys()) +
            input_ground_truth_test.sequence_name.values))
    self.assertLen(actual_interproscan_output, expected_df_length)

    for row in actual_interproscan_output.itertuples():
      self.assertIn(row.sequence_name, expected_predicted_labels_per_seq)
      self.assertSetEqual(row.predicted_label,
                          expected_predicted_labels_per_seq[row.sequence_name])


if __name__ == "__main__":
  absltest.main()


================================================
FILE: colab_evaluation.py
================================================
# coding=utf-8
# Copyright 2020 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Version of evaluation utilities intended for use in lower memory environments, such as colab.

This version of the evaluation code leverages the fact that the vast majority of example-label predictions 
are essentially zero, and so only contribute to false negatives. It therefore represents the data in "tidy
format" with one row per example-label pair and excludes example-label pairs below a defined threshold.
"""

import numpy as np
import pandas as pd
import sklearn
import tqdm

import inference
import utils
import evaluation


def read_blast_table(filename):
    """Read a table of BLAST results."""

    blast_out = pd.read_table(filename,
                              names=[
                                  'up_id', 'target', 'pc_identity',
                                  'pc_positives', 'alignment_length',
                                  'mismatches', 'gap_opens', 'q. start',
                                  'q. end', 's. start', 'evalue', 'bit_score'
                              ])

    def extract_accession(long_string):
        """Strip out accession decoration from a parameter"""
        return long_string.replace('accession="', '').replace('"', '')

    blast_out['up_id'] = blast_out['up_id'].map(extract_accession)
    blast_out['target'] = blast_out['target'].map(extract_accession)
    blast_out = blast_out[[
        'up_id', 'target', 'pc_identity', 'alignment_length', 'bit_score'
    ]]
    return blast_out


def stats_by_group(df):
    """Calculate statistics from a groupby'ed dataframe with TPs,FPs and FNs."""

    EPSILON = 1e-10
    result = df[['tp', 'fp', 'fn']].sum().reset_index().assign(
        precision=lambda x: (x['tp'] + EPSILON) /
        (x['tp'] + x['fp'] + EPSILON),
        recall=lambda x: (x['tp'] + EPSILON) /
        (x['tp'] + x['fn'] + EPSILON)).assign(
            f1=lambda x: 2 * x['precision'] * x['recall'] /
            (x['precision'] + x['recall'] + EPSILON),
            count=lambda x: x['tp'] + x['fn'])
    result['proportion'] = result['count'] / np.sum(result['count'])
    result['proportion_text'] = (result['proportion'] *
                                 100).round(2).astype(str) + "%"
    return result


def get_stats(df):
    """Calculate statistics from a dataframe with TPs,FPs and FNs."""
    df['dummy_group'] = 'all'
    data = stats_by_group(df.groupby('dummy_group')).drop(
        columns=['dummy_group', 'proportion', 'proportion_text'])
    return data


def apply_threshold_and_return_stats(predictions_df,
                                     ground_truth_df,
                                     threshold=0.5,
                                     grouping=None):
    """Given predictions, ground truth and a threshold get statistics."""
    calls = assign_tp_fp_fn(predictions_df, ground_truth_df, threshold)
    if grouping:
        calls['group'] = calls['label'].map(grouping)
        return stats_by_group(
            calls.groupby("group")).assign(threshold=threshold)
    else:
        return get_stats(calls).assign(threshold=threshold)


def batch_inferences(iterator, batch_size):
    """Yield batches of seq_ids and predictions matrices from an iterator."""
    counter = 0
    predictions = []
    seq_ids = []
    while True:
        try:
            inference = next(iterator)
        except StopIteration:
            if len(seq_ids) > 0:
                yield seq_ids, np.vstack(predictions)
            return
        seq_ids.append(inference[0])
        predictions.append(inference[1])
        counter += 1
        if counter == batch_size:
            yield seq_ids, np.vstack(predictions)
            predictions = []
            seq_ids = []
            counter = 0


def batched_inferences_from_files(shard_names, batch_size=100):
    """Iterate through TFRecord files of inferences and yield batches."""
    for file_name in tqdm.tqdm(shard_names, position=0):
        inference_iterator = inference.parse_shard(file_name)
        batched_iterator = batch_inferences(inference_iterator, batch_size)
        while True:
            try:
                yield next(batched_iterator)
            except StopIteration:
                break


def batched_inferences_from_dir(shard_dir_path, batch_size=100):
    """Iterate through directory of inference TFRecord files and yield batches."""
    files_to_process = utils.absolute_paths_of_files_in_dir(shard_dir_path)
    return batched_inferences_from_files(files_to_process, batch_size)


def _make_tidy_df_from_seq_names_and_prediction_array(
        sequence_names, predictions_array, vocab,
        min_decision_threshold=1e-20):
    """Given a list of sequences and a matrix of prediction values, yield a tidy dataframe of predictions."""
    up_ids = []
    labels = []
    values = []

    for i in range(len(sequence_names)):
        up_id = sequence_names[i]
        preds = predictions_array[i, :]

        for vocab_index in np.argwhere(preds > min_decision_threshold):
            vocab_index = vocab_index[0]
            up_ids.append(up_id)
            labels.append(vocab[vocab_index])
            values.append(preds[vocab_index])
    return pd.DataFrame({"up_id": up_ids, "label": labels, "value": values})


def get_normalized_inference_results(shard_dir_path,
                                     vocab,
                                     label_normalizer,
                                     min_decision_threshold=1e-20):
    """Take a directory of sharded inferences and output a tidy and normalized dataframe.

    Inferences are in the format defined in inference.py
    
    Args:
        shard_dir_path: directory of TFrecord inference shards
        vocab: a list of vocabulary items
        label_normalizer: a dictionary mapping vocabulary items to their parents
        min_decision_threshold: a threshold reflecting the minimum we will ever be 
            able to use to call a positive in subsequent analysis. Higher
            values use less RAM at the expense of lower maximum sensitivity.
        
    Returns:
        A pandas dataframe with one row per example-label (provided value > min_decision_threshold) and the 
        associated value from the neural network.
    """
    batches = batched_inferences_from_dir(shard_dir_path)
    dfs = []
    for seq_names, confidences in batches:
        normed_confidences = evaluation.normalize_confidences(
            confidences, vocab, label_normalizer)
        dfs.append(
            _make_tidy_df_from_seq_names_and_prediction_array(
                seq_names,
                normed_confidences,
                vocab,
                min_decision_threshold=min_decision_threshold))
    return pd.concat(dfs)


def make_tidy_df_from_ground_truth(ground_truth):
    """Create a tidy dataframe from ground truth data."""
    up_ids = []
    labels = []

    for i in tqdm.tqdm(ground_truth.index, position=0):
        up_id = ground_truth['sequence_name'][i]
        for vocab_entry in ground_truth['true_label'][i]:
            up_ids.append(up_id)
            labels.append(vocab_entry)
    return pd.DataFrame({"up_id": up_ids, "label": labels, "gt": True})


def merge_predictions_and_ground_truth(predictions_df, ground_truth_df):
    """Perform an outer join of predictions and ground truth, then set all empty values to False."""
    combined = predictions_df.merge(ground_truth_df,
                                    how="outer",
                                    suffixes=("_pred", "_gt"),
                                    left_on=["label", "up_id"],
                                    right_on=["label", "up_id"])
    combined = combined.fillna(False)
    return combined


def get_pr_curve_df(predictions_df,
                    ground_truth_df,
                    grouping=None,
                    filtered=True):
    """Given predictions and ground truth in tidy format, yield a precision recall curve.
    
    Args:
        predictions_df: predictions in tidy format
        ground_truth_df: ground truth in tidy format
        grouping: optional dictionary mapping sequence names to categories
        filtered: whether to remove almost redundant points on PR curve
    """
    combined = merge_predictions_and_ground_truth(predictions_df,
                                                  ground_truth_df)
    if grouping == None:
        to_process = {'all': combined}.items()
    else:
        combined['group'] = combined['label'].map(grouping)
        to_process = combined.groupby('group')

    del combined
    output_dfs = []
    for group_name, group in tqdm.tqdm(to_process, position=0):
        precisions, recalls, thresholds = sklearn.metrics.precision_recall_curve(
            group['gt'], group['value'])
        precisions = precisions[:-1]
        recalls = recalls[:-1]
        if filtered:
            precisions, recalls, thresholds = filter_pr_curve(
                precisions, recalls, thresholds)
        output_dfs.append(
            pd.DataFrame({
                'group':
                group_name,
                'precision':
                precisions,
                'recall':
                recalls,
                'threshold':
                thresholds,
                'f1':
                2 * precisions * recalls / (precisions + recalls)
            }))
    return pd.concat(output_dfs)


def filter_pr_curve(precisions, recalls, thresholds, resolution=1e-4):
    """Filters out imperceptible shifts in a PR curve."""
    last_precision = None
    last_recall = None
    new_precisions = []
    new_recalls = []
    new_thresholds = []
    for i in range(len(precisions)):
        if last_precision is None or abs(recalls[i] -
                                         last_recall) >= resolution:
            new_precisions.append(precisions[i])
            last_precision = precisions[i]
            new_recalls.append(recalls[i])
            last_recall = recalls[i]
            new_thresholds.append(thresholds[i])
    return np.array(new_precisions), np.array(new_recalls), np.array(
        new_thresholds)


def assign_tp_fp_fn(predictions_df, ground_truth_df, threshold):
    """Return a new predictions dataframe where each row is assigned as either a TP, FP or FN."""
    combined = merge_predictions_and_ground_truth(predictions_df,
                                                  ground_truth_df)

    combined['tp'] = (combined['gt'] == True) & (combined['value'] > threshold)
    combined['fp'] = (combined['gt'] == False) & (combined['value'] >
                                                  threshold)
    combined['fn'] = (combined['gt'] == True) & (combined['value'] < threshold)
    return combined

================================================
FILE: colab_evaluation_test.py
================================================
# coding=utf-8
# Copyright 2020 The Google Research Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tests for module colab_evaluation.py."""

import gzip
import os
import time
import math

from absl import flags
from absl.testing import absltest
from absl.testing import parameterized
import numpy as np
import pandas as pd
import colab_evaluation
import inference
import tensorflow.compat.v1 as tf

FLAGS = flags.FLAGS


class ColabEvaluationTest(parameterized.TestCase):
    def _generate_random_inferences(self, n):
        serialized_inferences = []
        accessions_list = []
        activations_list = []

        for _ in range(n):
            accession = f"ACCESSION_{time.time()}"
            activations = np.random.rand(100)
            accessions_list.append(accession)
            activations_list.append(activations)
            serialized_inferences.append(
                inference.serialize_inference_result(accession, activations))

        return serialized_inferences, accessions_list, activations_list

    @parameterized.parameters([{'batch_size': 1}, {'batch_size': 9}])
    def test_batched_inferences_from_dir(self, batch_size, num_examples=100):

        # Create input inference results.

        serialized_inferences, accessions_list, activations_list = self._generate_random_inferences(
            num_examples)

        shard_1_contents = b"\n".join(serialized_inferences[0:60])
        shard_2_contents = b"\n".join(serialized_inferences[60:])

        shard_dir = self.create_tempdir()

        shard_1_filename = shard_dir.create_file('shard_1').full_path
        shard_2_filename = shard_dir.create_file('shard_2').full_path

        # Write contents to a gzipped file.
        with tf.io.gfile.GFile(shard_1_filename, 'wb') as f:
            with gzip.GzipFile(fileobj=f, mode='wb') as f_gz:
                f_gz.write(shard_1_contents)

        with tf.io.gfile.GFile(shard_2_filename, 'wb') as f:
            with gzip.GzipFile(fileobj=f, mode='wb') as f_gz:
                f_gz.write(shard_2_contents)

        # Read these shards.

        iterator = colab_evaluation.batched_inferences_from_dir(
            shard_dir.full_path, batch_size=batch_size)

        actual = list(iterator)

        # Check output.

        self.assertEqual(len(actual), math.ceil(num_examples / batch_size))
        self.assertEqual(actual[0][0][0], accessions_list[0])
        if batch_size > 1:
            self.assertEqual(actual[1][0][1], accessions_list[batch_size + 1])
        np.testing.assert_equal(actual[0][1][0], activations_list[0])
        if batch_size > 1:
            np.testing.assert_equal(actual[1][1][1],
                                    activations_list[batch_size + 1])

    def test_make_tidy_df_from_seq_names_and_prediction_array(self):
        vocab = ["ENTRY0", "ENTRY1", "ENTRY2"]
        sequence_names = ['SEQ0', 'SEQ1']
        predictions_array = np.array([[0.1, 0.9, 0.5], [1, 1, 1]])
        min_decision_threshold = 0.4
        actual_df = colab_evaluation._make_tidy_df_from_seq_names_and_prediction_array(
            sequence_names,
            predictions_array,
            vocab,
            min_decision_threshold=min_decision_threshold)
        expected_df = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ0', 'SEQ1', 'SEQ1', 'SEQ1'],
            'label': ['ENTRY1', 'ENTRY2', 'ENTRY0', 'ENTRY1', 'ENTRY2'],
            'value': [0.9, 0.5, 1.0, 1.0, 1.0]
        })
        pd.testing.assert_frame_equal(actual_df, expected_df)

    def test_make_tidy_df_from_ground_truth(self):
        input_df = pd.DataFrame({
            'sequence_name': ['SEQ0', 'SEQ1', 'SEQ2', 'SEQ3'],
            'true_label': [['ENTRY1'], ['ENTRY1', 'ENTRY2'], [], ['ENTRY6']]
        })
        actual_df = colab_evaluation.make_tidy_df_from_ground_truth(input_df)
        expected_df = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ1', 'SEQ1', 'SEQ3'],
            'label': ['ENTRY1', 'ENTRY1', 'ENTRY2', 'ENTRY6'],
            'gt': [True, True, True, True]
        })
        pd.testing.assert_frame_equal(actual_df, expected_df)

    def test_merge_predictions_and_ground_truth(self):
        pred = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ0', 'SEQ1', 'SEQ1', 'SEQ1'],
            'label': ['ENTRY1', 'ENTRY2', 'ENTRY0', 'ENTRY1', 'ENTRY2'],
            'value': [0.9, 0.5, 1.0, 1.0, 1.0]
        })
        gt = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ1', 'SEQ1', 'SEQ3'],
            'label': ['ENTRY1', 'ENTRY1', 'ENTRY2', 'ENTRY6'],
            'gt': [True, True, True, True]
        })
        actual_df = colab_evaluation.merge_predictions_and_ground_truth(
            pred, gt)
        expected_df = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ0', 'SEQ1', 'SEQ1', 'SEQ1', 'SEQ3'],
            'label':
            ['ENTRY1', 'ENTRY2', 'ENTRY0', 'ENTRY1', 'ENTRY2', 'ENTRY6'],
            'value': [0.9, 0.5, 1.0, 1.0, 1.0, False],
            'gt': [True, False, False, True, True, True]
        })
        pd.testing.assert_frame_equal(actual_df, expected_df)

    def test_get_pr_curve_df(self):
        pred = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ0', 'SEQ1', 'SEQ1', 'SEQ1'],
            'label': ['ENTRY1', 'ENTRY2', 'ENTRY0', 'ENTRY1', 'ENTRY2'],
            'value': [0.9, 0.5, 1.0, 1.0, 1.0]
        })
        gt = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ1', 'SEQ1', 'SEQ3'],
            'label': ['ENTRY1', 'ENTRY1', 'ENTRY2', 'ENTRY6'],
            'gt': [True, True, True, True]
        })
        pr_curve = colab_evaluation.get_pr_curve_df(pred, gt, filtered=False)

        np.testing.assert_almost_equal(pr_curve['recall'],
                                       np.array([1, 0.75, 0.75, .5]))
        np.testing.assert_almost_equal(
            pr_curve['precision'], np.array([0.6666667, 0.6, 0.75, 0.6666667]))
        np.testing.assert_almost_equal(
            pr_curve['f1'], np.array([0.8, 0.6666667, 0.75, 0.5714286]))

    def test_assign_tp_fp_fn(self):
        pred = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ0', 'SEQ1', 'SEQ1', 'SEQ1'],
            'label': ['ENTRY1', 'ENTRY2', 'ENTRY0', 'ENTRY1', 'ENTRY2'],
            'value': [0.9, 0.5, 1.0, 1.0, 1.0]
        })
        gt = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ1', 'SEQ1', 'SEQ3'],
            'label': ['ENTRY1', 'ENTRY1', 'ENTRY2', 'ENTRY6'],
            'gt': [True, True, True, True]
        })
        tp_fp_fn = colab_evaluation.assign_tp_fp_fn(pred, gt, threshold=0.5)

        expected = pd.DataFrame({
            'tp': [True, False, False, True, True, False],
            'fp': [False, False, True, False, False, False],
            'fn': [False, False, False, False, False, True]
        })
        actual = tp_fp_fn.loc[:, ["tp", "fp", "fn"]]
        pd.testing.assert_frame_equal(expected, actual)


    def test_apply_threshold_and_return_stats(self):
        pred = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ0', 'SEQ1', 'SEQ1', 'SEQ1'],
            'label': ['ENTRY1', 'ENTRY2', 'ENTRY0', 'ENTRY1', 'ENTRY2'],
            'value': [0.9, 0.5, 1.0, 1.0, 1.0]
        })
        gt = pd.DataFrame({
            'up_id': ['SEQ0', 'SEQ1', 'SEQ1', 'SEQ3'],
            'label': ['ENTRY1', 'ENTRY1', 'ENTRY2', 'ENTRY6'],
            'gt': [True, True, True, True]
        })
        actual = colab_evaluation.apply_threshold_and_return_stats(pred,gt,grouping = {"ENTRY0":'A',"ENTRY1":'A',"ENTRY2":'A',"ENTRY6":'A'})
        expected = pd.DataFrame({
            'group': ['A'],
            'tp': [3.0],
            'fp': [1.0],
            'fn': [1.0],
            'precision': [0.75],
            'recall': [0.75],
            'f1': [0.75],
            'count': [4.0],
            'proportion': [1.0],
            'proportion_text': ['100.0%'],
            'threshold': [0.5]
        })
        pd.testing.assert_frame_equal(actual,expected, check_dtype=False)


    def test_read_blast_table(self):
        actual = colab_evaluation.read_blast_table("testdata/blast.tsv")
        expected = pd.DataFrame({'up_id': ['ABC'], 'target': ['DEF'], 'pc_identity': [50], 'alignment_length': [100], 'bit_score': [500]})
        pd.testing.assert_frame_equal(actual, expected)

if __name__ == '__main__':
    absltest.main()


================================================
FILE: colabs/Class_Activation_Mapping.ipynb
================================================
{
  "nbformat": 4,
  "nbformat_minor": 2,
  "metadata": {
    "accelerator": "GPU",
    "colab": {
      "name": "Copy of Proteinfer CAM v3",
      "provenance": [],
      "collapsed_sections": []
    },
    "kernelspec": {
      "display_name": "Python 3",
      "name": "python3"
    }
  },
  "cells": [
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "# Licensed under the Apache License, Version 2.0 (the \"License\");\r\n",
        "# you may not use this file except in compliance with the License.\r\n",
        "# You may obtain a copy of the License at\r\n",
        "#\r\n",
        "# https://www.apache.org/licenses/LICENSE-2.0\r\n",
        "#\r\n",
        "# Unless required by applicable law or agreed to in writing, software\r\n",
        "# distributed under the License is distributed on an \"AS IS\" BASIS,\r\n",
        "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n",
        "# See the License for the specific language governing permissions and\r\n",
        "# limitations under the License."
      ],
      "outputs": [],
      "metadata": {
        "id": "WkkXLGBrycfx"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# ProteInfer Class Activation Mapping (CAM)\n",
        "\n"
      ],
      "metadata": {
        "id": "XdR-yrNv9Fgk"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## Initial setup (code/data download)"
      ],
      "metadata": {
        "id": "Rqbcum1HM8aL"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "!git clone https://github.com/google-research/proteinfer \r\n",
        "\r\n",
        "%cd proteinfer\r\n",
        "!pip3 install -qr  requirements.txt\r\n",
        "\r\n",
        "import pandas as pd\r\n",
        "import tensorflow\r\n",
        "import inference\r\n",
        "import parenthood_lib\r\n",
        "import baseline_utils,subprocess\r\n",
        "import shlex\r\n",
        "import tqdm \r\n",
        "import sklearn\r\n",
        "import numpy as np\r\n",
        "import utils\r\n",
        "import colab_evaluation\r\n",
        "import plotly.express as px\r\n",
        "import seaborn as sns\r\n",
        "\r\n",
        "from plotnine import ggplot, geom_point, geom_point, geom_line, aes, stat_smooth, facet_wrap, xlim,coord_cartesian,theme_bw,labs,ggsave\r\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Cloning into 'proteinfer'...\n",
            "remote: Enumerating objects: 491, done.\u001b[K\n",
            "remote: Counting objects: 100% (214/214), done.\u001b[K\n",
            "remote: Compressing objects: 100% (177/177), done.\u001b[K\n",
            "remote: Total 491 (delta 96), reused 49 (delta 20), pack-reused 277\u001b[K\n",
            "Receiving objects: 100% (491/491), 11.55 MiB | 8.51 MiB/s, done.\n",
            "Resolving deltas: 100% (235/235), done.\n",
            "/content/proteinfer\n",
            "\u001b[K     |████████████████████████████████| 99 kB 3.4 MB/s \n",
            "\u001b[K     |████████████████████████████████| 2.3 MB 30.7 MB/s \n",
            "\u001b[K     |████████████████████████████████| 10.8 MB 26.8 MB/s \n",
            "\u001b[K     |████████████████████████████████| 2.8 MB 29.6 MB/s \n",
            "\u001b[K     |████████████████████████████████| 50 kB 6.4 MB/s \n",
            "\u001b[K     |████████████████████████████████| 59 kB 6.2 MB/s \n",
            "\u001b[K     |████████████████████████████████| 89 kB 7.5 MB/s \n",
            "\u001b[K     |████████████████████████████████| 56 kB 4.8 MB/s \n",
            "\u001b[K     |████████████████████████████████| 17.3 MB 48 kB/s \n",
            "\u001b[K     |████████████████████████████████| 10.5 MB 34.2 MB/s \n",
            "\u001b[K     |████████████████████████████████| 107 kB 50.4 MB/s \n",
            "\u001b[K     |████████████████████████████████| 13.1 MB 35.7 MB/s \n",
            "\u001b[K     |████████████████████████████████| 4.4 MB 33.5 MB/s \n",
            "\u001b[K     |████████████████████████████████| 226 kB 48.2 MB/s \n",
            "\u001b[K     |████████████████████████████████| 6.8 MB 35.5 MB/s \n",
            "\u001b[K     |████████████████████████████████| 110.5 MB 50 kB/s \n",
            "\u001b[K     |████████████████████████████████| 411.0 MB 22 kB/s \n",
            "\u001b[K     |████████████████████████████████| 503 kB 47.9 MB/s \n",
            "\u001b[K     |████████████████████████████████| 103 kB 54.1 MB/s \n",
            "\u001b[K     |████████████████████████████████| 45 kB 3.1 MB/s \n",
            "\u001b[K     |████████████████████████████████| 328 kB 42.4 MB/s \n",
            "\u001b[K     |████████████████████████████████| 63 kB 1.6 MB/s \n",
            "\u001b[K     |████████████████████████████████| 9.5 MB 38.7 MB/s \n",
            "\u001b[K     |████████████████████████████████| 3.8 MB 38.5 MB/s \n",
            "\u001b[?25h  Building wheel for absl-py (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
            "  Building wheel for gast (setup.py) ... \u001b[?25l\u001b[?25hdone\n",
            "\u001b[31mERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.\n",
            "xarray 0.18.2 requires numpy>=1.17, but you have numpy 1.16.2 which is incompatible.\n",
            "tensorflow-probability 0.13.0 requires gast>=0.3.2, but you have gast 0.2.2 which is incompatible.\n",
            "tensorflow-metadata 1.2.0 requires absl-py<0.13,>=0.9, but you have absl-py 0.7.1 which is incompatible.\n",
            "spacy 2.2.4 requires tqdm<5.0.0,>=4.38.0, but you have tqdm 4.28.1 which is incompatible.\n",
            "pyerfa 2.0.0 requires numpy>=1.17, but you have numpy 1.16.2 which is incompatible.\n",
            "pyarrow 3.0.0 requires numpy>=1.16.6, but you have numpy 1.16.2 which is incompatible.\n",
            "panel 0.12.1 requires tqdm>=4.48.0, but you have tqdm 4.28.1 which is incompatible.\n",
            "kapre 0.3.5 requires numpy>=1.18.5, but you have numpy 1.16.2 which is incompatible.\n",
            "kapre 0.3.5 requires tensorflow>=2.0.0, but you have tensorflow 1.15.4 which is incompatible.\n",
            "jaxlib 0.1.70+cuda110 requires numpy>=1.18, but you have numpy 1.16.2 which is incompatible.\n",
            "jax 0.2.19 requires numpy>=1.18, but you have numpy 1.16.2 which is incompatible.\n",
            "google-colab 1.0.0 requires astor~=0.8.1, but you have astor 0.7.1 which is incompatible.\n",
            "google-colab 1.0.0 requires six~=1.15.0, but you have six 1.12.0 which is incompatible.\n",
            "google-api-python-client 1.12.8 requires six<2dev,>=1.13.0, but you have six 1.12.0 which is incompatible.\n",
            "google-api-core 1.26.3 requires six>=1.13.0, but you have six 1.12.0 which is incompatible.\n",
            "fbprophet 0.7.1 requires tqdm>=4.36.1, but you have tqdm 4.28.1 which is incompatible.\n",
            "datascience 0.10.6 requires folium==0.2.1, but you have folium 0.8.3 which is incompatible.\n",
            "cupy-cuda101 9.1.0 requires numpy>=1.17, but you have numpy 1.16.2 which is incompatible.\n",
            "astropy 4.3.1 requires numpy>=1.17, but you have numpy 1.16.2 which is incompatible.\n",
            "albumentations 0.1.12 requires imgaug<0.2.7,>=0.2.5, but you have imgaug 0.2.9 which is incompatible.\u001b[0m\n"
          ]
        }
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "UbK8U6SEKqUU",
        "outputId": "b41adc0f-c74e-4a26-fd34-93ad42ee4989"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "!wget -qN https://storage.googleapis.com/brain-genomics-public/research/proteins/proteinfer/models/zipped_models/noxpnd_cnn_swissprot_ec_random_swiss-cnn_for_swissprot_ec_random-13685140.tar.gz\r\n",
        "!tar xzf noxpnd_cnn_swissprot_ec_random_swiss-cnn_for_swissprot_ec_random-13685140.tar.gz\r\n",
        "!wget -qN https://storage.googleapis.com/brain-genomics-public/research/proteins/proteinfer/colab_support/parenthood.json.gz\r\n",
        "!wget -qN https://storage.googleapis.com/brain-genomics-public/research/proteins/proteinfer/blast_baseline/fasta_files/SWISSPROT_RANDOM_EC/eval_test.fasta\r\n",
        "\r\n"
      ],
      "outputs": [],
      "metadata": {
        "id": "T7OLyg9DF3uw"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "def get_ec_num_mapping():\r\n",
        "  tree = ET.parse('enzyme-data.xml')\r\n",
        "  root = tree.getroot()\r\n",
        "  rows = root[0][3].findall('row')\r\n",
        "  rows = root.findall(\".//field[@name='accepted_name']..\")\r\n",
        "  ec_nums = {}\r\n",
        "  for row in rows:\r\n",
        "      ec_num = row.find(\".//*[@name='ec_num']\").text\r\n",
        "      name = row.find(\".//*[@name='accepted_name']\").text\r\n",
        "      try:\r\n",
        "        ec_nums[ec_num]=name\r\n",
        "      except TypeError:\r\n",
        "        continue\r\n",
        "  return ec_nums\r\n",
        "\r\n",
        "def download_dataset():\r\n",
        "  total = 13\r\n",
        "  file_shard_names = ['https://storage.googleapis.com/brain-genomics-public/research/proteins/proteinfer/datasets/swissprot/random/test-{:05d}-of-{:05d}.tfrecord'.format(i,total) for i in range(total)]\r\n",
        "\r\n",
        "  for shard_name in tqdm.tqdm(file_shard_names, position=0,desc=\"Downloading\"):\r\n",
        "    subprocess.check_output(shlex.split(f'wget {shard_name}'))\r\n",
        "  return "
      ],
      "outputs": [],
      "metadata": {
        "id": "ERnBaCG_O00k"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "\r\n",
        "import matplotlib as mpl\r\n",
        "mpl.rcParams['figure.dpi'] = 100\r\n",
        "!wget -qN https://www.enzyme-database.org/downloads/enzyme-data.xml.gz\r\n",
        "!gunzip -f enzyme-data.xml.gz\r\n",
        "\r\n",
        "\r\n",
        "import xml.etree.ElementTree as ET\r\n",
        "\r\n",
        "\r\n",
        "ec_nums = get_ec_num_mapping()\r\n",
        "download_dataset()"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "Downloading: 100%|██████████| 13/13 [00:04<00:00,  2.02it/s]\n"
          ]
        }
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "FrEsBu1CA7Ut",
        "outputId": "c4117fd4-ff22-4e67-cde9-55e7ddc20721"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "##Read in the test dataset"
      ],
      "metadata": {
        "id": "KWu0UovMMzEC"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "import protein_dataset\r\n",
        "import tqdm\r\n",
        "import numpy as np\r\n",
        "sequence_iterator = protein_dataset.yield_examples(\"./test*.tfrecord\")\r\n",
        "sequences = []\r\n",
        "labels = []\r\n",
        "ids = []\r\n",
        "for example in tqdm.tqdm(sequence_iterator):\r\n",
        "  ids.append(example[protein_dataset.SEQUENCE_ID_KEY])\r\n",
        "  sequences.append(example[protein_dataset.SEQUENCE_KEY])\r\n",
        "  labels.append(example[protein_dataset.LABEL_KEY])\r\n",
        "\r\n",
        "# If we want to optimise for inference speed we should sort the dataset by\r\n",
        "# sequence length:\r\n",
        "seq_lengths = [len(x) for x in sequences]\r\n",
        "indices = np.argsort(-np.array(seq_lengths)).tolist()\r\n",
        "\r\n",
        "ids = [ids[indices[x]] for x in range(len(indices))]\r\n",
        "sequences = [sequences[indices[x]] for x in range(len(indices))]\r\n",
        "labels = [set(labels[indices[x]]) for x in range(len(indices))]"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "\r0it [00:00, ?it/s]WARNING: Logging before flag parsing goes to stderr.\n",
            "W0909 12:36:54.501479 140666907211648 deprecation.py:323] From /content/proteinfer/protein_dataset.py:290: DatasetV1.make_one_shot_iterator (from tensorflow.python.data.ops.dataset_ops) is deprecated and will be removed in a future version.\n",
            "Instructions for updating:\n",
            "Use `for ... in dataset:` to iterate over a dataset. If using `tf.estimator`, return the `Dataset` object directly from your input function. As a last resort, you can use `tf.compat.v1.data.make_one_shot_iterator(dataset)`.\n",
            "54285it [00:27, 1953.67it/s]\n"
          ]
        }
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "RlyjYvhL9W8t",
        "outputId": "4db10071-5e40-41c9-bea3-2b2a7167ceaa"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## Load the saved model"
      ],
      "metadata": {
        "id": "8pScRsRCwX2H"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "inferrer = inference.Inferrer(\r\n",
        "    'noxpnd_cnn_swissprot_ec_random_swiss-cnn_for_swissprot_ec_random-13685140',use_tqdm= True, batch_size=32,activation_type=\"representation\"\r\n",
        ")\r\n",
        "\r\n",
        "label_vocab = list(inferrer.get_variable('label_vocab:0').astype(str))\r\n",
        "label_normalizer = parenthood_lib.get_applicable_label_dict(\r\n",
        "    'parenthood.json.gz')\r\n",
        "\r\n",
        "\r\n",
        "kernel = inferrer.get_variable(\"logits/kernel/read:0\")\r\n",
        "\r\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "W0909 12:37:24.753336 140666907211648 deprecation.py:323] From /usr/local/lib/python3.7/dist-packages/tensorflow_core/python/ops/ragged/ragged_tensor.py:1586: where (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.\n",
            "Instructions for updating:\n",
            "Use tf.where in 2.0, which has the same broadcast rule as np.where\n"
          ]
        }
      ],
      "metadata": {
        "id": "zljPgF2BMBj-",
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "outputId": "7cc5050c-877f-4bf0-91a2-d6638a2f4997"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "def get_multi_full_ec(labels, desired_number=3):\r\n",
        "  subset = [x for x in labels if x.startswith(b\"EC:\")]\r\n",
        "  subset = [x for x in subset if b'-' not in x]\r\n",
        "  if len(subset)==desired_number:\r\n",
        "    return subset\r\n",
        "  else:\r\n",
        "    return []"
      ],
      "outputs": [],
      "metadata": {
        "id": "EoIhtq-6f69y"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "def moving_average(a, n=3) :\r\n",
        "    new = np.zeros_like(a)\r\n",
        "    length_dim = a.shape[0]\r\n",
        "    for i in range(length_dim):\r\n",
        "      new[i,:]=np.mean(a[np.maximum(i-n,0):np.minimum(i+n,length_dim),:],axis=0)\r\n",
        "    return new\r\n"
      ],
      "outputs": [],
      "metadata": {
        "id": "W9h1rwNQk2yE"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "from matplotlib import colors as clr\r\n",
        "from matplotlib import pyplot as plt\r\n",
        "palette = clr.LinearSegmentedColormap.from_list('custom blue', ['#FFFFFF','#EEEEEE','#00EE00'], N=256)"
      ],
      "outputs": [],
      "metadata": {
        "id": "yxJF7xQYG13_"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "items_that_satisfy_criteria = []\r\n",
        "for i in range(len(sequences)):\r\n",
        "  new_lab = get_multi_full_ec(labels[i],desired_number=2)\r\n",
        "  new_seq = sequences[i]\r\n",
        "  new_id = ids[i]\r\n",
        "  if len(new_lab)>0:\r\n",
        "    items_that_satisfy_criteria.append({'labels':new_lab, 'sequence':new_seq, 'id':new_id})\r\n"
      ],
      "outputs": [],
      "metadata": {
        "id": "pFaTvOjAR0Il"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## Perform inference"
      ],
      "metadata": {
        "id": "FhGd6pdzodrP"
      }
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "source": [
        "counter = 0\r\n",
        "\r\n",
        "sns.set_style(\"whitegrid\")\r\n",
        "sns.set(rc={'figure.figsize': (15, 35)})\r\n",
        "\r\n",
        "one_by_one = False\r\n",
        "ids = ['Q4LB35', 'Q54QE4', 'P54889', 'Q9PLG1', 'O94632', 'P19835', 'Q3MEJ8']\r\n",
        "if True:\r\n",
        "    sns.set(rc={'figure.figsize': (15, 2)})\r\n",
        "\r\n",
        "for item in items_that_satisfy_criteria:\r\n",
        "    if item['id'].decode() in ids or one_by_one:\r\n",
        "\r\n",
        "        the_labels = item['labels']\r\n",
        "        representation = inferrer.get_activations(\r\n",
        "            [item['sequence'].decode(\"utf-8\")])\r\n",
        "        label_ids = [label_vocab.index(x.decode('utf-8')) for x in the_labels]\r\n",
        "        contributions = np.matmul(representation.squeeze(),\r\n",
        "                                  kernel)[:, label_ids]\r\n",
        "\r\n",
        "        sum_contributions = contributions.sum(axis=1)\r\n",
        "        contributions = np.maximum(contributions, 0)\r\n",
        "        contributions = moving_average(contributions, 80)\r\n",
        "        contributions = contributions / contributions.max(axis=0,\r\n",
        "                                                          keepdims=True)\r\n",
        "\r\n",
        "        df = pd.DataFrame(contributions.T)\r\n",
        "        try:\r\n",
        "            df.index = [\r\n",
        "                ec_nums[x.decode().replace(\"EC:\", \"\")].replace(\r\n",
        "                    \"<em>\",\r\n",
        "                    \"\").replace(\"</em>\",\r\n",
        "                                \"\").replace(\"<small>\",\r\n",
        "                                            \"\").replace(\"</small>\", \"\")\r\n",
        "                for x in the_labels\r\n",
        "            ]\r\n",
        "        except KeyError:\r\n",
        "            continue\r\n",
        "        print(item['id'].decode())\r\n",
        "        ax = None\r\n",
        "        if False:\r\n",
        "            ax = axes[counter]\r\n",
        "        try:\r\n",
        "            g = sns.heatmap(df, cmap=palette, xticklabels=500, ax=ax, vmin=0)\r\n",
        "            counter += 1\r\n",
        "            g.set_title(item['id'].decode())\r\n",
        "            for _, spine in g.spines.items():\r\n",
        "                spine.set_visible(True)\r\n",
        "            plt.yticks(rotation=0)\r\n",
        "            plt.xticks(rotation=0)\r\n",
        "            plt.subplots_adjust(hspace=0.7)\r\n",
        "        except ValueError:\r\n",
        "            continue\r\n",
        "\r\n",
        "        if True:\r\n",
        "            plt.show()\r\n",
        "\r\n",
        "plt.show()\r\n"
      ],
      "outputs": [
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "Annotating batches of sequences: 100%|██████████| 1/1 [00:01<00:00,  1.06s/it]\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Q54QE4\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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xVYerJUvX97qBvZ3YL6+/2l8fX1vdwgAAAAAAAD/WCtWrNDly5cVGxursmXLSpKysrIUFRWl8PBwValSxeZyfn5+Wr9+vZyd///RfZMmTdS2bVutXr1affr0schfv359eXt733ScRXrd3dyly44dO/T444/L29tbXbt21ffff28z/3vvvadHHnlEfn5+GjRokFXXIomJiRo6dKj8/Pzk6+urvn376vDhwxZ5tmzZoq5du6px48Zq2rSpunbtqh07dhjzzd1+LFq0SG3atFGjRo00cOBAnT171iqea9euaeLEiWrWrJn8/f01bdo0ZWZmWuT5+uuvFRoaKh8fH7Vo0UKRkZFKTk425pu7K1q1apXGjh2rFi1aqHv37pLy7gImISFB3bp1k7e3tzp16qRt27ZZ5VmxYoU6duyohg0bKigoSG+//bays7ON+ampqRo7dqzatGkjb29vBQYGGs1FkpKS1LBhQ3344YdW5Xbv3l3Dhg2zSjebMWOGOnfurMaNG6tNmzYaOXKk1b4zb9e6devUoUMHNWrUSAMGDFBKSooSExPVt29fNW7cWCEhIVZdkeTulsV8Du3bt09PPvmkfH191a1bNx08eNBiuWvXrmnKlCny9/eXt7e3unTpos2bN1vkyd3FkLkLmwMHDqhPnz5q1KiROnbsqN27dys7O1uzZ89Wq1at1KpVK82cOdNi/9rqYujo0aN64YUX5O3trXbt2unTTz+12n9Hjx7ViBEjFBgYqEaNGumxxx5TfHy8RdkRERE2m/sEBQUZeZKTkxUZGakWLVrIx8dHoaGh+vrrr/M8bjnXP3DgQOMa6t+/v06cOGGR5+OPP1ZISIhxTj/77LNKSEiQlHf3W5MnT7aIT5LOnDmjV199Va1atZKPj48effRRLV261CLP6tWr9eSTT8rb21stWrRQv379jOZPubsYMq/7s88++8vXpSRdv35ds2fPVnBwsBo2bKiAgABFREQY8/fv368BAwbI399fvr6+6tKli1avXm1Rhrlp1vbt2zV06FA1adLE6vop6F5T0LGcPHmymjVrpj/++MNI+/bbb/Xggw9qxYoVkqQrV65o4sSJ6tixoxo1aqSgoCCNHz9ely5dsliX+fpasmSJAgMD1bhxY0VEROj69ev66aefFBoaalxjue+ttrrbefvtt9W6dWs1btxYgwcP1oULF5SbyWTS4sWLjftVcHCwlixZYpUvtyNHjqhfv35q0aKFcW0uXLhQkrR161Z5eXnp119/tVgmJSVFPj4+eu+99yQV7v7h5eUlSYqOjjautZz3pezsbMXExKhVq1bGuZSzJdLZs2cVGRmp4OBg+fj4qEOHDpo1a5ZV87/8rqu/sp8AAAAAAMA/w86dO9WyZUujckCSOnXqpOzsbH311Vd5Lufp6WlROSDdeLm5fPnyNp95/1VFbkFw7tw5RUVFaciQIfL09NTChQvVt29fbdq0SRUqVDDybd26Vb/99pvGjx+vixcvasqUKXrzzTc1e/ZsSVJaWprCwsLk6OioqKgoubm5af78+XrhhRe0Zs0aVatWTSdOnNCwYcMUEhKiUaNGKTs7W4cOHVJKSopFTJs3b1b16tU1YcIEpaamasaMGRoyZIhWrlxpkW/OnDkKDg7WnDlztH//fsXExKhWrVp69tlnJUkHDx5U79691aJFC7311ls6f/68Zs6cqV9++UUrVqyQk5OTUdasWbMUGBho9aA5t4yMDI0YMUJ9+vRRjRo19MEHH2jw4MHGw1JJWrZsmSZNmqSwsDC1bdtW+/fvV2xsrC5duqQxY8ZIutFUZNeuXRo1apSqV6+uc+fOaefOnZJuvJndvn17ffLJJ+rRo4ex7iNHjighIUFDhw7NM74LFy4oPDxclStXVlJSkt59912FhYXp888/tzgRf/zxR128eFGvvvqq0tLSNGnSJI0bN06JiYl68skn1bt3b8XFxWnIkCHatm2bSpcunec6z507p0mTJql///7y8PDQzJkzNXjwYG3evFkuLi6SpNGjR2vXrl0aPny46tWrp88++0xDhgzRvHnzFBwcnGfZkjRmzBiFhoaqd+/eeueddzR48GA99dRTSktL07Rp0/TDDz8oJiZG999/vzp37myzjGvXrqlPnz666667FB0dLUmaO3eu0tLSVKdOHSPf2bNnVbduXXXu3FmlS5fWTz/9pJiYGF25ckWDBw+WdKMrpNDQUGOZy5cva+TIkapbt66kGzWH/fr108mTJzV69GhVrFhRy5YtU+/evbVixQo1bNjQZownT55UaGio6tevr6lTp8rBwUELFixQr169tGHDBrm6uurrr7/W66+/rj59+igwMFDp6elKSEiwetBckIsXL+qZZ56RJI0YMUI1atTQb7/9ZlEZsWjRIk2fPl3dunXTiBEjlJGRob179yopKUnVq1fPs+ziui6HDBmivXv3Kjw8XL6+vkpKStKmTZuM9Zw+fVpNmjTRs88+K1dXV3333XcaO3asTCaTnnrqKYuYxo0bpyeeeELz5s2z6DasoHtNYY7lqFGj9OWXXyoyMlLx8fG6evWqIiIi5O/vb5wn6enpysrK0ogRI1S+fHn9/vvvWrBggQYNGqRly5ZZxLplyxbVr19fEydO1MmTJzV16lS5uLjo+++/V69evVSxYkXNmDFDw4YN0xdffJFnN2jLly/XW2+9pT59+qhVq1bavXu3Xn/9dat8kydP1kcffaQBAwaoUaNG+u677zRjxgy5ubkZx8yWAQMGqGLFipo8ebLc3d114sQJo5IkMDBQVapU0SeffKJRo0YZy6xbt06SLK7Tgu4fK1eu1DPPPKOwsDCjAjFn66733ntPfn5+mjp1qn799VdFR0erQoUKGj16tKQb53rZsmUVGRkpT09P/frrr4qJidG5c+c0ZcoUSSrUdXWz+wkAAAAAANxZCnoWuWXLFpvpx44d09NPP22R5unpqUqVKunYsWNFiuH48eO6cOGC7r33Xqt5/fv3V3JysipVqqSQkBANGzasSL1hFLmCIDk5WXPmzFHLli0lSc2bN1dgYKCWLFli8WDHZDJp/vz5cnV1lXSjtUBcXJyys7Pl6OioVatW6fTp0/r888+NDWvWrJkeeeQRLV26VBEREfrxxx+VkZGhcePGyd3dXZLUpk0bq5guX76shQsXysPDQ9KNGpVevXpp165dFvl9fHw0duxYSVLr1q21b98+bdy40XhYs2DBAlWqVEkLFiwwHlRXq1ZNffv21Y4dOyzeqH7ggQc0efLkAvdXRkaGBg4cqG7dukmS/P391aFDB8XFxWnWrFnKysrSvHnzFBISYsTm7++vjIwMxcfHq3///ipXrpwOHDigxx9/3OJBZkhIiDHdo0cP9erVS0ePHjX25yeffKJq1aqpdevWecZnfuAl3Xi4aR74Yu/evfL39zfmpaWlacGCBUYXMYcPH1Z8fLwmTJhg7L/KlSurc+fO2rNnj9q1a5fnOlNSUrR8+XLVr19fknTXXXfpxRdf1A8//KCmTZvq0KFD2rRpk6KioowHpgEBAUpMTCxUBcELL7yg5557TpJUpUoVde7cWQcPHjQe4rZp00Zbt27Vhg0b8qwgWLVqlc6ePav169cbFQINGjTQo48+alFB0LJlS+NaMJlM8vPzU3p6upYvX25UENSqVcvoDslkMmngwIEqVaqUpk2bJknavn27EhISjDfTJcvzJCYmxmaMsbGxKlOmjN599125ublJutHcKDg4WB999JGef/55JSQkqGzZskZFk3RjLICiWrJkiS5cuKD169erRo0axrabXbp0SbGxsXrmmWcsWozkdx6YFcd1+dVXX2n79u2aOXOmRauSnNM5rxeTyaRmzZrpzJkzWrlypVUFQVBQkF555RWrWAu61xTmWJqP/bPPPqtly5bpl19+UWpqqsX9pHz58oqKijK+Z2ZmqkaNGnruued0/Phxo3LJ7O233zbutf/973/14YcfauHChQoICJB04635AQMG6Oeff9YDDzxgtV1ZWVmKi4tTly5djHOlTZs2unDhgj777DMjK5bhNwAAHipJREFU34kTJ7R8+XJFRUUZFUatWrVSenq65s2bp2f+r707D4+quv84/plsgJBJWMJS4IGJhUgwQRaRJAgIRgW0VBSlSx5QhKAoBQSNLIFIKhiWpmHREENBEUTABRWCCCpLItYW3CiFkqBQfoYgZJMty/z+oHObO5PJQoEg837lycPcc88999x1wvnee87DD1cagDh16pSOHTumadOmGffRXr16GfO9vb01dOhQbdiwQRMmTDCCPhs2bFB0dLRp3Irq7h+OrpNatWpVaTdKQUFBWrBggaSL95X9+/dry5YtRoAgJCTEdL1069ZNDRo0UFxcnOLj49WgQYNqr6tL3U8AAAAAAOD6UVhYWOlYnAEBAS4PwFfFbrcrMTFRzZs3N7Vv+fv767HHHtOtt96qevXq6bPPPtPy5cuVnZ2t1NTUGpdf6xYKf39/U8Ogv7+/IiMj9eWXX5ry3XrrrUaDlSTdeOONKikpMbqs+OKLL9ShQwdT1CMwMFCRkZH629/+JuliQ423t7cmT56s7du3u33q+bbbbjMa7CQZr24416lig7ejThW7+fjiiy80YMAAoxHSsYzVajXq5FCbRtbo6Gjjs7e3t+68806jbtnZ2Tp9+rTuuece0zKDBg1SSUmJ0WVFaGio3n77baWnp+vgwYMu6+jVq5fatm2r9evXS7rYoLhx40bdf//9VTZEffrppxo+fLi6d++u0NBQo0HRuauPm266ydR/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",
            "text/plain": [
              "<Figure size 1500x200 with 2 Axes>"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "Annotating batches of sequences: 100%|██████████| 1/1 [00:00<00:00,  1.17it/s]\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Q9PLG1\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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MULx8fF3bf8lSpTQxo0b5ePjc9fmAAAAAAAA7g0FkVM6JiZG/fr1k4+Pj7GIc+7cuUpISND06dMz7ffzzz9r165dev7551WrVi1FR0frvffeU9euXbV9+3Z5e3vbtLfEey2KFSuW67kWeFDaz89PNWrUkCQ9/vjj6tKlixYsWKCwsDDVqVNHzzzzjNHWZDKpdu3axnbNmjUVGBioDRs2ZHniLGNv27ZNQUFBKl++fMEcDCRJCQkJcnNzu9vTyDe7du1Su3bt1L9/f0lS06ZN7+6EkCcVKlS4q/t3cXGx+fwCAAAAAACwpw0bNujGjRtavHixvLy8JN1eyDtr1iwNGzYs0wWd9erV07/+9S85Of1/qLhu3bpq2bKltmzZooEDB9q0t4735tVdySk9evRoFS9eXB988EGW7cqUKSNvb+8cpTbo0qWLvL29FRYWluv5XL58WVOmTFHr1q1Vs2ZNtWvXTgsWLEi3tN1sNmvVqlXq0KGDHnvsMTVp0kSjR49WbGys0ebkyZN68cUXVa9ePdWuXVtDhw7VuXPnjHp/f39JUkhIiLHE/eDBgxo1apR69OiRbm4ffPCBatSooWvXrmU4d0uahu3bt6tdu3aqVauWhg8frpiYGEVGRmrQoEGqU6eOnnrqqXRpTDJKBbJ69WpjjtL/L8vfu3evRo8erbp162rMmDGSpC1btqhnz55q2LChGjRooD59+ujo0aM244WGhqpOnTo6fvy4unfvrpo1a6pz5846fvy4bt26pRkzZqhBgwZq3ry5Vq9ebdP38OHDGj58uJo2baratWurU6dO2rJli83c+vbtK+n29becT4vr169r5syZatq0qR577DE999xz+vLLL9OdQ5PJZHONsnIn5zspKUmvv/66GjZsqHr16umVV17RjRs30u3jrbfeUseOHVWnTh01a9ZM48aN0+XLl23a/PDDD+rdu7fq1aunOnXqqGPHjvr000/TjRUREaH27durTp066tu3b46PM7fi4+MVHBys9u3bq1atWgoMDNT06dNtvjYykt01lv7/Hvzqq680fvx41alTR61atdLy5ctt2qVN32FJr3H8+HENHjxYtWvXVrt27dKNL0l79+5V165dVbNmTTVq1EgzZszI9arrjNJ3JCYmavbs2WrYsKHq16+v6dOna9u2belStuTXNc+P4wAAAAAAAPem/fv3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qlQpNWvWTJ988olSUlIUHR2t5557Ltt+eeXi4qKkpCSbsrR5iS3S/vTmyJEjunjxosLCwvTII48Y5bGxscYq3Dtx69Yt7d27V5MnT1afPn2M8uwejmnh6ekpf39/m1QGaUVHRys+Pt64Ng4ODnrjjTd0/fp1DRkyRP369dOff/6pbt263dnB6HYaBel27mDrJ52m/Xr64osv5O7urrffftv4YURkZGS68WrWrKkVK1YoISFBBw8e1Lx58xQUFKQvvvjijuealcy+DiMiIlStWjUFBwcbdd9++22WY93pNc4PlhXZ06dPz/CDMyf57DNjuebR0dE219z6twqk/LnmBXkcAAAAAADg3uDr65sud3RsbKyioqJsnsWWmV27dmnmzJkaPXp0ltki8kOugtINGjTQsmXL9N1336lBgwZGeVxcnA4ePKju3bvnaJxFixYpKirKZsVldooVK5ZpIDqtF154QeHh4QoPD7cpL1mypE1wyCIhISHdSuZt27bZbDdq1EgODg765JNPjPywaQUEBOjEiROqXr16lnl6064Itta1a1eNGTNGV69eVUBAgMqWLZvpOHeqVKlS6YLvX3/9dY76JiQkSLJdAX7o0CFFRkbmS5qAxMREmc1mm/Hj4uLSPZAvs9W/jRs31r59+1SiRIkMr7l0e8Wql5eXIiIi1LVrV0m3H8L59ttvq2/fvnrnnXc0ZsyYLFfH51TVqlXl5uamXbt22fyaw7///W+bdpZ70fqHAGnvRWtubm5q0aKFzp07p9dff123bt0y8pkXhMy+DnPyNZRWTq9xQfL19VWpUqV0/vx5I+90fvHz85Orq6u++OILmx/cpP3BQX5c84I8DgAAAAAAcG9o3ry5li5dapNbOiIiQiaTSU2aNMmy78GDBzVu3Dh17do109/EzsiOHTskKdOFxpnJVVC6adOmql+/vkaOHKmgoCD5+fnp8uXLWrFihUwmk81qR4sTJ04oJSVFiYmJOn/+vLZv366vv/5affr0sUmUnZ/c3d3Vt29fm1QdWWncuLHWrl2r9evXy8fHR1u3btXZs2dt2lSqVEk9evTQO++8o5iYGAUEBCghIUF79+7VqFGjVLJkSeOnCIMGDVK3bt308MMP66+//tK3336r+vXrGw/Q8/X11e7du1W/fn0VKlRIlSpVkru7u6TbuZuLFSumw4cPa8GCBfl7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t1HHHtHf//Ox7teOZfto7cfaP1r8WMcOdjzvr+Hus/93w4nmcyI5DNc3Uq5jcbyfwUnDnB803P6v8P/bkT8P119IIZMyafg5TuPP+emcG984nV6j0XIZ7zzH+/45/XhPcCKuuuqqUftfffXVYds7OztTLBaHtNfU1KRUKo04X2dnZ6qqqlJdXT2gvVgspq+vL6VSKdOmTUtnZ2fOPPPMMc8/HEVpAEY0tTA1DZMbjh4IAAAAcIwUpQEAAAAAThMjnQl9NMViMV1dXUPaS6VSampqRh138ODBHDhwYMDZ0p2dnSkUCuWxxWIx3d3dw84/a9asMeVqTWkAAAAAgAmuoaFhyNrOXV1d+eKLL4asBT14XJK0t7cPaG9ra8s555yTadOmjTh/X19f2tvbR51/OIrSAAAAAAAT3KJFi7Jz5850dnaW27Zv355JkyalsbFxxHE/+clPcsYZZ2Tbtm3ltkOHDuXll1/OokWLBsz/0UcfZe/eveW2Xbt25b///W+uvPLKMeVa6Bvp6i8AAAAAAEwIpVIpS5cuTX19fVauXJmOjo6sXbs21157bdasWVOOW758efbv359XXnml3LZp06asX78+v/nNb3LBBRfkqaeeyo4dO/L888+nrq4uyTeF6htuuCFJcs899+Srr77KunXrMmfOnGzcuHFMuSpKAwAAAAB8C+zZsycPP/xwdu/enenTp+f666/PqlWrUlVVVY5ZtmxZPv3007z22mvltr6+vmzatCl//etf85///Cdz587Nfffdl/nz5w+Yv6OjI4888kh27NiRKVOm5Oqrr87q1atzxhlnjClPRWkAAAAAACrGmtIAAAAAAFSMojQAAAAAABWjKA0AAAAAQMUoSgMAAAAAUDGK0gAAAAAAVIyiNAAAAAAAFaMoDQAAAABAxShKAzDAnj17csstt2TevHlpbGzMunXrcvDgwfFOCwDghD333HOZM2fOkK/f//73A+K2bNmSX/ziF7noooty3XXX5fXXXx8yV1dXV1avXp3LLrss8+fPz1133ZXPP/+8Ug8FACa0KeOdAACnj1KplOXLl2f27NlZv359Ojo6snbt2nz99ddZs2bNeKcHAHBS/OlPf8qZZ55Z3p45c2b55xdffDEPPPBA7rjjjlx++eXZunVrfvWrX+XJJ5/MvHnzynF33313/v3vf6e5uTnV1dV57LHHcvvtt+fZZ5/NlCn+1AaA0fhNCUDZ5s2b8+WXX2bDhg2ZMWNGkqSnpycPPfRQVq5cOeAPNgCAierCCy/M2WefPWzf448/nqVLl+buu+9Oklx++eX517/+lT/+8Y9paWlJkuzevTs7duzIE088kYULFyZJ6uvrs2TJkrz88stZsmRJZR4IAExQlu8AoKy1tTULFiwoF6STpKmpKb29vXnrrbfGMTMAgFNv37592bt3b5qamga0L1myJLt27Sovadba2ppisZjGxsZyTENDQ+bOnZvW1taK5gwAE5GiNABlbW1taWhoGNBWLBZTW1ubtra2ccoKAODkuuaaazJ37txcddVV2bhxY3p6epKkfLxTX18/IP68887LoUOHsm/fvnJcfX19CoXCgLiGhgbHTABwDCzfAUBZZ2dnisXikPaampqUSqVxyAgA4OSpra3NnXfemUsuuSSFQiGvvfZaHnvssXR0dGTNmjXl453Bx0P92/39nZ2dA9ak7ldTU5MPPvjgFD8KAJj4FKUBAAD4TrjiiityxRVXlLcXLlyY6urq/OUvf8kdd9wxjpkBwHeL5TsAKCsWi+nq6hrSXiqVUlNTMw4ZAQCcWk1NTenp6ck//vGP8vHO4OOhzs7OJCn3F4vFdHd3D5nLMRMAHBtFaQDKhlsHsaurK1988cWQtaYBAL5t+o93Bh8PtbW1ZerUqamrqyvHtbe3p6+vb0Bce3u7YyYAOAaK0gCULVq0KDt37iyfDZQk27dvz6RJkwZcXR4A4Nti69atmTx5cn70ox+lrq4us2fPzvbt24fELFiwIFVVVUm+OWYqlUrZtWtXOaa9vT0ffvhhFi1aVNH8AWAimtzc3Nw83kkAcHo4//zzs2XLluzcuTM/+MEP8s477+R3v/tdbrzxxixdunS80wMAOCG33XZbOjo60t3dnU8++SR//vOf8+STT2bZsmVZvHhxkuSss87Khg0b0tvbmyRpaWnJ66+/nkcffTSzZs1KksyaNSvvvfdennnmmcycOTP79u3Lgw8+mNra2qxevTqTJjn/CwBGU+gb/P9GAHyn7dmzJw8//HB2796d6dOn5/rrr8+qVavKZwYBAExUjzzySP72t7/ls88+S29vb2bPnp2bbropy5YtS6FQKMdt2bIlLS0t2b9/f+rr63PPPffk5z//+YC5urq68uijj+aVV17J4cOHs3Dhwtx///2ZOXNmpR8WAEw4itIAAAAAAFSM/ykCAAAAAKBiFKUBAAAAAKgYRWkAAAAAACpGURoAAAAAgIpRlAYAAAAAoGIUpQEAAAAAqBhFaQAAAAAAKkZRGgAAAACAilGUBgAAAACgYhSlAQAAAACoGEVpAAAAAAAq5n8GVhMigIQALQAAAABJRU5ErkJggg==",
            "text/plain": [
              "<Figure size 1500x200 with 2 Axes>"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "Annotating batches of sequences: 100%|██████████| 1/1 [00:00<00:00,  1.20it/s]\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "P54889\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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Ibp8+bImTJigatWq6eOPP1ZERIQRtLM4cuSIrl69qmeffVapqamKjo7WCy+8oLNnz+rRRx/VkCFDFB8fr7Fjx+rLL78sMBXFuXPnFBwcrCeeeEJubm7av3+/pk2bppycHD322GMFruW5557TV199pcjISNWuXVvvvfeeNm3aZNPGnvUcPXpUV65cUWRkpMxms2bPnq3Jkycb2+l79+6t2bNnKyUlRV5eXpKu/8vMRx99pMcee0zOzs6SpNGjR+vChQuKioqSl5eX3nrrLf3+++9ycfm/X5uSvP+387otnnnmGfXt21cRERHy8PDQ77//rgEDBujuu+/Wq6++qmvXrun111/XgAED9PHHH8vT01OSNHXqVO3du1eTJ09WrVq1tHbtWv34449ltnZJunz5siIiIuTr66srV65o+fLlGjhwoD755BO5uLgoNTVVERERatasmV577TW5ubnp119/VXJysjHGli1bNHHiRPXs2VNjx47VpUuXNG/ePCUnJ+v1118v8L0GAAAAAAD/vLJI9XD8+HH16tXLpszb21tVq1bV8ePHiz3OH3/8ob1792rUqFH51o8YMUKJiYmqWrWqunXrpvHjx9v9TWu7Vr9o0SK1atVK0dHRkqS2bdsqKytLb7zxhl0HzY9lq3ONGjXybHt+7rnnjOdms1mtW7dWaGiotm7dqr59+xp1SUlJWrVqlRo0aCBJunjxombNmqXhw4drzJgxkqTGjRtr27Zt+vzzzzVo0CBJ0oABA4wxsrOzFRISol9//VVr165VSEiIKleurHvuuUfS9XwclStXNtq//fbbSk5O1rp164wgX2hoqLp06aKlS5fq2WefLXDNbm5ueuKJJ9SmTRt5e3vrhx9+0OLFi3X48GGtW7dOrq6uBfY9dOiQ2rRpo/79+xtlXbp0MZ5v3LhRP/30kz766CMjgN62bVv99ttv+s9//mPznqWmpmrx4sXGun7++WctW7ZMUVFReuKJJyRJvr6+6t69u7755huFh4fnO6du3boZz3NyctSiRQtduHBBa9asKTDw++uvv+qzzz5TdHS0Hn/8cUlSmzZt9OCDD9q0s2c9KSkp2rBhg7GetLQ0TZ06VefPn1f16tXVvXt3zZkzRxs3btSTTz4pSdq5c6cuXbpk/OLu2rVLhw8f1ooVKxQaGipJCgkJUfv27W2C8jf6/t/u67bo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s9ms5s2ba9q0afL399fVq1clydhlnVtxzisAAAAAALi9eXt7KyUlJU95UlKSfHx8ijXG2rVrtXDhQr300ks23zYuSNeuXTVz5kwdPnyYwC9QXJUrV5aLi4tx4zCLy5cvF9nXzc1NmZmZNmXF3dL/1VdfKS0tTXFxcUbQMisry66vBBTmt99+05EjR7Rw4UKFh4cb5enp6cXq7+Pjo7Zt22r8+PF56oobjC3MBx98UGBdt27d5OzsrEmTJik+Pr5YF8CifP755/L09NT8+fNlMl2/p+XZs2fztAsMDNSSJUuUnp6uffv2ac6cORozZow+//xzI+/y9OnT873I+vr6lnieAAAAAADg1ubn55cnl29KSoouXbokPz+/Ivtv27ZNUVFRGjdunB5//PGymqYkAr+4wzk7O6thw4bavn27kRtXuh4oLEr16tW1Z88e5eTkGF8dyO/uja6urnl28aanp8vJyckmxcGnn36aJwXCjbIcz7LzVboe6Dxw4ICRKsK6PiMjw6b/Aw88oI8//lj169cvMr9uUdLS0rRjxw41btzYKLN+np/nn39e165d0+jRo7VkyRI1a9asRHNIT0+Xq6urzVc8Nm7cWGB7d3d3tW/fXqdOndJLL72ka9euyc/PT9WrV9fp06fVv3//Es0HAAAAAADcntq1a6fFixfb5PrdsmWLTCaTWrduXWjfffv2adKkSerdu3eB3yjOzyeffCKp6HhKbgR+cccbNWqURo8erWnTpumhhx7SkSNHtGHDBkkydofmp0uXLvrggw80a9YshYeHa//+/TbpDCz8/Py0fft2NW/eXB4eHqpXr55atWolSZo6dar69eunX375RcuXL883OfiNsAQp582bp+zsbKWlpWnBggV5dqVa7hr57rvvKjw8XO7u7vL399fgwYO1ceNGDRgwQE899ZRq1qypK1eu6IcfflC1atU0ePDgfI/77bffasmSJercubNq1aqlixcvavny5bp06ZLdOZNnzJiha9euacSIEVq+fLldX2XIrXXr1nr77bc1a9Ysde7cWQcOHNBHH31k02bHjh364IMPFB4erpo1a+qPP/7QqlWrFBwcrHLlykmSpkyZosjISKWlpalDhw7y8PDQuXPntHPnTk2cOFH16tW74TkCAAAAAIDSVViO3xvVr18/rVy5UmPGjFFERIQuXLigmJgY9evX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",
            "text/plain": [
              "<Figure size 1500x200 with 2 Axes>"
            ]
          },
          "metadata": {}
        },
        {
          "output_type": "stream",
          "name": "stderr",
          "text": [
            "Annotating batches of sequences: 100%|██████████| 1/1 [00:00<00:00,  1.21it/s]\n"
          ]
        },
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "O94632\n"
          ]
        },
        {
          "output_type": "display_data",
          "data": {
            "image/png": "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Ro0ale38d+/erYoVK+qVV15RXFycFixYoNGjRzv8zGPJkiVq06aNlixZomPHjikiIkIPPfSQevXqlac59O3bVxcvXtTevXudzicjI0NRUVHatGmToqKiVKhQIXXo0EHdunVzWOcff/xRc+fOlXQz+Pr6669rzJgxCgwMVHx8vL777jslJiZmu/7OpKena9y4cRo4cKAqVaqkjz76SKNGjdKmTZuMQHDPnj3l5eWlzZs367333lPNmjXVpUsXderUyebkltvxXnzxRQ0YMEClSpXSggULNGrUKNWpU0clS5bUkiVLtGfPHs2dO1e1a9c20jEdO3ZMffv2VYsWLbR48WIlJydryZIlGjFihNatW6cyZcpo2bJlGjVqlMaPH6+GDRtKksqUKWN8QZLdfvXz81NAQIA2btyo4OBgY75fffWVLl++rK5duzqsS82aNTVt2jTNnDnT4ec/Fv/85z/15JNP2hzXFy5c0FNPPaWHHnpIaWlp+uyzz9SnTx9t3bpVVapUsdlWEydOVL9+/TRixAitXLlSY8aM0d69e1W8eHGdP39eY8eOVceOHTVhwgSZTCadOHHC+BJq2bJlWrlypf79739r2bJlxvb47bff1LNnT1WrVk3z5s2Ti4uLli9frgEDBmjnzp02vzB47733FBAQoDlz5hhfNPz++++qU6eOevXqJU9PTx09elRTp06V2WzW008/LUmKjIzUxx9/rIkTJ6patWq6fv26Dh48aOQsS0tL03PPPaerV6/q+eefV9myZbV161aFhYXZHHsAAAAAACD/3au0LWfOnHGIufj4+Kh06dI6c+ZMrvv5888/9c0332j48OFOlw8dOlQ3btxQ6dKl1bFjR40dOzZPv66/b8Fzs9mst956ywjOXLx4UZGRkTKZTE4DptHR0SpWrJhefPFFo6xly5bGc8vl+eXLl8823UpmZqYiIyPVuXNno69mzZrp6tWr+vTTT3Ocd3x8vLZs2aISJUpIkpKSkjRlyhRdunRJ5cqV0zfffKO9e/dq9erVatq0qSQpODhYV65cUURERK6C515eXho0aJAWLlyoESNGqFy5ck7r2Qdcvb291alTJ4WHh6tGjRo2da9du6YLFy7o5ZdfNq5abtSoUY5zyYqnp6feeOMNubm5GXOeOnWqRo0aZeSpb968uZo3b664uDh99tln2rx5s3r37i1f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uecUGBio4OBghYeHKy0tLds2ly9fVnh4uDp37qygoCA1b95cEyZM0MWLF23qHT58WH5+fg7/jRs3Lk9zJOc58D/siy++0A8//KA+ffrkuW2ZMmW0bt06+fr63v2JZWHMmDGKjo5WWFiYqlevrri4OH3//ff64osv8nxyu13du3dXixYt7stY99u3336rNWvWaNiwYfk9FQAAAAAA8Bdwr3Kex8bGqn///vL19VVERIRiYmI0b948paSkaNq0aVm2+/HHH7V792517dpVAQEBun79ut566y11795d27dvV4kSJWzqz507V1WrVjVeFy9ePE/zJHiO/zkpKSkqUKBAfk/jL8/T01OBgYH3bbxz585p//79mj9/vp566imjPCQkROPHj79v8yhXrpzKlSt338YDAAAAAAD4u/n444+VmJioZcuWqVixYpKkzMxMzZgxQ2FhYSpbtqzTdnXr1tWOHTvk7v7fsHadOnXUsmVLbdmyRQMHDrSpX61aNfn7+9/2PEnbgnxz+PBhPfXUUwoMDFS3bt30ww8/2Cw3m81avXq1QkJCVKtWLbVp00bvvPOOTR1Luono6Gj16NFD/v7+RpqS06dPa9SoUWrQoIECAgL05JNPavv27ZKk0aNHq2fPng5zWrt2rfz9/XXjxg1Jkp+fn1auXKmIiAg1adJEDRs21JQpU5SUlCRJunbtmmrVqqVPPvnEoa/u3btr7NixWa5/WlqaFi9erDZt2qhWrVpq3ry5Jk+eLEnau3ev/Pz8dPbsWZs2sbGxql27trGOp06d0pAhQ9SwYUMFBAQoJCREK1eulHQzPc7mzZt16tQp46cplv4l6dixY+rXr58CAwNVt25dTZgwQVevXjWWO0vb0rp1a82cOVMffvihWrVqpbp162rEiBEO6Wji4uL0yiuvqGnTpqpVq5a6dOmiAwcOZLktLOsmSaVLl3ZY5uqau1PVyZMnNWjQIGOdxowZo99//12SlJGRoS5duuiZZ55RZmam0WbFihWqVauWTpw4Icl5CpPTp0/r2Weflb+/v9q2bavNmzc7Hf/06dMaPny46tatq8DAQA0dOlTnz5/Pcd4rVqxQu3bt5O/vr0aNGmnAgAH67bffJEldunTRhAkTHNq89tpratq0qTIzM4199emnn2rmzJmqX7++mjZtqvnz5ysjI8NYr2XLlikpKck4HuzTzpw8eVK9evVSQECAOnXqpK+++spm+ZYtW9SrVy81aNBA9evXV9++fRUdHW1T59KlSxo7dqyaNGkif39/tW7dWq+++upd2U4AAAAAAOB/w/79+9W4cWMjcC5JoaGhMplMOnjwYJbtfHx8bALn0s0LIUuUKKHLly/f9Xly5TnyxZUrVzR79mwNHTpU3t7eWrhwoUaNGqXdu3fLw8NDkjRnzhytX79ew4YNU0BAgI4ePaoFCxbIy8tLvXr1MvpKT0/XhAkTNGDAAI0bN07FihXT2bNn1aNHD5UvX14vv/yySpcurV9++cUIpHbv3l1DhgzRmTNnbH66sXHjRrVr187mjfvhhx+qbt26mjdvns6ePavw8HCVLFlSEydOVIkSJdSuXTtt3LhRzzzzjNHm1KlTio6O1pgxY7LcBqNHj9Y333yjsLAwBQYG6tq1a/rXv/4lSWrRooXKKosokQAAHrNJREFUli2rjRs32gROLcH/J554QpI0bNgwlSpVSnPmzFGRIkV0/vz/t3fn8THd+x/H3xMklpjEEmlKyHJt0WooKmJr0SLtRQgtJYSK0qq1j1JVKX0kggqxhXLrWqqIFtVSD4pauqKqbm57k1haRH6WZOIiI8nvD825mZkkQu19PecxDzPf8/1+53vOzOQcn/Odzzmu06dPS5IR1E5JSdH06dMlyfjpyoEDB9S3b1+1adNGM2fO1KVLlxQXF6ehQ4fqo48+Kva92759u44dO6aJEyfq/Pnzio6O1uTJk4285NnZ2RowYIDOnj2rESNGyNPTUxs2bFBkZKSR+70wfn5+Kl++vGJiYjRq1Cg1a9ZMFSpUKHYsBZ06dUovvviivL29NW3aNF25ckUzZ87Uiy++qA0bNsjV1VXTpk1Tt27dtGDBAg0bNkxJSUmaPXu2hg8frnr16hXa75UrVxQREaFy5copNjZWkjR79mxlZWXZpLQ5ceKEnn/+edWuXVsxMTEymUxasGCB+vfvr82bN8vZ2bnQ/j/55BPNmjVLw4cPV2BgoCwWi3744QddvHhR0rXPakxMjCwWiypWrCjp2pnY9evXq1u3bipVqpTRV1xcnNq1a6e4uDgdOHBA8fHxqlmzpl544QWFhYXp9OnT+vTTT7V06VJJkqurq9HWarVqzJgx6tevn4YOHapFixZp+PDh2r59u/GTpt9++01du3ZVzZo1lZ2drU2bNqlPnz7asGGDfH19JUmvv/66zpw5owkTJqhKlSo6deqUzYmxm91OAAAAAADg3tOuXbtil2/btq3Q8pSUFHXv3t2mzGw2y8PDQykpKTc0htTUVJ09e1b+/v4OywYPHqwLFy7Iw8NDISEheu21124oYwXBc9wVGRkZWr58uWrXri1JKleunPr166cff/xRTZo00fHjx7V8+XJFRUWpV69ekqQWLVro8uXLmjt3rnr16mXMRrZarRo5cqQ6d+5s9D969GiVKVNGH374oREgbNGihbG8ZcuWevjhh5WYmKixY8dKkn755RcdPnzYIUWIh4eHZsyYIUlq3bq1jhw5oi1btmjMmDGSpJ49e6p///5KTk42vqSJiYny8vJScHBwoeu/Z88e7dixQzNmzLC5eGr+41KlSik0NFSJiYkaMWKEESDND+6bzWadO3dOv/32m95880099dRTkqTmzZsbfdWsWVOVK1fWyZMnHdKvzJgxQ4888ojmzJlj5K6qU6eOnn32We3cubPYnN95eXmaP3++EeT8/ffflZCQoNzcXDk5OWnjxo1KSkrS+vXr9be//U2S1KpVKx07dkzz5s3TrFmzCu3X1dVV7777riZMmKAhQ4aoVKlSqlevnjp06KDw8HCVL1++yDFJ0gcffKCrV69qyZIlxsmP+vXrKyQkRB9//LH69u0rf39/jRo1StOnT1dQUJDefvttNWzYUIMGDSqy33Xr1unMmTP6/PPPjWB5QECAOnbsaBM8nzNnjtzc3PSPf/xDLi4ukq79bKhdu3Zas2ZNkXnnDx06pLp16yoyMtIoa9++vfH4ueee09SpU7Vx40b17t1bkrRz506lp6c77GQaNmyoCRMmSJKCg4P1zTffaMuWLXrhhReMdDROTk6FpuPJD57nv/e+vr5q166ddu3apS5dukiSXnnlFaN+bm6ugoODdejQIX388cfG9+ann37SqFGjbL6PBdPw3Ox2AgAAAAAAD47MzEyZzWaHcjc3NyM7QUnk5eVpypQpqlatmkJCQozyihUratCgQWratKlcXFz09ddfa8mSJUpJSVFCQkKJ+ydtC+6KatWqGYFzSUaQNS0tTZK0d+9eSdLTTz+tq1evGvcWLVooPT1dp06dsunPPtj79ddf65lnnrGZWVuQk5OTunfvrvXr1xtpLRITE1W9enUFBQXZ1C0YdJck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
Download .txt
gitextract_3w94dd4w/

├── .github/
│   └── workflows/
│       └── test.yml
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── baseline_utils.py
├── baseline_utils_test.py
├── colab_evaluation.py
├── colab_evaluation_test.py
├── colabs/
│   ├── Class_Activation_Mapping.ipynb
│   ├── Embeddings.ipynb
│   ├── Price.ipynb
│   ├── README.md
│   ├── Random_EC.ipynb
│   └── Random_GO.ipynb
├── evaluation.py
├── evaluation_test.py
├── hparams_sets.py
├── inference.py
├── inference_test.py
├── install_models.py
├── misc/
│   └── price_et_al_table_s12.csv
├── parenthood_bin.py
├── parenthood_lib.py
├── parenthood_lib_test.py
├── protein_dataset.py
├── protein_dataset_test.py
├── protein_model.py
├── protein_model_test.py
├── proteinfer.py
├── proteinfer_test.py
├── requirements.txt
├── test_util.py
├── test_util_test.py
├── testdata/
│   ├── blast.tsv
│   ├── dev-00000-of-00001.tfrecord
│   ├── enzclass.txt
│   ├── go.obo
│   ├── label_vocab.tsv
│   ├── saved_model/
│   │   ├── saved_model.pb
│   │   └── variables/
│   │       ├── variables.data-00000-of-00001
│   │       └── variables.index
│   ├── test-00000-of-00001.tfrecord
│   ├── test_hemoglobin.fasta
│   └── train-00000-of-00001.tfrecord
├── train.py
├── train_test.py
├── utils.py
└── utils_test.py
Download .txt
SYMBOL INDEX (272 symbols across 25 files)

FILE: baseline_utils.py
  function _get_sequence_name_from_sequence_header (line 60) | def _get_sequence_name_from_sequence_header(s):
  function _get_labels_from_sequence_header (line 70) | def _get_labels_from_sequence_header(s):
  class BlastResult (line 86) | class BlastResult(
    method from_string (line 98) | def from_string(s, ground_truth_lookup):
  class InterproScanResult (line 119) | class InterproScanResult(
    method from_string (line 125) | def from_string(s):
  function load_ground_truth (line 137) | def load_ground_truth(filename):
  function _set_predicted_labels_missing (line 169) | def _set_predicted_labels_missing(row):
  function _fillna (line 177) | def _fillna(df, column_name, value):
  function _blast_row_to_confidence_array (line 192) | def _blast_row_to_confidence_array(
  function load_blast_output (line 205) | def load_blast_output(filename, label_vocab,
  function _pad_ec_label_with_hyphens (line 278) | def _pad_ec_label_with_hyphens(label):
  function limit_set_of_labels (line 286) | def limit_set_of_labels(df, acceptable_labels,
  function limit_labels_for_label_normalizer (line 296) | def limit_labels_for_label_normalizer(
  function load_interproscan_output (line 309) | def load_interproscan_output(

FILE: baseline_utils_test.py
  function _write_to_file (line 35) | def _write_to_file(contents):
  class BaselineUtilsTest (line 42) | class BaselineUtilsTest(parameterized.TestCase):
    method test_get_sequence_name_from_sequence_header (line 56) | def test_get_sequence_name_from_sequence_header(self, header, expected):
    method test_get_labels_from_sequence_header (line 72) | def test_get_labels_from_sequence_header(self, header, expected):
    method test_load_ground_truth (line 76) | def test_load_ground_truth(self):
    method test_blast_row_to_confidence_array (line 130) | def test_blast_row_to_confidence_array(self, input_row, input_label_vo...
    method test_load_blast_output (line 138) | def test_load_blast_output(self):
    method test_limit_set_of_labels (line 186) | def test_limit_set_of_labels(self):
    method test_limit_labels_for_label_normalizer (line 207) | def test_limit_labels_for_label_normalizer(self):
    method test_load_interproscan_output (line 295) | def test_load_interproscan_output(self, interproscan_output,

FILE: colab_evaluation.py
  function read_blast_table (line 32) | def read_blast_table(filename):
  function stats_by_group (line 55) | def stats_by_group(df):
  function get_stats (line 73) | def get_stats(df):
  function apply_threshold_and_return_stats (line 81) | def apply_threshold_and_return_stats(predictions_df,
  function batch_inferences (line 95) | def batch_inferences(iterator, batch_size):
  function batched_inferences_from_files (line 117) | def batched_inferences_from_files(shard_names, batch_size=100):
  function batched_inferences_from_dir (line 129) | def batched_inferences_from_dir(shard_dir_path, batch_size=100):
  function _make_tidy_df_from_seq_names_and_prediction_array (line 135) | def _make_tidy_df_from_seq_names_and_prediction_array(
  function get_normalized_inference_results (line 155) | def get_normalized_inference_results(shard_dir_path,
  function make_tidy_df_from_ground_truth (line 189) | def make_tidy_df_from_ground_truth(ground_truth):
  function merge_predictions_and_ground_truth (line 202) | def merge_predictions_and_ground_truth(predictions_df, ground_truth_df):
  function get_pr_curve_df (line 213) | def get_pr_curve_df(predictions_df,
  function filter_pr_curve (line 259) | def filter_pr_curve(precisions, recalls, thresholds, resolution=1e-4):
  function assign_tp_fp_fn (line 278) | def assign_tp_fp_fn(predictions_df, ground_truth_df, threshold):

FILE: colab_evaluation_test.py
  class ColabEvaluationTest (line 34) | class ColabEvaluationTest(parameterized.TestCase):
    method _generate_random_inferences (line 35) | def _generate_random_inferences(self, n):
    method test_batched_inferences_from_dir (line 51) | def test_batched_inferences_from_dir(self, batch_size, num_examples=100):
    method test_make_tidy_df_from_seq_names_and_prediction_array (line 93) | def test_make_tidy_df_from_seq_names_and_prediction_array(self):
    method test_make_tidy_df_from_ground_truth (line 110) | def test_make_tidy_df_from_ground_truth(self):
    method test_merge_predictions_and_ground_truth (line 123) | def test_merge_predictions_and_ground_truth(self):
    method test_get_pr_curve_df (line 145) | def test_get_pr_curve_df(self):
    method test_assign_tp_fp_fn (line 165) | def test_assign_tp_fp_fn(self):
    method test_apply_threshold_and_return_stats (line 187) | def test_apply_threshold_and_return_stats(self):
    method test_read_blast_table (line 215) | def test_read_blast_table(self):

FILE: evaluation.py
  function normalize_confidences (line 40) | def normalize_confidences(
  function get_ground_truth_multihots (line 76) | def get_ground_truth_multihots(label_sets,
  function get_pr_f1_df_from_arrays (line 89) | def get_pr_f1_df_from_arrays(
  function get_pr_f1_df (line 130) | def get_pr_f1_df(
  function true_false_positive_negative_df (line 175) | def true_false_positive_negative_df(df):
  function multilabel_precision_per_example_label_pair (line 220) | def multilabel_precision_per_example_label_pair(
  function multilabel_recall_per_example_label_pair (line 239) | def multilabel_recall_per_example_label_pair(
  function multilabel_f1_per_example_label_pair (line 258) | def multilabel_f1_per_example_label_pair(
  function normalize_predictions (line 278) | def normalize_predictions(
  function precision_recall_f1 (line 285) | def precision_recall_f1(
  function filter_predictions_to_above_threshold (line 314) | def filter_predictions_to_above_threshold(
  function get_predictions_above_threshold (line 341) | def get_predictions_above_threshold(
  function _family_and_clan_to_just_clan (line 379) | def _family_and_clan_to_just_clan(
  function pfam_label_normalizer_to_lifted_clan (line 408) | def pfam_label_normalizer_to_lifted_clan(
  function convert_pfam_ground_truth_to_lifted_clans (line 416) | def convert_pfam_ground_truth_to_lifted_clans(
  function _ec_label_at_level (line 441) | def _ec_label_at_level(label, level):
  function ec_agreement_for_level (line 452) | def ec_agreement_for_level(df,

FILE: evaluation_test.py
  class _InferrerFixture (line 32) | class _InferrerFixture(object):
    method __init__ (line 35) | def __init__(self, vocab_size):
    method get_activations (line 38) | def get_activations(self, l):
    method get_variable (line 42) | def get_variable(self, _):
  class TestEvaluation (line 46) | class TestEvaluation(parameterized.TestCase):
    method test_normalize_confidences_edge (line 70) | def test_normalize_confidences_edge(self, confidences, vocab, normalized,
    method test_normalize_confidences (line 76) | def test_normalize_confidences(self):
    method test_get_ground_truth_multihots (line 96) | def test_get_ground_truth_multihots(self):
    method test_get_pr_f1_df (line 120) | def test_get_pr_f1_df(self,
    method test_get_pr_f1_df_precision_truncation (line 144) | def test_get_pr_f1_df_precision_truncation(self):
    method test_multilabel_recall_per_example_label_pair (line 239) | def test_multilabel_recall_per_example_label_pair(self, df, expected):
    method test_multilabel_precision_per_example_label_pair (line 318) | def test_multilabel_precision_per_example_label_pair(self, df, expected):
    method test_multilabel_f1_per_example_label_pair (line 323) | def test_multilabel_f1_per_example_label_pair(self):
    method test_precision_recall_f1_integration_test (line 334) | def test_precision_recall_f1_integration_test(self):
    method test_filter_predictions_to_above_threshold (line 398) | def test_filter_predictions_to_above_threshold(self, input_predictions,
    method test_get_predictions_above_threshold (line 428) | def test_get_predictions_above_threshold(self, input_seqs, decision_th...
    method test_pfam_label_normalizer_to_lifted_clan (line 474) | def test_pfam_label_normalizer_to_lifted_clan(self, input_normalizer,
    method test_pfam_label_normalizer_to_lifted_clan_raises_when_wrong_num (line 485) | def test_pfam_label_normalizer_to_lifted_clan_raises_when_wrong_num(se...
    method test_pfam_label_normalizer_to_lifted_clan_raises_when_no_clan (line 491) | def test_pfam_label_normalizer_to_lifted_clan_raises_when_no_clan(self):
    method test_convert_pfam_ground_truth_to_lifted_clans (line 498) | def test_convert_pfam_ground_truth_to_lifted_clans(self):
    method test_ec_agreement_for_level (line 662) | def test_ec_agreement_for_level(self, input_level, input_df, expected_...

FILE: hparams_sets.py
  function _starting_hparams (line 26) | def _starting_hparams():
  function tuned_for_ec (line 42) | def tuned_for_ec():
  function small_test_model (line 65) | def small_test_model():

FILE: inference.py
  function call_module (line 41) | def call_module(module, one_hots, row_lengths, signature):
  function in_graph_inferrer (line 92) | def in_graph_inferrer(sequences,
  function memoized_inferrer (line 131) | def memoized_inferrer(
  class Inferrer (line 153) | class Inferrer(object):
    method __init__ (line 156) | def __init__(
    method __repr__ (line 220) | def __repr__(self):
    method _get_tensor_by_name (line 226) | def _get_tensor_by_name(self, name):
    method _get_activations_for_batch_unmemoized (line 230) | def _get_activations_for_batch_unmemoized(self,
    method _get_activations_for_batch_memoized (line 263) | def _get_activations_for_batch_memoized(self,
    method get_activations (line 269) | def get_activations(self, list_of_seqs, custom_tensor_to_retrieve=None):
    method get_variable (line 323) | def get_variable(self, variable_name):
  function latest_savedmodel_path_from_base_path (line 336) | def latest_savedmodel_path_from_base_path(base_path):
  function predictions_for_df (line 354) | def predictions_for_df(df, inferrer):
  function serialize_inference_result (line 372) | def serialize_inference_result(sequence_name,
  function deserialize_inference_result (line 403) | def deserialize_inference_result(results_b64):
  function parse_shard (line 441) | def parse_shard(shard_path):
  function parse_all_shards (line 459) | def parse_all_shards(shard_dir_path):
  function get_preds_at_or_above_threshold (line 479) | def get_preds_at_or_above_threshold(input_df,

FILE: inference_test.py
  class _InferrerFixture (line 39) | class _InferrerFixture(object):
    method __init__ (line 46) | def __init__(self, activation_rank=1):
    method get_variable (line 55) | def get_variable(self, x):
    method get_activations (line 62) | def get_activations(self, input_seqs):
  class InGraphInferrerTest (line 81) | class InGraphInferrerTest(tf.test.TestCase, parameterized.TestCase):
    method testCanInfer (line 83) | def testCanInfer(self):
  class InferenceLibTest (line 101) | class InferenceLibTest(parameterized.TestCase, tf.test.TestCase):
    method testBatchedInference (line 103) | def testBatchedInference(self):
    method testSortUnsortInference (line 112) | def testSortUnsortInference(self):
    method testStringInput (line 122) | def testStringInput(self):
    method testMemoizedInferrerLoading (line 130) | def testMemoizedInferrerLoading(self):
    method testMemoizedInferenceResults (line 138) | def testMemoizedInferenceResults(self):
    method testGetVariable (line 146) | def testGetVariable(self):
    method test_predictions_for_df (line 151) | def test_predictions_for_df(self):
    method test_serialize_deserialize_inference_result (line 165) | def test_serialize_deserialize_inference_result(self):
    method test_parse_sharded_inference_results (line 178) | def test_parse_sharded_inference_results(self):
    method testGetPredsAboveThreshold (line 250) | def testGetPredsAboveThreshold(self, input_df, expected, threshold):
    method testGetPredsAboveThresholdRaisesOnZeroThreshold (line 258) | def testGetPredsAboveThresholdRaisesOnZeroThreshold(self):

FILE: install_models.py
  function download_models (line 41) | def download_models(model_cache_path,
  function get_description_file (line 58) | def get_description_file(model_cache_path):
  function run (line 66) | def run(install_ensemble, model_cache_path):
  function main (line 83) | def main(_):

FILE: parenthood_bin.py
  function _get_ec_transitive (line 47) | def _get_ec_transitive():
  function _get_go_transitive (line 61) | def _get_go_transitive():
  function get_output_dict (line 72) | def get_output_dict():
  function write_output_dict (line 96) | def write_output_dict(output_dict, output_path):
  function main (line 113) | def main(_):

FILE: parenthood_lib.py
  class GoTerm (line 143) | class GoTerm(
    method from_string (line 166) | def from_string(cls, s):
  function _is_go_attribute_line (line 216) | def _is_go_attribute_line(s):
  function _parse_go_attribute (line 220) | def _parse_go_attribute(s):
  function _get_go_term_from_text (line 225) | def _get_go_term_from_text(s):
  function _yield_terms_for_alt_ids (line 235) | def _yield_terms_for_alt_ids(term):
  function parse_full_go_file (line 263) | def parse_full_go_file(file_contents = None):
  function go_label_to_description (line 287) | def go_label_to_description(
  function _go_term_applicable_labels_should_include_themselves (line 294) | def _go_term_applicable_labels_should_include_themselves(term):
  function transitive_go_parenthood (line 313) | def transitive_go_parenthood(go_terms):
  function _transitive_parenthood (line 356) | def _transitive_parenthood(key,
  function _replace_one_level_up_with_dash_for_ec (line 384) | def _replace_one_level_up_with_dash_for_ec(s):
  function _all_ec_parents_for_label (line 408) | def _all_ec_parents_for_label(label):
  function _get_leaf_node_ec_labels_from_file_contents (line 433) | def _get_leaf_node_ec_labels_from_file_contents(
  function _get_non_leaf_node_ec_labels_from_file_contents (line 473) | def _get_non_leaf_node_ec_labels_from_file_contents(
  function ec_label_to_description (line 508) | def ec_label_to_description(
  function parse_full_ec_file_to_transitive_parenthood (line 539) | def parse_full_ec_file_to_transitive_parenthood(
  function get_applicable_label_dict (line 584) | def get_applicable_label_dict(
  function reverse_map (line 589) | def reverse_map(
  function is_implied_by_something_else (line 619) | def is_implied_by_something_else(
  function _filter_label_set_to_most_specific (line 650) | def _filter_label_set_to_most_specific(
  function filter_labels_to_most_specific (line 669) | def filter_labels_to_most_specific(

FILE: parenthood_lib_test.py
  class ParenthoodLibTest (line 34) | class ParenthoodLibTest(parameterized.TestCase):
    method setUp (line 36) | def setUp(self):
    method test_go_term_from_string (line 125) | def test_go_term_from_string(self, input_text, expected):
    method test_go_term_parsing_integration_test (line 130) | def test_go_term_parsing_integration_test(self):
    method test_transitive_go_parenthood (line 148) | def test_transitive_go_parenthood(self):
    method test_transitive_go_parenthood_with_cycle (line 189) | def test_transitive_go_parenthood_with_cycle(self):
    method test_all_ec_parents_for_term (line 265) | def test_all_ec_parents_for_term(self, label, expected):
    method test_parse_full_ec_file_to_transitive_parenthood_integration_test (line 269) | def test_parse_full_ec_file_to_transitive_parenthood_integration_test(...
    method test_ec_label_to_description (line 315) | def test_ec_label_to_description(self):
    method test_yield_terms_for_alt_ids (line 442) | def test_yield_terms_for_alt_ids(self, input_go_term, expected):
    method test_reverse_map_filters_items (line 447) | def test_reverse_map_filters_items(self):
    method test_is_implied_by_something_else_positive_case (line 454) | def test_is_implied_by_something_else_positive_case(self):
    method test_is_implied_by_something_else_negative_case (line 466) | def test_is_implied_by_something_else_negative_case(self):
    method test_filter_labels_to_most_specific (line 477) | def test_filter_labels_to_most_specific(self):

FILE: protein_dataset.py
  function _map_sequence_to_ints (line 57) | def _map_sequence_to_ints(example, amino_acid_table):
  function _map_labels_to_ints (line 77) | def _map_labels_to_ints(example, protein_class_table):
  function _to_one_hot_sequence (line 98) | def _to_one_hot_sequence(indexed_sequence_tensors):
  function _add_sequence_length (line 128) | def _add_sequence_length(example):
  function _is_sequence_short_enough_for_training (line 133) | def _is_sequence_short_enough_for_training(example):
  function non_batched_dataset (line 138) | def non_batched_dataset(train_dev_or_test,
  function batched_dataset (line 192) | def batched_dataset(input_dataset, batch_size, bucket_boundaries):
  function make_input_fn (line 236) | def make_input_fn(batch_size, data_file_pattern, train_dev_or_test,
  function yield_examples (line 279) | def yield_examples(tfrecord_path):

FILE: protein_dataset_test.py
  function _numpy_one_hot (line 31) | def _numpy_one_hot(x, depth):
  function _dataset_iterator_to_list (line 44) | def _dataset_iterator_to_list(itr, session):
  class ProteinDatasetTest (line 63) | class ProteinDatasetTest(parameterized.TestCase):
    method test_non_padded_dataset (line 65) | def test_non_padded_dataset(self):
    method test_padded_dataset (line 125) | def test_padded_dataset(self):
    method test_yield_examples (line 171) | def test_yield_examples(self):

FILE: protein_model.py
  function _f1_score (line 35) | def _f1_score(labels, predictions):
  function _mean_examplewise_f1_score (line 44) | def _mean_examplewise_f1_score(labels, predictions):
  function _custom_recall_at_k (line 79) | def _custom_recall_at_k(labels_as_multi_hot, predictions, k):
  function _make_evaluation_metrics (line 127) | def _make_evaluation_metrics(labels, predictions, num_output_classes, hp...
  function _set_padding_to_sentinel (line 174) | def _set_padding_to_sentinel(padded_representations, sequence_lengths,
  function _make_per_sequence_features (line 216) | def _make_per_sequence_features(per_location_representations, raw_features,
  function _convert_representation_to_prediction_ops (line 243) | def _convert_representation_to_prediction_ops(representation, raw_features,
  function _make_representation (line 275) | def _make_representation(features, hparams, mode):
  function _make_prediction_ops (line 311) | def _make_prediction_ops(features, hparams, mode, label_vocab):
  function _batch_norm (line 331) | def _batch_norm(features, is_training):
  function _conv_layer (line 335) | def _conv_layer(sequence_features, sequence_lengths, num_units, dilation...
  function _residual_block (line 354) | def _residual_block(sequence_features, sequence_lengths, hparams, layer_...
  function _indices_to_multihot (line 391) | def _indices_to_multihot(indices, vocab_size):
  function _make_loss (line 421) | def _make_loss(predictions_for_loss, labels, num_output_classes):
  function _make_train_op (line 435) | def _make_train_op(loss, hparams):
  function make_model_fn (line 459) | def make_model_fn(label_vocab, hparams):

FILE: protein_model_test.py
  class ProteinModelTest (line 31) | class ProteinModelTest(parameterized.TestCase):
    method testF1Score (line 33) | def testF1Score(self):
    method testMeanExampleWiseF1Score (line 47) | def testMeanExampleWiseF1Score(self):
    method testRecallAtK (line 63) | def testRecallAtK(self):
    method testSetPaddingToSentinel (line 128) | def testSetPaddingToSentinel(self, padded_representations, sequence_le...
    method testIndicesToMultiHot (line 148) | def testIndicesToMultiHot(self, input_array, vocab_size, expected):

FILE: proteinfer.py
  function _num_decimal_places (line 77) | def _num_decimal_places(f):
  function _gcs_path_to_relative_unzipped_path (line 83) | def _gcs_path_to_relative_unzipped_path(p):
  function _get_inferrer_paths (line 89) | def _get_inferrer_paths(model_urls,
  function load_models (line 98) | def load_models(model_cache_path, num_ensemble_elements):
  function _assert_fasta_parsable (line 162) | def _assert_fasta_parsable(input_text):
  function parse_input_to_text (line 177) | def parse_input_to_text(input_fasta_path):
  function input_text_to_df (line 197) | def input_text_to_df(input_text):
  function perform_inference (line 207) | def perform_inference(input_df, models,
  function _sort_df_multiple_columns (line 235) | def _sort_df_multiple_columns(df, key):
  function order_df_for_output (line 254) | def order_df_for_output(predictions_df):
  function _format_float_confidence_for_output (line 305) | def _format_float_confidence_for_output(input_float,
  function format_df_for_output (line 311) | def format_df_for_output(predictions_df,
  function write_output (line 338) | def write_output(predictions_df, output_path):
  function run (line 345) | def run(input_text, models, reporting_threshold,
  function load_assets_and_run (line 376) | def load_assets_and_run(input_fasta_path, output_path,
  function main (line 405) | def main(_):

FILE: proteinfer_test.py
  class ProteinferTest (line 26) | class ProteinferTest(parameterized.TestCase):
    method test_gcs_path_to_relative_unzipped_path (line 28) | def test_gcs_path_to_relative_unzipped_path(self):
    method test_parse_input (line 34) | def test_parse_input(self):
    method test_parse_input_malformed_fasta (line 49) | def test_parse_input_malformed_fasta(self):
    method test_format_output_adds_description_and_formats_float_confidence (line 55) | def test_format_output_adds_description_and_formats_float_confidence(s...
    method test_format_float_confidence (line 83) | def test_format_float_confidence(self, input_confidence, num_decimal_p...
    method test_load_models_raises_on_model_missing_no_ensemble (line 89) | def test_load_models_raises_on_model_missing_no_ensemble(self):
    method test_load_models_raises_on_model_missing_with_ensemble (line 103) | def test_load_models_raises_on_model_missing_with_ensemble(self):
    method test_order_df_for_output (line 202) | def test_order_df_for_output(self, input_df, expected):

FILE: test_util.py
  function model_testdata_path (line 31) | def model_testdata_path():
  function savedmodel_path (line 36) | def savedmodel_path():
  function assert_dataframes_equal (line 40) | def assert_dataframes_equal(abseil_testcase_instance,

FILE: test_util_test.py
  class TestUtilTest (line 31) | class TestUtilTest(parameterized.TestCase):
    method test_assert_dataframes_equal_no_error (line 113) | def test_assert_dataframes_equal_no_error(self,
    method test_assert_dataframes_equal_error (line 176) | def test_assert_dataframes_equal_error(self, df1, df2, order_by_column...
    method test_assert_dataframes_equal_nan_equal_nan (line 180) | def test_assert_dataframes_equal_nan_equal_nan(self):
    method test_assert_dataframes_equal_nan_raises (line 185) | def test_assert_dataframes_equal_nan_raises(self):

FILE: train.py
  function _make_estimator (line 70) | def _make_estimator(hparams, label_vocab, output_dir):
  function get_serving_input_fn (line 94) | def get_serving_input_fn():
  function _make_estimator_and_inputs (line 126) | def _make_estimator_and_inputs(hparams, label_vocab, data_base_path, out...
  function get_hparams (line 178) | def get_hparams(hparams_set_name):
  function parse_label_vocab (line 191) | def parse_label_vocab(label_vocab_path):
  function train (line 216) | def train(data_base_path, output_dir, label_vocab_path, hparams_set_name,
  function main (line 250) | def main(_):

FILE: train_test.py
  class TrainTest (line 35) | class TrainTest(parameterized.TestCase):
    method setUp (line 37) | def setUp(self):
    method test_parse_label_vocab (line 44) | def test_parse_label_vocab(self):
    method test_train_gives_non_nan_loss (line 55) | def test_train_gives_non_nan_loss(self):

FILE: utils.py
  function residues_to_indices (line 117) | def residues_to_indices(amino_acid_residues):
  function normalize_sequence_to_blosum_characters (line 121) | def normalize_sequence_to_blosum_characters(seq):
  function _build_one_hot_encodings (line 138) | def _build_one_hot_encodings():
  function residues_to_one_hot (line 169) | def residues_to_one_hot(amino_acid_residues):
  function fasta_indexer (line 197) | def fasta_indexer():
  function fasta_encoder (line 208) | def fasta_encoder():
  function in_graph_residues_to_onehot (line 220) | def in_graph_residues_to_onehot(residues):
  function calculate_bucket_batch_sizes (line 244) | def calculate_bucket_batch_sizes(bucket_boundaries, max_expected_sequenc...
  function batch_iterable (line 281) | def batch_iterable(iterable, batch_size):
  function pad_one_hot (line 314) | def pad_one_hot(one_hot, length):
  function make_padded_np_array (line 323) | def make_padded_np_array(ragged_arrays):
  function absolute_paths_of_files_in_dir (line 341) | def absolute_paths_of_files_in_dir(dir_path):
  function load_gz_json (line 346) | def load_gz_json(path):
  function fetch_oss_pretrained_models (line 352) | def fetch_oss_pretrained_models(

FILE: utils_test.py
  class TestTensorUtils (line 32) | class TestTensorUtils(parameterized.TestCase):
    method testBucketSize (line 34) | def testBucketSize(self):
    method testBatchIterable (line 54) | def testBatchIterable(self, input_iterable, batch_size, expected):
    method testSparseToOneHot (line 59) | def testSparseToOneHot(self):
    method testPadOneHotSameLength (line 100) | def testPadOneHotSameLength(self, input_one_hot, pad_length, expected):
    method test_absolute_paths_of_files_in_dir (line 107) | def test_absolute_paths_of_files_in_dir(self):
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (6,127K chars).
[
  {
    "path": ".github/workflows/test.yml",
    "chars": 719,
    "preview": "name: Testing\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  test:\n    runs-on:"
  },
  {
    "path": ".gitignore",
    "chars": 31,
    "preview": "cached_models/**\n__pycache__/**"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 863,
    "preview": "This repository exists to provide a record of published work, and so we do not\nexpect to receive pull-requests and do no"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 3431,
    "preview": "[![GitHub license](https://img.shields.io/badge/license-Apache2-blue.svg)](https://github.com/google-research/nisaba/blo"
  },
  {
    "path": "baseline_utils.py",
    "chars": 12154,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "baseline_utils_test.py",
    "chars": 13960,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "colab_evaluation.py",
    "chars": 11557,
    "preview": "# coding=utf-8\r\n# Copyright 2020 The Google Research Authors.\r\n#\r\n# Licensed under the Apache License, Version 2.0 (the "
  },
  {
    "path": "colab_evaluation_test.py",
    "chars": 8978,
    "preview": "# coding=utf-8\r\n# Copyright 2020 The Google Research Authors.\r\n#\r\n# Licensed under the Apache License, Version 2.0 (the "
  },
  {
    "path": "colabs/Class_Activation_Mapping.ipynb",
    "chars": 190188,
    "preview": "{\n  \"nbformat\": 4,\n  \"nbformat_minor\": 2,\n  \"metadata\": {\n    \"accelerator\": \"GPU\",\n    \"colab\": {\n      \"name\": \"Copy o"
  },
  {
    "path": "colabs/Embeddings.ipynb",
    "chars": 227008,
    "preview": "{\n  \"nbformat\": 4,\n  \"nbformat_minor\": 0,\n  \"metadata\": {\n    \"accelerator\": \"GPU\",\n    \"colab\": {\n      \"name\": \"Protei"
  },
  {
    "path": "colabs/Price.ipynb",
    "chars": 39188,
    "preview": "{\n  \"nbformat\": 4,\n  \"nbformat_minor\": 0,\n  \"metadata\": {\n    \"colab\": {\n      \"name\": \"Price comparison\",\n      \"proven"
  },
  {
    "path": "colabs/README.md",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "colabs/Random_EC.ipynb",
    "chars": 669279,
    "preview": "{\n  \"nbformat\": 4,\n  \"nbformat_minor\": 0,\n  \"metadata\": {\n    \"colab\": {\n      \"name\": \"Random_EC.ipynb\",\n      \"provena"
  },
  {
    "path": "colabs/Random_GO.ipynb",
    "chars": 4143057,
    "preview": "{\n  \"nbformat\": 4,\n  \"nbformat_minor\": 0,\n  \"metadata\": {\n    \"colab\": {\n      \"name\": \"Random_GO.ipynb\",\n      \"provena"
  },
  {
    "path": "evaluation.py",
    "chars": 18562,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "evaluation_test.py",
    "chars": 25373,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "hparams_sets.py",
    "chars": 2835,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "inference.py",
    "chars": 18606,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "inference_test.py",
    "chars": 9540,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "install_models.py",
    "chars": 3119,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "misc/price_et_al_table_s12.csv",
    "chars": 402038,
    "preview": "Category,organism,orgId,locusId,sysName,locus_tag,protein_id,uniprotId,new_annotation,comment,original_description,SEED"
  },
  {
    "path": "parenthood_bin.py",
    "chars": 3746,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "parenthood_lib.py",
    "chars": 22741,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "parenthood_lib_test.py",
    "chars": 17861,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "protein_dataset.py",
    "chars": 10287,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "protein_dataset_test.py",
    "chars": 6527,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "protein_model.py",
    "chars": 18497,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "protein_model_test.py",
    "chars": 5988,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "proteinfer.py",
    "chars": 15326,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "proteinfer_test.py",
    "chars": 8657,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "requirements.txt",
    "chars": 494,
    "preview": "absl-py==0.7.1\nastor==0.7.1\nbiopython==1.78\nbackports.weakref==1.0.post1\nenum34==1.1.6\nfuncsigs==1.0.2\ngast==0.2.2\ngrpci"
  },
  {
    "path": "test_util.py",
    "chars": 3810,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "test_util_test.py",
    "chars": 5722,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "testdata/blast.tsv",
    "chars": 62,
    "preview": "accession=\"ABC\"\taccession=\"DEF\"\t50\t10\t100\t20\t4\t5\t6\t7\t1-e10\t500"
  },
  {
    "path": "testdata/enzclass.txt",
    "chars": 20206,
    "preview": "---------------------------------------------------------------------------\n        ENZYME nomenclature database\n       "
  },
  {
    "path": "testdata/label_vocab.tsv",
    "chars": 200,
    "preview": "vocab_item\tvocab_index\nGO:GO:0005737\t0\nGO:GO:0005524\t1\nGO:GO:0006457\t2\nGO:GO:0020038\t3\nGO:GO:0020039\t4\nGO:GO:0020040\t5\nG"
  },
  {
    "path": "testdata/test_hemoglobin.fasta",
    "chars": 973,
    "preview": ">sp|P69891|HBG1_HUMAN Hemoglobin subunit gamma-1 OS=Homo sapiens OX=9606 GN=HBG1 PE=1 SV=2\nMGHFTEEDKATITSLWGKVNVEDAGGETL"
  },
  {
    "path": "train.py",
    "chars": 8438,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "train_test.py",
    "chars": 2897,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "utils.py",
    "chars": 14390,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  },
  {
    "path": "utils_test.py",
    "chars": 4187,
    "preview": "# coding=utf-8\n# Copyright 2020 The Google Research Authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Li"
  }
]

// ... and 7 more files (download for full content)

About this extraction

This page contains the full source code of the google-research/proteinfer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 49 files (38.3 MB), approximately 1.5M tokens, and a symbol index with 272 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!