Source code for mhcflurry.pseudosequences

# 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.

"""Class I pseudosequence artifact names and lookup helpers.

This module is the canonical registry for pseudosequence CSV filenames used by
MHCflurry training and model serialization.

Canonical pseudosequence files:

* ``pseudosequences.netmhcpan.34aa.csv``: NetMHCpan-derived 34 amino acid
  pseudosequences.
* ``pseudosequences.mhcflurry.37aa.csv``: MHCflurry-generated 37 amino acid
  pseudosequences used by older public pan-allele model bundles.
* ``pseudosequences.mhcflurry.39aa.csv``: MHCflurry-generated 39 amino acid
  pseudosequences from the aligned full-sequence pipeline.

Compatibility aliases:

* ``allele_sequences.csv``: legacy runtime/model artifact filename.
* ``class1_pseudosequences.csv``: legacy NetMHCpan 34aa filename.
* ``allele_sequences.no_differentiation.csv``: legacy pan-variant experiment
  output; not a canonical pseudosequence artifact name.

Trained model directories should ship the exact pseudosequence CSV used during
training. The saved weights depend on both the representation width and the
position definition.

Expected locations:

* ``downloads-generation/*/pseudosequences.netmhcpan.34aa.csv``: checked-in
  generation seed tables.
* standalone ``allele_sequences`` download: should contain
  ``pseudosequences.mhcflurry.39aa.csv`` plus ``allele_sequences.csv`` as a
  compatibility copy.
* trained model directories: should contain ``allele_sequences.csv`` and, for
  newly-saved models, the matching canonical ``pseudosequences.*.*aa.csv``.
"""
from __future__ import annotations

import argparse
import logging
from dataclasses import dataclass
from glob import glob
from os.path import basename, exists, join

import pandas


LEGACY_ALLELE_SEQUENCES_FILENAME = "allele_sequences.csv"
LEGACY_CLASS1_PSEUDOSEQUENCES_FILENAME = "class1_pseudosequences.csv"
LEGACY_NO_DIFFERENTIATION_FILENAME = "allele_sequences.no_differentiation.csv"
PSEUDOSEQUENCE_GLOB = "pseudosequences.*.*aa.csv"


[docs] @dataclass(frozen=True) class PseudosequenceDefinition: """A named pseudosequence CSV definition.""" source: str length: int filename: str description: str
PSEUDOSEQUENCE_DEFINITIONS = ( PseudosequenceDefinition( source="netmhcpan", length=34, filename="pseudosequences.netmhcpan.34aa.csv", description="NetMHCpan-derived 34 amino acid pseudosequences.", ), PseudosequenceDefinition( source="mhcflurry", length=37, filename="pseudosequences.mhcflurry.37aa.csv", description=( "MHCflurry-generated 37 amino acid pseudosequences used by older " "public pan-allele model bundles." ), ), PseudosequenceDefinition( source="mhcflurry", length=39, filename="pseudosequences.mhcflurry.39aa.csv", description=( "MHCflurry-generated 39 amino acid pseudosequences from the " "aligned full-sequence pipeline." ), ), ) PSEUDOSEQUENCE_DEFINITIONS_BY_LENGTH = { definition.length: definition for definition in PSEUDOSEQUENCE_DEFINITIONS } PSEUDOSEQUENCE_FILENAMES_BY_LENGTH = { length: definition.filename for (length, definition) in PSEUDOSEQUENCE_DEFINITIONS_BY_LENGTH.items() } PSEUDOSEQUENCE_FILENAME_PREFERENCE = ( LEGACY_ALLELE_SEQUENCES_FILENAME, PSEUDOSEQUENCE_FILENAMES_BY_LENGTH[39], PSEUDOSEQUENCE_FILENAMES_BY_LENGTH[37], PSEUDOSEQUENCE_FILENAMES_BY_LENGTH[34], LEGACY_CLASS1_PSEUDOSEQUENCES_FILENAME, ) LEGACY_FILENAMES = { "allele_sequences": LEGACY_ALLELE_SEQUENCES_FILENAME, "class1_pseudosequences": LEGACY_CLASS1_PSEUDOSEQUENCES_FILENAME, "no_differentiation": LEGACY_NO_DIFFERENTIATION_FILENAME, }
[docs] def pseudosequence_length(allele_to_sequence): """Return the common pseudosequence length, or ``None`` if ambiguous.""" if not allele_to_sequence: return None lengths = set() for sequence in allele_to_sequence.values(): if pandas.isnull(sequence): continue lengths.add(len(str(sequence))) if len(lengths) == 1: return lengths.pop() return None
[docs] def pseudosequence_filename_for_length(length): """Return the canonical pseudosequence filename for a representation length.""" if length is None: return None return PSEUDOSEQUENCE_FILENAMES_BY_LENGTH.get(int(length))
[docs] def pseudosequence_filename_for_mapping(allele_to_sequence): """Return the canonical pseudosequence filename for a saved mapping.""" return pseudosequence_filename_for_length( pseudosequence_length(allele_to_sequence))
[docs] def pseudosequence_filename_candidates(models_dir): """ Pseudosequence files accepted when loading a saved predictor. ``allele_sequences.csv`` and ``class1_pseudosequences.csv`` are legacy artifact filenames. The ``pseudosequences.*.*aa.csv`` names make the source and representation width explicit for newly-generated artifacts. """ result = [] for filename in PSEUDOSEQUENCE_FILENAME_PREFERENCE: if exists(join(models_dir, filename)): result.append(filename) for path in sorted(glob(join(models_dir, PSEUDOSEQUENCE_GLOB))): filename = basename(path) if filename not in result: result.append(filename) return result
[docs] def pseudosequence_path(directory, length, fallback_legacy=True): """Return the preferred pseudosequence path in ``directory``. When the canonical ``pseudosequences.*.<length>aa.csv`` is absent and ``fallback_legacy`` is true, returns the legacy ``allele_sequences.csv`` path and logs a warning so callers don't silently read a different sequence set than requested. """ filename = pseudosequence_filename_for_length(length) if filename is None: raise ValueError("No canonical pseudosequence filename for %saa" % length) path = join(directory, filename) if exists(path) or not fallback_legacy: return path legacy_path = join(directory, LEGACY_ALLELE_SEQUENCES_FILENAME) logging.warning( "Canonical pseudosequence file %s not found in %s; falling back to " "legacy %s. The legacy file may carry a different representation " "width than requested (%saa).", filename, directory, LEGACY_ALLELE_SEQUENCES_FILENAME, length, ) return legacy_path
def _run_filename(args): filename = pseudosequence_filename_for_length(args.length) if filename is None: raise SystemExit("No canonical pseudosequence filename for %saa" % ( args.length,)) print(filename) def _run_legacy(args): print(LEGACY_FILENAMES[args.name]) def _run_path(args): print(pseudosequence_path( args.directory, args.length, fallback_legacy=args.fallback_legacy)) def _run_list(_args): for definition in PSEUDOSEQUENCE_DEFINITIONS: print("%s\t%s\t%s" % ( definition.length, definition.source, definition.filename, ))
[docs] def main(argv=None, prog=None): """Command-line access for shell generation scripts.""" parser = argparse.ArgumentParser(prog=prog, description=__doc__) subparsers = parser.add_subparsers(dest="command", required=True) filename_parser = subparsers.add_parser( "filename", help="Print the canonical pseudosequence filename for a length.") filename_parser.add_argument("--length", required=True, type=int) filename_parser.set_defaults(func=_run_filename) legacy_parser = subparsers.add_parser( "legacy", help="Print a legacy compatibility filename.") legacy_parser.add_argument("name", choices=sorted(LEGACY_FILENAMES)) legacy_parser.set_defaults(func=_run_legacy) path_parser = subparsers.add_parser( "path", help="Print the preferred pseudosequence path in a directory.") path_parser.add_argument("--directory", required=True) path_parser.add_argument("--length", required=True, type=int) path_parser.add_argument( "--fallback-legacy", action="store_true", help="Use allele_sequences.csv if the canonical file is absent.") path_parser.set_defaults(func=_run_path) list_parser = subparsers.add_parser( "list", help="List canonical pseudosequence definitions.") list_parser.set_defaults(func=_run_list) args = parser.parse_args(argv) args.func(args)
if __name__ == "__main__": main()