Skip to content

NMRStarEntry

NMRStarEntry

NMRStarEntry(data=None, entry_id=None, source_file=None)

A parsed NMR-STAR file, indexed by saveframe category and framecode.

Construct with one of the classmethods rather than the initializer:

entry = NMRStarEntry.from_bmrb(15000)
entry = NMRStarEntry.from_file("bmr15000_3.str")
Source code in makeshift/entry.py
def __init__(self, data=None, entry_id=None, source_file=None):
    self.data = data or {}
    self.entry_id = entry_id
    self.source_file = source_file

categories property

categories

Saveframe categories as an attribute-accessible mapping.

from_bmrb classmethod

from_bmrb(bmrb_id, output_dir='', keep_download=False)

Download a BMRB entry and parse it.

Source code in makeshift/entry.py
@classmethod
def from_bmrb(cls, bmrb_id, output_dir="", keep_download=False):
    """
    Download a BMRB entry and parse it.
    """
    url = _BMRB_URL.format(id=bmrb_id)

    if keep_download:
        path = os.path.join(output_dir, f"bmr{bmrb_id}_3.str")
        urllib.request.urlretrieve(url, path)
        return cls.from_file(path, entry_id=bmrb_id)

    fd, path = tempfile.mkstemp(suffix=f"_bmr{bmrb_id}_3.str")
    os.close(fd)
    try:
        urllib.request.urlretrieve(url, path)
        data = cls._parse(path)
    finally:
        os.remove(path)
    return cls(data=data, entry_id=bmrb_id)

saveframe

saveframe(category, framecode=None)

Return one saveframe dict, or all framecodes for a category.

Source code in makeshift/entry.py
def saveframe(self, category, framecode=None):
    """Return one saveframe dict, or all framecodes for a category."""
    cat = self.data.get(category, {})
    return cat if framecode is None else cat[framecode]

loop_to_dataframe staticmethod

loop_to_dataframe(loop)

Turn a loop (list of row dicts) into a DataFrame.

Source code in makeshift/entry.py
@staticmethod
def loop_to_dataframe(loop):
    """Turn a loop (list of row dicts) into a DataFrame."""
    if not loop:
        return pd.DataFrame()
    cols = {k: [] for k in loop[0].keys()}
    for row in loop:
        for k, v in row.items():
            cols[k].append(v)
    return pd.DataFrame.from_records(cols)

sequences

sequences(entity_id=None)

One row per entity: ID, polymer type, one-letter sequence.

Source code in makeshift/entry.py
def sequences(self, entity_id=None):
    """One row per entity: ID, polymer type, one-letter sequence."""
    tags = ["ID", "Polymer_type", "Polymer_seq_one_letter_code"]
    rows = []
    for framecode, sf in self.saveframe("entity").items():
        rows.append({"entity": framecode, **{t: sf.get(t) for t in tags}})
    seq_df = pd.DataFrame.from_records(rows, columns=["entity"] + tags)
    seq_df = seq_df.astype({
        "ID": "Int64",  # nullable integer dtype
        "Polymer_type": "string",
        "Polymer_seq_one_letter_code": "string",
    })

    if entity_id is not None:
        try:
            return seq_df.loc[seq_df["ID"] == int(entity_id), "Polymer_seq_one_letter_code"].item()
        except Exception:
            warnings.warn(f"Could not find entity: {entity_id}. Returning all sequences", UserWarning)
    return seq_df

polymer_type

polymer_type(entity_id=None)

One row per entity: ID, polymer type, one-letter sequence.

Source code in makeshift/entry.py
def polymer_type(self, entity_id=None):
    """One row per entity: ID, polymer type, one-letter sequence."""

    seq_df = self.sequences()
    if entity_id is not None:
        try:
            return seq_df.loc[seq_df["ID"] == int(entity_id), "Polymer_type"].item()
        except:
            warnings.warn(f"Could not find entity: {entity_id}. Returning all information", UserWarning)
    return seq_df

sample_info

sample_info()

One row per sample component (flattens the _Sample_component loop).

Source code in makeshift/entry.py
def sample_info(self):
    """One row per sample component (flattens the _Sample_component loop)."""
    tags = ["ID", "Sample_ID", "Mol_common_name", "Entity_ID",
            "Isotopic_labeling", "Concentration_val", "Concentration_val_units"]
    return self._loop_records("sample", "_Sample_component", tags, id_key="sample")

is_deuterated

is_deuterated(entity_id=None, sample_id=None, sample_name=None)

Bool if an id is given, else a [Sample_ID, Entity_ID, deuterated] table.

Source code in makeshift/entry.py
def is_deuterated(self, entity_id=None, sample_id=None, sample_name=None):
    """Bool if an id is given, else a [Sample_ID, Entity_ID, deuterated] table."""
    if entity_id is None and sample_id is None:
        return self._entity_flag_table(_DEUTER_KEYWORDS, "deuterated")
    rows = self._sample_rows(entity_id, sample_id, sample_name)
    return self._any_keyword(rows["Isotopic_labeling"], _DEUTER_KEYWORDS)

is_methyl_labeled

is_methyl_labeled(entity_id=None, sample_id=None, sample_name=None)

Bool if an id is given, else a [Sample_ID, Entity_ID, methyl_labeled] table.

Source code in makeshift/entry.py
def is_methyl_labeled(self, entity_id=None, sample_id=None, sample_name=None):
    """Bool if an id is given, else a [Sample_ID, Entity_ID, methyl_labeled] table."""
    if entity_id is None and sample_id is None:
        return self._entity_flag_table(_METHYL_KEYWORDS, "methyl_labeled")
    rows = self._sample_rows(entity_id, sample_id, sample_name)
    return self._any_keyword(rows["Isotopic_labeling"], _METHYL_KEYWORDS)

is_denatured

is_denatured(entity_id=None, sample_id=None, sample_name=None)

Bool if sample_id is given, else a [Sample_ID, Entity_ID, denatured] table.

Source code in makeshift/entry.py
def is_denatured(self, entity_id=None, sample_id=None, sample_name=None):
    """Bool if sample_id is given, else a [Sample_ID, Entity_ID, denatured] table."""
    if sample_id is None:
        return self._sample_flag_table(_DENATURANT_KEYWORDS, "denatured")
    rows = self._sample_rows(entity_id, sample_id, sample_name)
    return self._any_keyword(rows["Mol_common_name"], _DENATURANT_KEYWORDS)

assembly_info

assembly_info()

One row per entity assembly (flattens the _Entity_assembly loop).

Source code in makeshift/entry.py
def assembly_info(self):
    """One row per entity assembly (flattens the _Entity_assembly loop)."""
    return self._loop_records("assembly", "_Entity_assembly",
                              ["Entity_assembly_name"], id_key="assembly")

spectrometers

spectrometers()

One row per spectrometer: name, manufacturer, model, field strength.

Source code in makeshift/entry.py
def spectrometers(self):
    """One row per spectrometer: name, manufacturer, model, field strength."""
    tags = ["ID", "Name", "Manufacturer", "Model", "Field_strength"]
    return self._loop_records("NMR_spectrometer_list", "_NMR_spectrometer_view",
                              tags, id_key="spectrometer_list")

shift_reference

shift_reference()

One row per referenced nucleus (how the depositor referenced shifts).

Source code in makeshift/entry.py
def shift_reference(self):
    """One row per referenced nucleus (how the depositor referenced shifts)."""
    tags = ["Atom_type", "Atom_isotope_number", "Mol_common_name",
            "Chem_shift_val", "Indirect_shift_ratio", "Ref_method", "Ref_type"]
    return self._loop_records("chem_shift_reference", "_Chem_shift_ref",
                              tags, id_key="reference")

sample_conditions

sample_conditions()

One row per sample-condition set, with each condition Type as a column (e.g. pH, temperature, pressure, ionic_strength) plus its units.

Source code in makeshift/entry.py
def sample_conditions(self):
    """One row per sample-condition set, with each condition Type as a
    column (e.g. pH, temperature, pressure, ionic_strength) plus its units."""
    rows = []
    for framecode, sf in self.saveframe("sample_conditions").items():
        row = {"sample_conditions": framecode}
        for var in sf.get("_Sample_condition_variable", []):
            ctype = var.get("Type")
            ctype = ctype.strip('*')
            if not ctype or ctype in (".", "?"):
                continue
            row[ctype] = self._num(var.get("Val"))
            units = self._clean(var.get("Val_units"))
            if units and units.lower() != "na":
                row[f"{ctype}_units"] = units
        rows.append(row)
    return pd.DataFrame.from_records(rows)

relaxation

relaxation(kind)

Heteronuclear relaxation data as a tidy DataFrame, one row per residue.

kind : 'T1'/'R1', 'T2'/'R2', 'T1rho'/'R1rho', or 'NOE' (case-insensitive). Columns: Seq_ID, Comp_ID, Atom_ID, Val, Val_err (T2 lists also carry Rex_val/Rex_err when present; NOE adds the second atom Seq_ID_2 etc.). The source list framecode is the first column, so multiple field strengths stay distinct.

BMRB is inconsistent about the value tag — some entries use the generic Val/Val_err, others the type-prefixed T1_val/T1_val_err — so whichever is present is coalesced into a single Val/Val_err.

Source code in makeshift/entry.py
def relaxation(self, kind):
    """
    Heteronuclear relaxation data as a tidy DataFrame, one row per residue.

    kind : 'T1'/'R1', 'T2'/'R2', 'T1rho'/'R1rho', or 'NOE' (case-insensitive).
    Columns: Seq_ID, Comp_ID, Atom_ID, Val, Val_err (T2 lists also carry
    Rex_val/Rex_err when present; NOE adds the second atom Seq_ID_2 etc.).
    The source list framecode is the first column, so multiple field
    strengths stay distinct.

    BMRB is inconsistent about the value tag — some entries use the generic
    `Val`/`Val_err`, others the type-prefixed `T1_val`/`T1_val_err` — so
    whichever is present is coalesced into a single `Val`/`Val_err`.
    """
    key = kind.upper().replace("-", "").replace("_", "")
    key = self._RELAX_ALIASES.get(key, key)
    if key not in self._RELAX:
        raise ValueError(
            f"unknown relaxation kind {kind!r}; "
            "choose from T1/R1, T2/R2, T1rho/R1rho, NOE"
        )
    category, loop, prefix = self._RELAX[key]
    df = self._coalesce_value(self.data_loop(category, loop), prefix)

    if key == "NOE":
        keep = ["list", "Seq_ID_1", "Comp_ID_1", "Atom_ID_1",
                "Seq_ID_2", "Comp_ID_2", "Atom_ID_2", "Val", "Val_err"]
        df = df.reindex(columns=keep).rename(
            columns={"Seq_ID_1": "Seq_ID", "Comp_ID_1": "Comp_ID",
                     "Atom_ID_1": "Atom_ID"})
        return self._coerce(df, ["Seq_ID", "Seq_ID_2", "Val", "Val_err"])

    keep = ["list", "Seq_ID", "Comp_ID", "Atom_ID", "Atom_type", "Val", "Val_err"]
    if "Rex_val" in df.columns and df["Rex_val"].notna().any():
        keep += ["Rex_val", "Rex_err"]
    df = df.reindex(columns=keep)
    return self._coerce(df, ["Seq_ID", "Val", "Val_err", "Rex_val", "Rex_err"])

order_parameters

order_parameters()

Model-free order parameters (S2) as a tidy DataFrame, one row per residue: Seq_ID, Comp_ID, Atom_ID, S2, S2_err, Tau_e, Rex, Model_fit.

Source code in makeshift/entry.py
def order_parameters(self):
    """
    Model-free order parameters (S2) as a tidy DataFrame, one row per
    residue: Seq_ID, Comp_ID, Atom_ID, S2, S2_err, Tau_e, Rex, Model_fit.
    """

    category, loop, prefix = self._ORDER['Order']
    df = self._coalesce_value(self.data_loop(category, loop), prefix)

    tags = ["Seq_ID", "Comp_ID", "Atom_ID",
            "Order_param_val", "Order_param_val_fit_err",
            "Tau_e_val", "Rex_val", "Model_fit"]
    df = self._loop_records("order_parameters", "_Order_param", tags, id_key="list")
    df = self._coerce(df, ["Seq_ID", "Order_param_val",
                           "Order_param_val_fit_err", "Tau_e_val", "Rex_val"])
    return df.rename(columns={"Order_param_val": "S2",
                              "Order_param_val_fit_err": "S2_err",
                              "Tau_e_val": "Tau_e", "Rex_val": "Rex"})

datasets

datasets()

What data the entry contains: one row per data type with its count (from the entry_information _Data_set loop). Use this to discover which of the methods above will return anything.

Source code in makeshift/entry.py
def datasets(self):
    """What data the entry contains: one row per data type with its count
    (from the entry_information _Data_set loop). Use this to discover which
    of the methods above will return anything."""
    return self._loop_records("entry_information", "_Data_set",
                              ["Type", "Count"], id_key="entry")

data_loop

data_loop(category, loop_name, tags=None)

Generic escape hatch: flatten the loop_name loop from every saveframe of category into one DataFrame (framecode in the first column). Keeps all columns when tags is None. Use for data types without a dedicated method (coupling constants, RDCs, spectral density, CSA, ...); inspect a saveframe's available loops with entry.saveframe(category).

Source code in makeshift/entry.py
def data_loop(self, category, loop_name, tags=None):
    """
    Generic escape hatch: flatten the `loop_name` loop from every saveframe
    of `category` into one DataFrame (framecode in the first column). Keeps
    all columns when `tags` is None. Use for data types without a dedicated
    method (coupling constants, RDCs, spectral density, CSA, ...); inspect a
    saveframe's available loops with `entry.saveframe(category)`.
    """
    frames = []
    for framecode, sf in self.saveframe(category).items():
        if loop_name in sf:
            df = self.loop_to_dataframe(sf[loop_name])
            if tags is not None:
                df = df.reindex(columns=tags)
            df.insert(0, "list", framecode)
            frames.append(df)
    return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()

get_entry_title

get_entry_title()

Entry title: from entry_information, falling back to the entry citation.

Source code in makeshift/entry.py
def get_entry_title(self):
    """Entry title: from entry_information, falling back to the entry citation."""
    for sf in self.saveframe("entry_information").values():
        try:
            title = self._clean(sf.get("Title"))
            return title
        except:
            continue
    return None

get_citation_title

get_citation_title()

Citation title: entry_information first, then the entry citation.

Source code in makeshift/entry.py
def get_citation_title(self):
    """Citation title: entry_information first, then the entry citation."""
    for sf in self.saveframe("entry_information").values():
        title = self._clean(sf.get("Title"))
        if title:
            return title
    cite = self._entry_citation()
    return self._clean(cite.get("Title")) if cite else None

citation_info

citation_info()

Title, journal, year, DOI, PubMed ID, and authors of the entry citation.

Source code in makeshift/entry.py
def citation_info(self):
    """Title, journal, year, DOI, PubMed ID, and authors of the entry citation."""
    cite = self._entry_citation() or {}
    authors = [
        f"{self._clean(a.get('Given_name')) or ''} {self._clean(a.get('Family_name')) or ''}".strip()
        for a in cite.get("_Citation_author", [])
    ]
    return {
        "citation_title": self.get_citation_title(),
        "journal": self._clean(cite.get("Journal_name_full"))
                   or self._clean(cite.get("Journal_abbrev")),
        "year": self._clean(cite.get("Year")),
        "pubmed_id": self._clean(cite.get("PubMed_ID")),
        "authors": authors,
    }