Source code for molsysmt.basic.get

import numpy as np
from smonitor import signal

from molsysmt._private.argdigest import arg_digest
from molsysmt._private.chemical_state import resolve_chemical_state
from molsysmt._private.variables import is_all, is_iterable_of_iterables


[docs] @signal(tags=["api", "get"]) @arg_digest() @resolve_chemical_state def get( molecular_system, element="system", selection="all", structure_indices="all", mask=None, syntax="MolSysMT", get_missing_bonds=True, output_type="values", chemical_state="reference", skip_digestion=False, **kwargs, ): """ Retrieving attribute values from a molecular system. This function retrieves values of one or more attributes from a molecular system (or from a selected subset of it), optionally specifying the hierarchical `element` level. Attributes to be returned are indicated via keyword flags in `**kwargs` (e.g., ``n_atoms=True``, ``coordinates=True``). Parameters ---------- molecular_system : molecular system Molecular system in any supported MolSysMT format. element : str, default='system' Structural element level to query ('atom', 'group', 'component', 'molecule', 'chain', 'entity'). selection : str, list, tuple, or numpy.ndarray, default='all' Selection string or boolean/integer array specifying elements. structure_indices : int, list, tuple, or numpy.ndarray, default='all' Structure indices (0-based) to include or process. mask : str, list, tuple, numpy.ndarray, or None, default=None Boolean mask combined with `selection` when choosing elements. syntax : str, default='MolSysMT' Selection syntax used to evaluate `selection` (e.g., 'MolSysMT', 'MDTraj'). get_missing_bonds : bool, default=True Whether to infer missing covalent bonds before resolving derived attributes. output_type : str, default='values' Representation used for the returned result. chemical_state : {'reference', 'structure'}, int, or None, default='reference' Chemical state used to resolve state-dependent attributes. skip_digestion : bool, default=False Whether to skip MolSysMT's internal argument digestion mechanism. Returns ------- Any or list or dict or None With output_type='values', a single requested attribute returns its value (including None when absent). Multiple attributes return a list of values in their requested order. With output_type='dictionary', the result maps each requested attribute name to its value. Raises ------ NotSupportedFormError If the molecular system has an unsupported form. ArgumentError If any input argument is invalid or inconsistent, including malformed selections and out-of-range element, mask, or structure indices. FormatError If a legacy H5MSM file has missing, invalid, or contradictory units, or structures YAML has an unsupported schema or invalid quantity record. NotWithThisFormError If a form declares a requested attribute but provides neither a compatible direct getter, registered derivation, nor usable attribute pipe. Notes ----- Native atom-level group identities preserve missing parent membership as None per selected atom when the hierarchy is partially populated. The same missing-parent mapping applies to group-based molecule/entity identities. A wholly absent group table retains its group-identity None result. - Supported molecular-system forms are summarized in :ref:`Introduction_Forms`. - Selection strings must follow one of the syntaxes described in :ref:`Introduction_Selection`. - A request may combine attributes from different element levels when an incompatible attribute has exactly one catalog-supported level. MolSysMT evaluates that attribute at its supported level and preserves the input attribute order. For example, with ``element='atom'``, ``coordinates`` are evaluated on atoms while ``structure_id`` is evaluated on the system. The atom selection is not applied to system-level attributes. - Native Structures and MolSys objects expose stored temperature, potential energy, and kinetic energy series. Total energy is returned only when both energy components are present. OpenMM Context and Simulation objects expose temperature only when their integrator defines it. - Native Topology and MolSys objects resolve state-dependent atom chemistry, components, and bonds from ``chemical_state``. The default uses the reference-state rules. Access is rejected when multiple states exist without a reference and no explicit index is supplied. - A partial native MolSys without topology can still expose its atom count, chemical-state inventory, and stored structural series. Attributes that require an absent domain return ``None``. Explicit chemical-state selection on such an object is not supported yet. - ``isotope`` is stable atom metadata. Rich bond attributes keep integral and fractional order, relationship type, aromaticity, conjugation, stereochemistry and reference atoms, direction, component participation, and evidence independent. - State inventory attributes such as ``chemical_state_index`` and ``chemical_state_id`` continue to describe every state even when ``chemical_state`` selects one state for other requested attributes. - Explicit integer state selection currently requires a native Topology or MolSys. Convert an external form before querying a non-reference state. - ``chemical_state='structure'`` requires a native MolSys and rejects missing associations or structure selections spanning multiple states. - A native topology with zero groups returns ``None`` for atom-level ``group_id``, ``group_name``, and ``group_type`` queries. No group or ligand label is invented. - Empty native connectivity is valid. With ``element='atom'``, ``n_bonds`` returns a zero count and ``bond_index`` an empty list for each selected atom. With ``element='system'`` (the default), the total bond count is zero. This does not establish that chemical connectivity is complete. - With ``element='group'``, ``n_bonds`` counts distinct bonds incident on each selected group in the selected chemical state. An internal bond contributes once; a bond between groups contributes once to each group. - Form-independent attributes such as box lengths, angles, shape, and volume are derived from the box matrix when the source form exposes that matrix but does not implement a dedicated getter. - Sparse ``alternate_location`` mappings are keyed by integer atom indices in the source system. Atom selections filter these keys without renumbering them. The outer list follows ``structure_indices``, including repeated indices and structures with no alternate sites (empty dictionaries). Each entry's ``atom_id`` values remain string labels. An absent alternate-location attribute returns ``None``. See Also -------- :func:`molsysmt.basic.select` Select elements from a molecular system. :func:`molsysmt.basic.get_attributes` Get the list of available attributes for a molecular system. Examples -------- >>> import molsysmt as msm >>> from molsysmt import systems >>> molsys = systems['T4 lysozyme L99A']['181l.h5msm'] >>> msm.get(molsys, element='group', selection=[10,11,12], n_atoms=True) [9, 4, 8] >>> msm.get(molsys, element='molecule', selection='molecule_type=="water"', n_molecules=True) 136 >>> msm.get(molsys, element='bond', selection=[0,1,2,3,4], bonded_atoms=True) [0, 1, 2, 3, 4, 8] >>> from molsysmt.native import Topology >>> topology = Topology(n_atoms=3) >>> msm.set(topology, element='atom', formal_charge=[0, -1, 1]) >>> msm.get(topology, element='atom', chemical_state=0, formal_charge=True) [0, -1, 1] >>> msm.set(topology, element='atom', isotope=[13, None, 2]) >>> msm.get(topology, element='atom', isotope=True)[0] 13 >>> msm.get(topology, element='system', n_chemical_states=True, ... reference_chemical_state_index=True) [1, 0] >>> import numpy as np >>> from molsysmt.native import MolSys >>> molsys = MolSys(n_atoms=1) >>> molsys.structures.coordinates = msm.pyunitwizard.quantity(np.zeros((2, 1, 3)), 'nm') >>> msm.get(molsys, structure_chemical_state_index=True) [0, 0] >>> molsys = Topology(n_atoms=2) >>> msm.get(molsys, element='atom', n_bonds=True) [0, 0] >>> msm.get(molsys, element='atom', bond_index=True) [[], []] >>> msm.get(molsys, n_groups=True) 0 >>> msm.get(molsys, element='atom', group_name=True) is None True >>> from molsysmt.native import Structures >>> molsys = Structures( ... coordinates=msm.pyunitwizard.quantity(np.zeros((1, 3, 3)), 'nm'), ... alternate_location=[{2: { ... 'location_id': np.array(['A', 'B']), 'atom_id': [10, 11], ... 'occupancy': np.array([0.6, 0.4]), ... 'b_factor': msm.pyunitwizard.quantity(np.zeros(2), 'nm**2'), ... 'coordinates': msm.pyunitwizard.quantity(np.zeros((2, 3)), 'nm'), ... }}]) >>> sites = msm.get(molsys, selection=[2], alternate_location=True) >>> list(sites[0]) [2] >>> sites[0][2]['atom_id'] ['10', '11'] .. admonition:: Tutorial with more examples See the following tutorial for a practical demonstration of how to use this function, along with additional examples: :ref:`Tutorial_Get`. .. versionadded:: 1.0.0 """ from molsysmt._private.h5msm import maybe_read_modular_h5msm from molsysmt.attribute import ( attributes, ) from molsysmt.form import _dict_modules from .. import get_form, select, where_is_attribute molecular_system = maybe_read_modular_h5msm(molecular_system) form = get_form(molecular_system) if isinstance(form, (list, tuple)): attributes_filter = _dict_modules[form[0]].attributes.copy() for aux_form in form[1:]: for aux_attribute, aux_bool in _dict_modules[aux_form].attributes.items(): if aux_bool: attributes_filter[aux_attribute] = True else: attributes_filter = _dict_modules[form].attributes in_attributes = [] # print(f"DEBUG: get() called with kwargs keys: {list(kwargs.keys())}") for key in kwargs.keys(): if kwargs[key]: in_attributes.append(key) # print(f"DEBUG: in_attributes resolved: {in_attributes}") if not isinstance(molecular_system, (list, tuple)): molecular_system = [molecular_system] form = [form] if not in_attributes: return {} if output_type == "dictionary" else [] if any(attributes[attribute]["runs_on_structures"] for attribute in in_attributes): from ._index_validation import validate_structure_indices structure_indices = validate_structure_indices( molecular_system, structure_indices, "molsysmt.get" ) attribute_groups = _group_attributes_by_element(element, selection, in_attributes) if attribute_groups and ( len(attribute_groups) > 1 or element not in attribute_groups ): output_dictionary = {} for target_element, target_attributes in attribute_groups.items(): if element == "system" and not is_all(selection): use_requested_selection = target_element == "atom" else: use_requested_selection = target_element == element target_selection = selection if use_requested_selection else "all" target_mask = mask if use_requested_selection else None output_dictionary.update( get( molecular_system, element=target_element, selection=target_selection, structure_indices=structure_indices, chemical_state=chemical_state, mask=target_mask, syntax=syntax, get_missing_bonds=get_missing_bonds, output_type="dictionary", skip_digestion=True, **{attribute: True for attribute in target_attributes}, ) ) if output_type == "dictionary": return { attribute: output_dictionary[attribute] for attribute in in_attributes } output = [output_dictionary[attribute] for attribute in in_attributes] return output[0] if len(output) == 1 else output if not is_all(selection): indices = select( molecular_system, element=element, selection=selection, chemical_state=chemical_state, mask=mask, syntax=syntax, skip_digestion=True, ) else: if (mask is None) or (is_all(mask)): indices = "all" else: indices = select( molecular_system, element=element, selection=mask, chemical_state=chemical_state, syntax=syntax, skip_digestion=True, ) piped_molecular_systems, piped_attributes = _piped_molecular_system( molecular_system, element, in_attributes ) if piped_molecular_systems is None: output = [] for in_attribute in in_attributes: if attributes_filter[in_attribute]: dict_indices = {} if element != "system": if attributes[in_attribute]["runs_on_elements"]: dict_indices["indices"] = indices if attributes[in_attribute]["runs_on_structures"]: dict_indices["structure_indices"] = structure_indices aux_item, aux_form = where_is_attribute( molecular_system, in_attribute, skip_digestion=True ) if aux_item is None: result = None else: getter_name = f"get_{in_attribute}_from_{element}" aux_get = getattr(_dict_modules[aux_form], getter_name, None) if aux_get is None: from molsysmt._private.attribute_derivation import ( NOT_DERIVABLE, derive_attribute, ) result = derive_attribute( _dict_modules[aux_form], aux_item, in_attribute, element, structure_indices=dict_indices.get( "structure_indices", "all" ), ) if result is NOT_DERIVABLE: from molsysmt._private.smonitor import NotWithThisFormError raise NotWithThisFormError( caller="molsysmt.get", form=aux_form, requested_attribute=in_attribute, message=( f"Form {aux_form!r} declares attribute {in_attribute!r} but " f"does not implement {getter_name!r}, a registered derivation, " "or a usable attribute pipe." ), ) else: result = aux_get(aux_item, **dict_indices) if (result is not None) and in_attribute.endswith("_id"): result = _coerce_ids_to_string(result) if in_attribute == "alternate_location" and result is not None: result = _coerce_alternate_location_ids(result) else: result = None output.append(result) else: output_dictionary = {} for aux_molecular_system, aux_attributes in zip( piped_molecular_systems, piped_attributes ): if aux_molecular_system is None: aux_molecular_system = molecular_system aux_dict = get( aux_molecular_system, element=element, selection=indices, structure_indices=structure_indices, chemical_state=chemical_state, mask=mask, syntax=syntax, get_missing_bonds=get_missing_bonds, output_type="dictionary", skip_digestion=False, **{ii: True for ii in aux_attributes}, ) output_dictionary.update(aux_dict) output = [] for in_attribute in in_attributes: output.append(output_dictionary[in_attribute]) import pyunitwizard as puw _CANONICAL_UNITS = { "coordinates": "nm", "box": "nm", "box_lengths": "nm", "box_angles": "radians", "box_volume": "nm**3", "velocities": "nm/ps", "time": "ps", "time_step": "ps", "potential_energy": "kJ/mol", "kinetic_energy": "kJ/mol", "total_energy": "kJ/mol", "temperature": "K", "b_factor": "nm**2", } def _standardize(value, attribute): if value is None: return None if puw.is_quantity(value): return puw.standardize(value) if attribute in _CANONICAL_UNITS: return puw.quantity(value, _CANONICAL_UNITS[attribute]) if attribute in _ATTRIBUTES_WITH_NUMPY_SCALARS: return _coerce_native_scalars(value) return value if output_type == "values": if len(output) == 1: return _standardize(output[0], in_attributes[0]) else: return [_standardize(val, attr) for val, attr in zip(output, in_attributes)] elif output_type == "dictionary": return {ii: _standardize(jj, ii) for ii, jj in zip(in_attributes, output)}
# Attributes whose getters assemble Python containers by iterating NumPy arrays, so # the scalars inside them arrive as `np.int64` or `np.str_` instead of native types. # `devguide/INTERFACES.md`, *Scalar types in returned values*, requires native scalars # in Python containers, and this is where that is enforced. # # The set is deliberate rather than a blanket pass over every return. Walking a value # to find out whether it needs fixing costs more than the getter itself where nothing # does: measured on a 78 974-atom system, normalising the already-clean `atom_index` # costs 12.4 ms against a 6.1 ms call, while `bonded_atoms` pays 100.7 ms against # 589.6 ms. Membership is O(1); only the attributes below are walked. # # `tests/basic/get/test_native_scalar_delivery.py` sweeps the whole attribute # catalogue and fails if an attribute outside this set delivers NumPy scalars, so a # new one cannot be forgotten silently. _ATTRIBUTES_WITH_NUMPY_SCALARS = frozenset( { "bonded_atoms", "bonded_atom_pairs", "inner_bonded_atoms", "inner_bonded_atom_pairs", "chain_index", "chain_name", "chain_type", "component_index", "entity_index", "group_index", "molecule_index", "n_amino_acids", "n_dnas", "n_ions", "n_lipids", "n_nucleotides", "n_peptides", "n_polysaccharides", "n_proteins", "n_rnas", "n_saccharides", "n_small_molecules", "n_waters", } ) def _coerce_native_scalars(value): """Replace NumPy scalars with native Python ones, preserving container and shape. `ndarray` and `Quantity` values are returned untouched: NumPy scalars belong inside a typed buffer, and those are the numeric magnitudes the rule keeps there. """ if isinstance(value, np.ndarray): return value if isinstance(value, np.generic): return value.item() if isinstance(value, list): return [_coerce_native_scalars(item) for item in value] if isinstance(value, tuple): return tuple(_coerce_native_scalars(item) for item in value) if isinstance(value, set): return {_coerce_native_scalars(item) for item in value} if isinstance(value, dict): return { _coerce_native_scalars(k): _coerce_native_scalars(v) for k, v in value.items() } return value def _coerce_ids_to_string(value): """Normalize present *_id values to strings while preserving missing IDs.""" if value is None: return None if is_iterable_of_iterables(value): return [_coerce_ids_to_string(aux_value) for aux_value in value] else: arr = np.asarray(value) if arr.shape == (): if arr.item() is None: return None return str(arr.astype(str)) else: if arr.dtype.kind == "O" and any(item is None for item in arr.flat): normalized = np.asarray( [None if item is None else str(item) for item in arr.flat], dtype=object, ).reshape(arr.shape) return normalized.tolist() return arr.astype(str).tolist() def _coerce_alternate_location_ids(value): """Normalize atom ID labels without changing source atom-index keys.""" output = [] for structure_dict in value: if structure_dict is None: output.append(structure_dict) continue new_struct = {} for key, entry in structure_dict.items(): new_entry = dict(entry) if "atom_id" in new_entry: new_entry["atom_id"] = _coerce_ids_to_string(new_entry["atom_id"]) new_struct[key] = new_entry output.append(new_struct) return output def _group_attributes_by_element(element, selection, in_attributes): """Group attributes by an unambiguous catalog-compatible element.""" from molsysmt.attribute import attributes output = {} atom_selection_from_system = element == "system" and not is_all(selection) for attribute in in_attributes: supported_elements = attributes[attribute]["get_from"] target_element = element if atom_selection_from_system and "atom" in supported_elements: target_element = "atom" elif supported_elements and element not in supported_elements: if len(supported_elements) == 1: target_element = supported_elements[0] output.setdefault(target_element, []).append(attribute) return output def _piped_molecular_system(molecular_system, element, in_attributes): """Resolve piped attributes across items for get(); returns tuple (item, attribute list) or (None, None).""" from molsysmt.attribute import ( bonds_are_required_to_get_attribute, is_structural_attribute, is_topological_attribute, ) from molsysmt.form import _dict_modules from .. import convert, get_form, where_is_attribute topological_pipes = {} structural_pipes = {} any_pipes = {} form = get_form(molecular_system) if not isinstance(molecular_system, (list, tuple)): molecular_system = [molecular_system] form = [form] for aux_form in form: topological_pipes[aux_form] = getattr( _dict_modules[aux_form], "piped_topological_attribute" ) structural_pipes[aux_form] = getattr( _dict_modules[aux_form], "piped_structural_attribute" ) any_pipes[aux_form] = getattr(_dict_modules[aux_form], "piped_any_attribute") not_piped = ( all([ii is None for ii in topological_pipes.values()]) & all([ii is None for ii in structural_pipes.values()]) & all([ii is None for ii in any_pipes.values()]) ) single_attribute_has_direct_getter = False if len(in_attributes) == 1: in_attribute = next(iter(in_attributes)) _, aux_form = where_is_attribute( molecular_system, in_attribute, skip_digestion=True ) if aux_form is not None: getter_name = f"get_{in_attribute}_from_{element}" single_attribute_has_direct_getter = ( getattr(_dict_modules[aux_form], getter_name, None) is not None ) if not single_attribute_has_direct_getter: from molsysmt._private.attribute_derivation import can_derive_attribute single_attribute_has_direct_getter = can_derive_attribute( _dict_modules[aux_form], in_attribute, element, ) if not_piped or single_attribute_has_direct_getter: return None, None else: aux_topological_attributes = [] aux_topological_pipes = [] aux_structural_attributes = [] aux_structural_pipes = [] aux_any_pipes = [] bonds_required_by_attributes = False for in_attribute in in_attributes: bonds_required_by_attributes += bonds_are_required_to_get_attribute( in_attribute, element, skip_digestion=True ) if is_topological_attribute(in_attribute, skip_digestion=True): aux_topological_attributes.append(in_attribute) _, aux_form = where_is_attribute( molecular_system, in_attribute, skip_digestion=True ) if aux_form is not None: if topological_pipes[aux_form] is not None: if topological_pipes[aux_form] not in aux_topological_pipes: aux_topological_pipes.append(topological_pipes[aux_form]) if any_pipes[aux_form] is not None: if any_pipes[aux_form] not in aux_any_pipes: aux_any_pipes.append(any_pipes[aux_form]) elif is_structural_attribute(in_attribute, skip_digestion=True): _, aux_form = where_is_attribute(molecular_system, in_attribute) aux_structural_attributes.append(in_attribute) if aux_form is not None: if structural_pipes[aux_form] is not None: if structural_pipes[aux_form] not in aux_structural_pipes: aux_structural_pipes.append(structural_pipes[aux_form]) if any_pipes[aux_form] is not None: if any_pipes[aux_form] not in aux_any_pipes: aux_any_pipes.append(any_pipes[aux_form]) n_top_pipes = len(aux_topological_pipes) n_str_pipes = len(aux_structural_pipes) n_any_pipes = len(aux_any_pipes) n_top_atts = len(aux_topological_attributes) n_str_atts = len(aux_structural_attributes) output_systems = [] output_attributes = [] if n_top_pipes == 0 and n_str_pipes == 0 and n_any_pipes == 0: output_systems = None output_attributes = None elif n_top_atts > 0 and n_str_atts == 0: if n_top_pipes == 1: aux_molecular_system = convert( molecular_system, to_form=aux_topological_pipes[0], get_missing_bonds=bonds_required_by_attributes, skip_digestion=True, ) else: aux_molecular_system = convert( molecular_system, to_form="molsysmt.Topology", get_missing_bonds=bonds_required_by_attributes, skip_digestion=True, ) output_systems.append(aux_molecular_system) output_attributes.append(aux_topological_attributes) elif n_top_atts == 0 and n_str_atts > 0: if n_str_pipes == 1: aux_molecular_system = convert( molecular_system, to_form=aux_structural_pipes[0], skip_digestion=True, ) else: aux_molecular_system = convert( molecular_system, to_form="molsysmt.Structures", skip_digestion=True ) output_systems.append(aux_molecular_system) output_attributes.append(aux_structural_attributes) else: if n_any_pipes == 1: aux_molecular_system = convert( molecular_system, to_form=aux_any_pipes[0], get_missing_bonds=bonds_required_by_attributes, skip_digestion=True, ) output_systems.append(aux_molecular_system) output_attributes.append( aux_topological_attributes + aux_structural_attributes ) elif n_any_pipes > 1: aux_molecular_system = convert( molecular_system, to_form="molsysmt.MolSys", get_missing_bonds=bonds_required_by_attributes, skip_digestion=True, ) output_systems.append(aux_molecular_system) output_attributes.append( aux_topological_attributes + aux_structural_attributes ) elif n_any_pipes == 0: if n_top_pipes == 1: aux_molecular_system = convert( molecular_system, to_form=aux_topological_pipes[0], get_missing_bonds=bonds_required_by_attributes, skip_digestion=True, ) elif n_top_pipes > 1: aux_molecular_system = convert( molecular_system, to_form="molsysmt.Topology", get_missing_bonds=bonds_required_by_attributes, skip_digestion=True, ) else: aux_molecular_system = None output_systems.append(aux_molecular_system) output_attributes.append(aux_topological_attributes) if n_str_pipes == 1: aux_molecular_system = convert( molecular_system, to_form=aux_structural_pipes[0], skip_digestion=True, ) elif n_str_pipes > 1: aux_molecular_system = convert( molecular_system, to_form="molsysmt.Structures", skip_digestion=True, ) else: aux_molecular_system = None output_systems.append(aux_molecular_system) output_attributes.append(aux_structural_attributes) if output_systems is not None: covered = {attribute for group in output_attributes for attribute in group} remaining = [ attribute for attribute in in_attributes if attribute not in covered ] if remaining: # Reduced topology/structure pipes cannot deliver other domains. # Query them on the original source instead of omitting their keys. output_systems.append(None) output_attributes.append(remaining) return output_systems, output_attributes