Source code for ccdc.entry

#
# This code is Copyright (C) 2015 The Cambridge Crystallographic Data Centre
# (CCDC) of 12 Union Road, Cambridge CB2 1EZ, UK and a proprietary work of CCDC.
# This code may not be used, reproduced, translated, modified, disassembled or
# copied, except in accordance with a valid licence agreement with CCDC and may
# not be disclosed or redistributed in any form, either in whole or in part, to
# any third party. All copies of this code made in accordance with a valid
# licence agreement as referred to above must contain this copyright notice.
#
# No representations, warranties, or liabilities are expressed or implied in the
# supply of this code by CCDC, its servants or agents, except where such
# exclusion or limitation is prohibited, void or unenforceable under governing
# law.
#
'''
The main class of the :mod:`ccdc.entry` module is :class:`ccdc.entry.Entry`.

A :class:`ccdc.entry.Entry` is often a CSD entry. It contains attributes that
are beyond the concepts of chemistry and crystallography. An example of such
an attribute would be the publication details of a CSD entry.

.. code-block:: python

>>> from ccdc.io import EntryReader
>>> csd_reader = EntryReader('CSD')
>>> abebuf = csd_reader.entry('ABEBUF')
>>> print(abebuf.publication)  # doctest: +NORMALIZE_WHITESPACE
Citation(authors='S.W.Gordon-Wylie, E.Teplin, J.C.Morris, M.I.Trombley, S.M.McCarthy, W.M.Cleaver, G.R.Clark',
         journal='Journal(Crystal Growth and Design)',
         volume='4', year=2004, first_page='789',
         doi='10.1021/cg049957u')

However, a :class:`ccdc.entry.Entry` does not necessarily have to be a CSD
entry. If, for example, a sdf file is read in using a
:class:`ccdc.io.EntryReader` then the sdf tags will be added to a
dictionary-like object named ``attributes`` of the entry. Entries read in from
CIF files will also contain ``attributes`` with the raw data from the CIF
file.

'''
#############################################################################

import warnings
warnings.filterwarnings('always', '.*deprecated.*', DeprecationWarning, '.*', 0)

import collections
import datetime
import functools
import re

from ccdc.crystal import Crystal
from ccdc.molecule import Molecule, _file_object_factory
from ccdc.utilities import nested_class, _detect_format

from ccdc.utilities import _private_importer
with _private_importer():
    import UtilitiesLib
    import DatabaseEntryLib
    import SolubilityPlatformLib
    import ChemicalAnalysisLib
    import ChemistryLib
    import FileFormatsLib
    import MathsLib

#############################################################################

Citation = collections.namedtuple('Citation', [
    'authors',
    'journal',
    'volume',
    'year',
    'first_page',
    'doi'
])
def hash_citation(cit):
    '''hash a citation, rather than using the default, tuple hash.'''
    return hash(str(cit))
Citation.__hash__ = hash_citation

def print_citation(citation):
    return "Citation(authors='%s', journal='%s', volume='%s', year=%d, first_page='%s', doi=%s)" % (citation.authors, citation.journal, citation.volume, citation.year, citation.first_page, 'None' if citation.doi is None else '\'%s\'' % citation.doi)

Citation.__repr__ = print_citation

[docs]class Entry(object): '''A database entry.''' _radiation_source_map = dict([ (DatabaseEntryLib.ExperimentalInfo.UNKNOWN_PROBE, 'Unknown'), (DatabaseEntryLib.ExperimentalInfo.X_RAY, 'X-ray'), (DatabaseEntryLib.ExperimentalInfo.NEUTRON, 'Neutron'), (DatabaseEntryLib.ExperimentalInfo.ELECTRON, 'Electron'), (DatabaseEntryLib.ExperimentalInfo.GAMMA, 'Gamma ray') ]) _KELVIN = "K" _CENTIGRADE = "deg.C"
[docs] @nested_class('Entry') class CrossReference(object): '''A cross-reference between entries in the database.''' def __init__(self, _xr): '''Private: constructs the cross-reference.''' self._xr = _xr def __str__(self): return self.text def __repr__(self): return 'CrossReference(%s)' % self.text @property def text(self): '''The text of the cross-reference.''' return self._xr.text() @property def scope(self): '''Whether the cross-reference applies to the individual identifier or the family of related identifiers.''' return { self._xr.INDIVIDUAL: 'Individual', self._xr.FAMILY : 'Family' }[self._xr.scope()] @property def type(self): '''The type of cross-reference. Available types are 'Unknown', 'Racemate', 'Stereoisomer', 'Isomer', 'Reinterpretation of', 'Reinterpretation ref', and 'Coordinates ref'. ''' return { self._xr.UNKNOWN : 'Unknown', self._xr.RACEMATE : 'Racemate', self._xr.STEREOMER : 'Stereoisomer', self._xr.ISOMER : 'Isomer', self._xr.REINTERPRETATION_OF : 'Reinterpretation of', self._xr.REINTERPRETATION_SEE : 'Reinterpretation ref', self._xr.COORDINATES_SEE : 'Coordinates ref' }[self._xr.type()] @property def identifiers(self): '''A tuple containing identifiers of the cross-referenced entries.''' return tuple(self._xr.identifiers())
class _CifAttributes(object): '''Private: dict-like access to lazily computed Cif data items.''' def __init__(self, _data_block): '''Initialisation''' self._data_block = _data_block self._fetched = dict() def __getitem__(self, name): '''See if it's present in the dict, or go and fetch it, or fail.''' if name not in self._fetched: v = self._data_block.obtain(name) if not v.has_value(): raise AttributeError('This entry does not have the attribute %s' % name) if self._data_block.is_looped(name): val = [v.value(i) if v.has_significant_value(i) else None for i in range(v.size())] else: val = v.value() if v.has_significant_value() else None self._fetched[name] = val return self._fetched[name] def get_raw_item(self, name): '''Provide the literal text of an item so as to distinguish values deemed insignificant.''' v = self._data_block.obtain(name) if not v.has_value(): raise AttributeError('This entry does not have the attribute %s' % name) if self._data_block.is_looped(name): val = [v.value(i) for i in range(v.size())] else: val = v.value() return val def __setitem__(self, name, value): '''Set the item in the dict-like class.''' if name in self._fetched: self._fetched[name] = str(value) if hasattr(value, '__iter__') and not isinstance(value, str): # Stick a looped item in there self._data_block.create_loop((name,)) item = self._data_block.obtain(name) for v in value: item.add_value('%s' % v) else: self._data_block.set_value(name, '%s' % value) v = self._data_block.obtain(name).value() def __delitem__(self, name): '''Remove the item from the dict-like class.''' if name in self._fetched: del self._fetched[name] self._data_block.remove(name) def keys(self): return self._data_block.data_item_names() def values(self): return [self[n] for n in self.keys()] def items(self): return list(zip(self.keys(), self.values())) def update(self, other): if isinstance(other, Entry._CifAttributes): for k in other.keys(): self[k] = other.get_raw_item(k) else: for k, v in other.items(): self[k] = v def __len__(self): return len(self.keys()) def __contains__(self, n): return n in self.keys()
[docs] @staticmethod def from_molecule(mol, **attributes): '''Construct an entry from a molecule, using the keyword arguments as attributes.''' entry = DatabaseEntryLib.CrystalStructureImmediateDatabaseEntry( UtilitiesLib.DatabaseEntryIdentifier(mol.identifier) ) if hasattr(mol, '_crystal'): c = mol._crystal flag = DatabaseEntryLib.EditorsInfo.PERFECT_MATCH else: c = ChemistryLib.ConcreteCrystalStructure() c.set_editable_molecule(mol._molecule.create_editable_molecule()) if hasattr(mol, '_cell'): cell = mol._cell else: cell = ChemistryLib.Cell() c.set_cell( cell, ChemistryLib.CrystalStructure.KEEP_ORTHOGONAL_COORDINATES ) c.update_crystal_symmetry_match() cdg = ChemistryLib.ChemicalDiagramGenerator() diag = cdg.create_chemical_diagram(mol._molecule) ChemistryLib.normalise_diagram(diag) cdv = ChemistryLib.ChemicalDiagramViews2D(diag) entry.set_chemical_diagram_views(cdv) matcher = DatabaseEntryLib.CrystalChemicalDiagramMatcher( entry.crystal_structure(), cdv ) match = matcher.crystal_diagram_match() entry.set_crystal_diagram_match(match) if matcher.crystal_structure_matched() and matcher.chemical_diagram_matched(): flag = DatabaseEntryLib.EditorsInfo.PERFECT_MATCH elif matcher.crystal_diagram_match().nnode_matches() == 0: flag = DatabaseEntryLib.EditorsInfo.NOT_MATCHED else: flag = DatabaseEntryLib.EditorsInfo.PARTIAL_MATCH entry.set_crystal_structure(c) entry.set_editors_info(DatabaseEntryLib.EditorsInfo()) entry.editors_info().set_aser_match_flag(flag) entry.editors_info().set_accession_date( UtilitiesLib.Date.today() ) entry.editors_info().set_modification_date( UtilitiesLib.Date.today() ) chemical_info = DatabaseEntryLib.ChemicalInfo() chemical_info.set_formula(mol.formula) entry.set_chemical_info(chemical_info) e = Entry(entry) e.attributes = dict() e.attributes.update(attributes) return e
def _make_diagram(self): cdg = ChemistryLib.ChemicalDiagramGenerator() m = self._entry.crystal_structure().editable_molecule() diag = cdg.create_chemical_diagram(m) ChemistryLib.normalise_diagram(diag) cdv = ChemistryLib.ChemicalDiagramViews2D(diag) match = DatabaseEntryLib.CrystalDiagramMatch() for i in range(m.natoms()): match.add_node_match(m.atom(i), diag.atom(i)) for i in range(m.nbonds()): match.add_edge_match(m.bond(i), diag.bond(i)) self._entry.set_chemical_diagram_views(cdv) self._entry.set_crystal_diagram_match(match) def __init__(self, _entry=None): '''_entry should be a toolkit CrystalStructureDatabaseEntry.''' self._entry = _entry if hasattr(self._entry, 'chemical_diagram') and self._entry.chemical_diagram() is None: try: self._make_diagram() except RuntimeError: pass self.attributes = dict() def __eq__(self, other): '''Equality of entries.''' return isinstance(other, Entry) and self.identifier == other.identifier def __hash__(self): '''So entries may be placed in dictionaries.''' return hash(self.identifier) @property def deposition_date(self): '''The date when this entry was deposited as a datetime.date object or None if the date is not available. ''' d = self._entry.editors_info().accession_date() return datetime.date(d.year(), d.month(), d.day()) if d.year() > 0 else None @property def identifier(self): '''The string identifier of the entry, e.g. 'ABEBUF'.''' return self._entry.identifier().str() @identifier.setter def identifier(self, value): self._entry.set_identifier(UtilitiesLib.DatabaseEntryIdentifier(value)) @property def chemical_name(self): '''The chemical name of the entry. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acsala13 = csd_reader.entry('ACSALA13') >>> print(acsala13.chemical_name) 2-acetoxybenzoic acid ''' if self._entry.chemical_info(): name = self._entry.chemical_info().name().plain() if name: return name @property def chemical_name_as_html(self): u'''The chemical name of the entry formatted as HTML. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> e = csd_reader.entry('AACRHA') >>> print(e.chemical_name_as_html) Tetra-ammonium bis(bis(\u03bc<sub>2</sub>-acetato-O,O')-bromo-rhodium(iv)) dibromide ''' if self._entry.chemical_info(): name = self._entry.chemical_info().name().html() if name: return name @chemical_name.setter def chemical_name(self, value): chemical_name = DatabaseEntryLib.ChemicalName.from_plain(value) if self._entry.chemical_info(): self._entry.chemical_info().set_name(chemical_name) else: chemical_info = DatabaseEntryLib.ChemicalInfo() chemical_info.set_name(chemical_name) self._entry.set_chemical_info(chemical_info) @property def formula(self): '''The published chemical formula in an entry. If no published chemical formula is available it will be calculated from the molecule. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aacani10 = csd_reader.entry('AACANI10') >>> print(aacani10.formula) C10 H18 N2 Ni1 O5,2(H2 O1) ''' if self._entry.chemical_info(): return self._entry.chemical_info().formula() else: return self.crystal.formula @formula.setter def formula(self, value): if self._entry.chemical_info(): self._entry.chemical_info().set_formula(value) else: chemical_info = DatabaseEntryLib.ChemicalInfo() chemical_info.set_formula(value) self._entry.set_chemical_info(chemical_info) @property def crystal(self): '''The :class:`ccdc.crystal.Crystal` contained in a database entry.''' return Entry._make_crystal(self._entry) @staticmethod def _make_crystal(e): '''Private: make a crystal from an entry.''' try: c = e.crystal_structure() except RuntimeError as error: de = DatabaseEntryLib.CSDE_as_deferred(e) if de: cds = DatabaseEntryLib.DataSource_as_CifDataSource(de.data_source()) if cds: rw = cds.read_write_options() if rw.read_geom_bonds_: rw.read_geom_bonds_ = False cds.set_read_write_options(rw) c = cds.crystal_structure() else: raise error else: raise error else: raise error identifier = e.identifier().str().strip('_') crystal = Crystal(c, identifier) crystal._crystal_info = e.crystal_info() crystal._chemical_info = e.chemical_info() return crystal @property def molecule(self): '''The :class:`ccdc.molecule.Molecule` contained in a database entry.''' c = self.crystal if self.has_3d_structure: return c.molecule m = Molecule(self.identifier, _molecule=self._entry.two_and_a_half_d_molecule()) for a in m.atoms: a._atom.set_site(None) return m @property def disordered_molecule(self): '''The :class:`ccdc.molecule.Molecule` contained in a database entry, including disordered atoms.''' c = self.crystal if self.has_3d_structure: return self.crystal.disordered_molecule m = Molecule(self.identifier, _molecule=self._entry.two_and_a_half_d_molecule()) m.remove_atoms(a for a in m.atoms if a.label.endswith('?')) for a in m.atoms: a._atom.set_site(None) return m @property def has_3d_structure(self): '''Whether the entry has 3d information. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aqefor = csd_reader.entry('AQEFOR') >>> aqefor.has_3d_structure False ''' try: return self._entry.characteristics().has_3d_structure() except RuntimeError: c = self.crystal return self._entry.characteristics().has_3d_structure() @property def has_disorder(self): '''Whether the structure has disorder. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> abacir = csd_reader.entry('ABACIR') >>> abacir.has_disorder True ''' return self._entry.characteristics().has_disorder() @property def is_organic(self): '''Whether the structure is organic. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aacani10 = csd_reader.entry('AACANI10') >>> aacani10.is_organic False ''' return not self._entry.characteristics().is_organometallic() @property def is_organometallic(self): '''Whether the structure is organometallic. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aacani10 = csd_reader.entry('AACANI10') >>> aacani10.is_organometallic True ''' return self._entry.characteristics().is_organometallic() @property def is_polymeric(self): '''Whether the structure contains polymeric bonds. >>> from ccdc.io import EntryReader >>> csd = EntryReader('CSD') >>> abacuf = csd.entry('ABACUF') >>> abacuf.is_polymeric True ''' if hasattr(self._entry, 'editors_info'): if self._entry.editors_info() is not None: return self._entry.editors_info().polymeric_bonds() def _diagram_is_polymeric(self): '''Private.''' return self._entry.diagram_is_polymeric() @property def bioactivity(self): '''Recorded information about bioactivity if available otherwise ``None``. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aadmpy10 = csd_reader.entry('AADMPY10') >>> print(aadmpy10.bioactivity) antineoplastic activity ''' try: s = self._entry.chemical_info().bioactivity() if s: return s except AttributeError: pass @property def synonyms(self): '''List containing any recorded synonyms for the entry. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acsala13 = csd_reader.entry('ACSALA13') >>> print(' '.join(acsala13.synonyms)) # doctest: +NORMALIZE_WHITESPACE Aspirin DrugBank: DB00945 ''' if self._entry.chemical_info(): return tuple(s.plain() for s in self._entry.chemical_info().synonyms()) else: return () @property def synonyms_as_html(self): u'''Tuple containing any recorded synonyms for the entry formatted as HTML. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aalpro = csd_reader.entry('AALPRO') >>> print(aalpro.synonyms_as_html[0]) \u03b1-Allylprodine hydrochloride ''' if self._entry.chemical_info(): return tuple(s.html() for s in self._entry.chemical_info().synonyms()) else: return () @staticmethod def _make_citation(_p): '''Private.''' doi = _p.doi() if not doi: doi = None return Citation( _p.authors_comma_delimited(), Journal(_p.journal()), _p.volume(), _p.year(), _p.page(), doi ) @property def publication(self): '''The first publication of a structure. This attribute gives the publication details of a CSD entry, expressed as a tuple of (authors, journal, volume, year, first_page, doi). .. code-block:: python >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aqefor = csd_reader.entry('AQEFOR') >>> print(aqefor.publication) # doctest: +NORMALIZE_WHITESPACE Citation(authors='M.E.Bluhm, M.Ciesielski, H.Gorls, O.Walter, M.Doring', journal='Journal(Inorganic Chemistry)', volume='42', year=2003, first_page='8878', doi='10.1021/ic034773a') :rtype: Citation ''' try: p = self._entry.publication() return self._make_citation(p) except AttributeError: pass @property def publications(self): '''All publications of the entry.''' try: return tuple(self._make_citation(p) for p in self._entry.publications()) except (AttributeError, RuntimeError): return None def _fetch_datum(self, whence, what): '''Private: fish out a datum from an entry component.''' if hasattr(self._entry, whence): g = getattr(self._entry, whence)() if g is not None: if hasattr(g, what): s = getattr(g, what)() if s: return s @property def previous_identifier(self): '''Previous identifier if any. >>> from ccdc.io import EntryReader >>> entry_reader = EntryReader('CSD') >>> acpret03 = entry_reader.entry('ACPRET03') >>> print(acpret03.identifier) ACPRET03 >>> print(acpret03.previous_identifier) DABHUJ ''' return self._fetch_datum('editors_info', 'previous_refcode') @property def color(self): '''The colour of the crystal if given, otherwise ``None``. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acesor = csd_reader.entry('ACESOR') >>> print(acesor.color) yellow ''' return self._fetch_datum('crystal_info', 'color') @property def database_name(self): """The name of the source database. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acesor = csd_reader.entry('ACESOR') >>> print(acesor.database_name) # doctest: +SKIP as536be_ASER """ return self._entry.database_name() @property def polymorph(self): '''Polymorphic information about the crystal if given otherwise ``None``. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acsala13 = csd_reader.entry('ACSALA13') >>> print(acsala13.polymorph) polymorph II ''' return self._fetch_datum('crystal_info', 'polymorph') @polymorph.setter def polymorph(self, value): if self._entry.crystal_info(): self._entry.crystal_info().set_polymorph(value) else: crystal_info = DatabaseEntryLib.CrystalInfo() crystal_info.set_polymorph(value) self._entry.set_crystal_info(crystal_info) def _melting_point_parser(self): '''Private''' if self._entry.crystal_info() is None: crystal_info = DatabaseEntryLib.CrystalInfo() self._entry.set_crystal_info(crystal_info) return self._entry.crystal_info().melting_point_parser() @property def melting_point(self): '''Melting point of the crystal if given otherwise ``None``. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acsala13 = csd_reader.entry('ACSALA13') >>> print(acsala13.melting_point) 135.5deg.C ''' return self._fetch_datum('crystal_info', 'melting_point') @property def melting_point_display_string(self): '''The parsed and formatted melting point string.''' return self._melting_point_parser().display_string() @property def melting_point_default_units(self): '''Return the default melting point units.''' return Entry._KELVIN @property def formatted_melting_point_range(self): '''Get the formatted melting point in default units. :returns: a three-item tuple containing the minimum value, maximum value and units string. If the melting point is a single value the min and max will be set to the same value. ''' mpp = self._melting_point_parser() return mpp.min(), mpp.max(), Entry._KELVIN @property def formatted_melting_point_text(self): '''Get the formatted melting point descriptive text.''' return self._melting_point_parser().descriptive_text() @property def input_melting_point_range(self): '''Set or get the original input unparsed melting point range. When setting pass in a tuple e.g. (285,) no max value provided, use the default units. (28.2, None, "deg.C") single value, use degrees centigrade (28.2, 29.5, "deg.C") min, max, use degrees centigrade (275, 278) min and max, default units (275, 278, "K") min and max in Kelvin ''' min_max = self._fetch_datum('crystal_info', 'melting_point_range') return min_max.minimum_value(), min_max.maximum_value(), self._fetch_datum('crystal_info', 'melting_point_units') @input_melting_point_range.setter def input_melting_point_range(self, value): if self._entry.crystal_info() is None: crystal_info = DatabaseEntryLib.CrystalInfo() self._entry.set_crystal_info(crystal_info) try: mp1 = float(value[0]) if len(value) > 1: mp2 = float(value[1]) if value[1] is not None else float(value[0]) else: mp2 = float(value[0]) except Exception as exc: raise ValueError(str(exc)) crystal_info = self._entry.crystal_info() crystal_info.set_melting_point_range(MathsLib.DoubleRange.inclusive(mp1, mp2)) # set the units if len(value) == 3: crystal_info.set_melting_point_units(value[2]) else: crystal_info.set_melting_point_units(Entry._KELVIN) @property def input_melting_point_text(self): '''Get or set the input melting point text.''' return self._fetch_datum('crystal_info', 'melting_point_text') @input_melting_point_text.setter def input_melting_point_text(self, value): if self._entry.crystal_info() is None: crystal_info = DatabaseEntryLib.CrystalInfo() self._entry.set_crystal_info(crystal_info) self._entry.crystal_info().set_melting_point_text(value) @property def disorder_details(self): '''Information about any disorder present if given otherwise ``None``. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> abacir = csd_reader.entry('ABACIR') >>> print(abacir.disorder_details) O4 and O4A disordered over two sites with occupancies 0.578:0.422. ''' try: s = self._entry.crystal_info().disorder_details().display_text() if s: return s except AttributeError: pass @property def solvent(self): '''Recrystallisation solvent. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> rechul = csd_reader.entry('REKHUL') >>> print(rechul.solvent) pentane ''' return self._fetch_datum('crystal_info', 'recrystallisation_solvent') @property def radiation_source(self): '''The radiation source of the crystal's determination. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> aabhtz = csd_reader.entry('AABHTZ') >>> aabhtz.radiation_source 'X-ray' >>> csd_reader.entry('ABINOR01').radiation_source 'Neutron' ''' try: source = self._entry.experimental_info().radiation_probe() except AttributeError: return 'Unknown' else: return Entry._radiation_source_map[source] @property def r_factor(self): '''Resolution of crystallographic determination, given as a percentage value. >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acsala13 = csd_reader.entry('ACSALA13') >>> acsala13.r_factor 16.22 ''' try: s = self._entry.experimental_info().r_factor() except AttributeError: try: s = float(self.attributes['_refine_ls_R_factor_gt']) * 100. except KeyError: return None if s != 0.0: return s @property def temperature(self): '''Experimental temperature of the entry >>> from ccdc.io import EntryReader >>> csd_reader = EntryReader('CSD') >>> acsala13 = csd_reader.entry('ACSALA13') >>> print(acsala13.temperature) at 100 K ''' try: s = self._entry.experimental_info().temperature_text() except AttributeError: try: s = self.attributes['_diffrn_ambient_temperature'] except KeyError: return None if s: return s def _experimental_info(self): if self._entry.experimental_info() is None: experimental_info = DatabaseEntryLib.ExperimentalInfo() self._entry.set_experimental_info(experimental_info) return self._entry.experimental_info() @property def calculated_density(self): '''The density.''' try: calculated_density = self._experimental_info().calculated_density().value() return calculated_density if calculated_density != 0.0 else None except AttributeError: pass @property def heat_capacity(self): '''Get or set the heat capacity. :returns: a two-item tuple containing the heat capacity value and units When setting, pass a tuple with either a single value, or a value and a units string. >>> from ccdc.entry import Entry >>> e=Entry.from_string("OCO") >>> e.heat_capacity=(54.55,) >>> e.heat_capacity (54.55, '') >>> e.heat_capacity=(54.55,'J/K') >>> e.heat_capacity (54.55, 'J/K') ''' try: cp = self._experimental_info().solubility_info().heat_capacity() cp_unit = self._entry.experimental_info().solubility_info().heat_capacity_units() return cp, cp_unit except AttributeError: pass @heat_capacity.setter def heat_capacity(self, value): try: cp = float(value[0]) except Exception as exc: raise ValueError(str(exc)) experimental_info = self._experimental_info() solubility_info = experimental_info.solubility_info() solubility_info.set_heat_capacity(cp) # set the units if len(value) == 2: solubility_info.set_heat_capacity_units(value[1]) experimental_info.set_solubility_info(solubility_info) @property def heat_capacity_notes(self): '''Get or set the notes on the heat capacity >>> from ccdc.entry import Entry >>> e=Entry.from_string("OCO") >>> e.heat_capacity_notes '' >>> e.heat_capacity_notes='from Wikipedia, at 189.78K' >>> e.heat_capacity_notes 'from Wikipedia, at 189.78K' ''' try: return self._experimental_info().solubility_info().heat_capacity_notes() except AttributeError: pass @heat_capacity_notes.setter def heat_capacity_notes(self, value): experimental_info = self._experimental_info() solubility_info = experimental_info.solubility_info() solubility_info.set_heat_capacity_notes(value) experimental_info.set_solubility_info(solubility_info) @property def heat_of_fusion(self): '''Get or set the heat of fusion A tuple is required to set this. The first item is a value or lower bound for heat of fusion. The optional second item is an upper bound for heat of fusion, or None. The optional third item is units for the heat of fusion. >>> from ccdc.entry import Entry >>> e=Entry.from_string("OCO") >>> e.heat_of_fusion = (9.019,) >>> e.heat_of_fusion (9.019, 9.019, '') >>> e.heat_of_fusion = (9,9.1) >>> e.heat_of_fusion (9.0, 9.1, '') >>> e.heat_of_fusion = (9,9.1,'KJ/mol') >>> e.heat_of_fusion (9.0, 9.1, 'KJ/mol') ''' try: hf_values = self._experimental_info().solubility_info().heat_of_fusion() hf_unit = self._experimental_info().solubility_info().heat_of_fusion_units() return hf_values.minimum_value(), hf_values.maximum_value(), hf_unit except AttributeError: pass @heat_of_fusion.setter def heat_of_fusion(self, value): try: hf1 = hf2 = float(value[0]) if len(value) > 1 and value[1] is not None: hf2 = float(value[1]) except Exception as exc: raise ValueError(str(exc)) experimental_info = self._experimental_info() solubility_info = experimental_info.solubility_info() solubility_info.set_heat_of_fusion(MathsLib.DoubleRange.inclusive(hf1, hf2)) # set the units if len(value) == 3: solubility_info.set_heat_of_fusion_units(value[2]) experimental_info.set_solubility_info(solubility_info) @property def heat_of_fusion_notes(self): '''Get or set the notes of heat of fusion''' try: return self._experimental_info().solubility_info().heat_of_fusion_notes() except AttributeError: pass @heat_of_fusion_notes.setter def heat_of_fusion_notes(self, value): experimental_info = self._experimental_info() solubility_info = experimental_info.solubility_info() solubility_info.set_heat_of_fusion_notes(value) experimental_info.set_solubility_info(solubility_info) @property def solubility_data(self): '''Get or set the solubility data, a list of :class:`ccdc.entry.SolubilityMeasurement` >>> from ccdc.entry import Entry, SolubilityMeasurement >>> e=Entry.from_string('CC(C)Cc1ccc(cc1)C(C)C(O)=O') >>> sol = SolubilityMeasurement('21-22', 25, 'mg/L', 'deg.C', 'from PubChem') >>> sol.add_solvent('water', 100) >>> e.solubility_data = [sol] >>> e.solubility_data [SolubilityMeasurement(21 - 22, 25.0, "mg/L", "deg.C", from PubChem)] >>> e.solubility_data[0].solvents (('water', 100.0),) ''' try: experimental_info = self._entry.experimental_info() solubility_info = experimental_info.solubility_info() solubility_measurements = solubility_info.solubility_tests() except AttributeError: solubility_measurements = [] solubility_data = [] for solubility_measurement in solubility_measurements: sol = SolubilityMeasurement( solubility_measurement.solubility(), solubility_measurement.temperature(), solubility_measurement.solubility_units(), solubility_measurement.temperature_units(), solubility_measurement.notes() ) solvent_ratios = solubility_measurement.solvents() for solvent_ratio in solvent_ratios: sol.add_solvent(solvent_ratio[0], solvent_ratio[1]) solubility_data.append(sol) return solubility_data @solubility_data.setter def solubility_data(self, value): experimental_info = self._experimental_info() solubility_info = experimental_info.solubility_info() solubility_data = [] for solubility_measurement in value: solvent_data = [] for solvent in solubility_measurement.solvent_ratios: sd = SolubilityPlatformLib.SolventData(solvent[0], solvent[1]) solvent_data.append(sd) measurement = SolubilityPlatformLib.SolubilityData( solubility_measurement.solubility._sol, solubility_measurement.solubility_unit, float(solubility_measurement.temperature), solubility_measurement.temperature_unit, solubility_measurement.notes, solvent_data ) solubility_data.append(measurement) solubility_info.set_solubility_tests(solubility_data) experimental_info.set_solubility_info(solubility_info) @property def habit(self): '''The crystal habit. ''' return self._fetch_datum('crystal_info', 'habit') @property def ccdc_number(self): '''The CCDC deposition number. >>> from ccdc.io import EntryReader >>> entry_reader = EntryReader('CSD') >>> abebuf = entry_reader.entry('ABEBUF') >>> print(abebuf.ccdc_number) 241370 ''' try: s = self._entry.deposition_info().text(DatabaseEntryLib.DepositionInfo.CCDC_NUMBER) except AttributeError: return None if s: try: x = int(s.split()[1]) except: x = None return x @property def doi(self): # '''The CCDC Document Object Identifier (DOI) or `None` if not available. # # This property is not implemented for all CCDC database formats. # # >>> from ccdc.io import EntryReader # >>> entry_reader = EntryReader('CSD') # >>> namsav = entry_reader.entry('NAMSAV') # >>> print(namsav.doi) # 10.5517/cc3c1yf # # ''' # For current, public formats of the CSD this property is always `None`. try: doi = self._entry.deposition_info().text(DatabaseEntryLib.DepositionInfo.DOI) except AttributeError: return None return doi @property def source(self): '''The source of the compound(s) in the entry. >>> from ccdc.io import EntryReader >>> entry_reader = EntryReader('CSD') >>> fifdut = entry_reader.entry('FIFDUT') >>> print(fifdut.source) dried venom of Chinese toad Ch'an Su ''' return self._fetch_datum('chemical_info', 'source') @property def remarks(self): '''Any remarks on the entry registered by the editors of the database. These may include details like a US Patent number: >>> from ccdc.io import EntryReader >>> csd = EntryReader('csd') >>> print(csd.entry('ARISOK').remarks) U.S. Patent: US 6858644 B2 or reflect editorial decisions: >>> print(csd.entry('ABAPCU').remarks) The position of the hydrate is dubious. It has been deleted ''' return self._fetch_datum('editors_info', 'remarks') @property def pressure(self): '''The experimental pressure of the crystallisation of the entry, where known. This is a text field. If ``None`` the experiment was performed at ambient pressure or not recorded. >>> from ccdc.io import EntryReader >>> csd = EntryReader('csd') >>> print(csd.entry('AABHTZ').pressure) None >>> print(csd.entry('ABULIT03').pressure) at 1.4 GPa ''' return self._fetch_datum('experimental_info', 'pressure') @property def is_powder_study(self): '''Whether or not the crystal determination was performed on a powder study >>> from ccdc.io import EntryReader >>> csd = EntryReader('csd') >>> print(csd.entry('AABHTZ').is_powder_study) False >>> print(csd.entry('ACATAA').is_powder_study) True ''' b = self._fetch_datum('experimental_info', 'powder') return b if b is None else b != 1 @property def peptide_sequence(self): '''The peptide sequence of the entry if any.''' return self._fetch_datum('chemical_info', 'peptide_sequence') @property def phase_transition(self): '''Phase transition of the entry.''' return self._fetch_datum('crystal_info', 'phase_transitions') @property def analogue(self): '''Analogue information.''' return self._fetch_datum('crystal_info', 'analogues') @property def cross_references(self): '''The tuple of :class:`ccdc.entry.Entry.CrossReference` for this entry. These are cross-references between entries of the CSD.''' try: cross_reference = self._entry.cross_reference_info() if cross_reference: return tuple( Entry.CrossReference(cross_reference.cross_reference(i)) for i in range(cross_reference.size()) ) except AttributeError: pass return ()
[docs] def to_string(self, format='mol2'): '''Return a string representation of an entry. :param format: 'mol2', 'sdf', 'mol' or 'cif' :rtype: string :raises: TypeError if the format is not 'mol2', 'sdf', 'mol' or 'cif' ''' stream = FileFormatsLib.ostringstream() mf = _file_object_factory(format) mf.set(self._entry) if hasattr(self, 'attributes'): if format == 'mol2': comments = FileFormatsLib.Mol2Comment() for k, v in self.attributes.items(): comments.add_comment_line('> <%s>' % k) for l in str(v).split('\n'): comments.add_comment_line(l) comments.add_comment_line('') mf.add_comment(comments) elif format in ['sdf', 'mol']: for k, v in self.attributes.items(): mf.add_sd_tag(k, str(v)) mf.write(stream) return stream.str()
[docs] @staticmethod def from_string(s, format=''): '''Create an entry from a string representation. The format will be auto-detected if not specified. :param s: string representation of an entry, crystal or molecule :param format: one of 'mol2', 'sdf', 'mol' or 'cif' :returns: a :class:`ccdc.entry.Entry` :raises: TypeError if the format string is not '', 'mol2', 'sdf', 'mol', 'cif' or 'smiles'. :raises: RuntimeError if the string representation is incorrectly formatted ''' if format == '': format = _detect_format(s) if format in ['sdf', 'mol']: if 'V2000' not in s and 'M END' not in s and '$$$$' not in s: raise RuntimeError('This does not appear to be an SDF format string: %s' % s) if format == 'cif': format = 'cifreader' stream = FileFormatsLib.istringstream(str(s)) mf = _file_object_factory(format) mf.read(stream) try: e = Entry(mf.database_entry()) except RuntimeError as exc: if 'There is no data' in str(exc): entry = DatabaseEntryLib.CrystalStructureImmediateDatabaseEntry( mf.identifier() ) e = Entry(_entry=entry) else: raise RuntimeError('Invalid content for {} formatted string: {}'.format(format, s)) if format in ['sdf', 'mol']: e._entry.set_identifier(UtilitiesLib.DatabaseEntryIdentifier(mf.molecule_name())) if format == 'mol2': comments = mf.comments() e.attributes = dict() lines = [] for c in comments: lines.extend(c.comment_lines()) i = 0 while i < len(lines): k = lines[i] if k.startswith('> <'): k = k[3:].strip()[:-1] v = [] i += 1 while lines[i]: v.append(lines[i]) i += 1 e.attributes[k] = '\n'.join(v) i += 1 elif format in ['sdf', 'mol']: xx = FileFormatsLib.DatabaseEntryToSDfileDatabaseEntry(e._entry) tags = xx.tags() e.attributes = tags return e
##########################################################################
[docs]class Journal(object): '''Information about a journal held in the CSD.''' def __init__(self, _journal): '''Private.''' self._journal = _journal self._sanitised_names = '%s:%s:%s:%s:%s' % ( JournalList._sanitise(self.full_name), JournalList._sanitise(self.abbreviated_name), JournalList._sanitise(self.translated_name), JournalList._sanitise(self.abbreviated_translated_name), JournalList._sanitise(self.international_coden), ) def __str__(self): return 'Journal(%s)' % (self.name if self.full_name == '' else self.full_name) def __repr__(self): return '"%s"' % self.__str__() def __eq__(self, other): '''Equality.''' return self._ccdc_coden == other._ccdc_coden def __lt__(self, other): '''For sorting.''' return self._ccdc_coden < other._ccdc_coden def _fish_out(name, journal): return getattr(journal._journal, name)() abbreviated_name = property( functools.partial(_fish_out, 'abbreviated_name'), doc='The abbreviated name of the journal.' ) abbreviated_translated_name = property( functools.partial(_fish_out, 'abbreviated_translated_name'), doc='The abbreviated translated name of the journal.' ) _ccdc_coden = property( functools.partial(_fish_out, 'ccdc_coden'), doc='The ccdc identifier for the journal.' ) eissn = property( functools.partial(_fish_out, 'eissn'), doc='The electronic international sequence number of the journal.' ) end_year = property( functools.partial(_fish_out, 'end_year'), doc='The date of termination of publication of the journal.' ) full_name = property( functools.partial(_fish_out, 'full_name'), doc='The full name of the journal.' ) image_url = property( functools.partial(_fish_out, 'image_url'), doc='The URL of an image of the journal.' ) international_coden = property( functools.partial(_fish_out, 'int_coden'), doc='The ASTM international identifier of the journal.' ) issn = property( functools.partial(_fish_out, 'issn'), doc='The international sequence number of the journal.' ) language_name = property( functools.partial(_fish_out, 'language_name'), doc="The name of the journal's original language (where possible)." ) name = property( functools.partial(_fish_out, 'name'), doc='The name of the journal.' ) _page_format = property( functools.partial(_fish_out, 'page_format'), doc='The format of a page in the journal.' ) _page_issue = property( functools.partial(_fish_out, 'page_issue'), doc='The page issue of the journal.' ) publisher_name = property( functools.partial(_fish_out, 'publisher_name'), doc='The name of the publisher of the journal.' ) _replacement_coden = property( functools.partial(_fish_out, 'replacement_coden'), doc='The replacement identifier of the journal.' ) start_year = property( functools.partial(_fish_out, 'start_year'), doc='The year of first publication of the journal.' ) @property def state(self): '''Whether or not the journal is current.''' return ['Current', 'Discontinued', 'Unknown'][self._journal.state()] translated_name = property( functools.partial(_fish_out, 'translated_name'), doc='The translated name of the journal.' ) url = property( functools.partial(_fish_out, 'url'), doc="The URL of the journal on the publisher's website." )
[docs]class JournalList(object): '''The collection of journals read from a database.''' def __init__(self, database): '''Instantiate from a database.''' if hasattr(database._db, 'journal_list_info'): try: self._journals = [ Journal(_journal=j) for j in database._db.journal_list_info().journal_list() ] except TypeError: pass if hasattr(database._db, 'journal_info'): self._journals = [ Journal(_journal=j) for j in database._db.journal_info().journal_list() ] else: self._journals = [] def __getitem__(self, index): '''Index method.''' return self._journals[index] def __len__(self): '''The number of journals in the list.''' return len(self._journals) @staticmethod def _sanitise(n): '''Private.''' return ''.join(a for a in n.lower() if a.isalpha()) def _by_member(self, member_name, value): '''Private.''' name = '_dice_by_%s' % member_name if not hasattr(self, name): d = collections.defaultdict(list) def _add(j): d[getattr(j, member_name)].append(j) for j in self._journals: _add(j) setattr(self, name, d) return getattr(self, name)[value] def _by_ccdc_coden(self, value): '''Private: the unique journal for a ccdc_coden.''' return self._by_member('_ccdc_coden', value)[0]
[docs] def by_full_name(self, value): '''The unique journal with a given full name or ``None``.''' l = self._by_member('full_name', value) if l is not None and len(l) == 1: return l[0]
[docs] def by_abbreviated_name(self, value): '''The unique journal with a given abbreviated name or ``None``.''' l = self._by_member('name', value) if l is None or len(l) > 1: return None return l[0]
def _by_regexp_member(self, member_name, regexp): '''Private.''' if isinstance(regexp, str): regexp = re.compile(regexp) return [j for j in self._journals if regexp.match(getattr(j, member_name))]
[docs] def match_full_name(self, regexp): '''Those journals whose full name is matched by the regexp.''' return self._by_regexp_member('full_name', regexp)
[docs] def match_abbreviated_name(self, regexp): '''Those journals matching a regular expression on the abbreviated name.''' return self._by_regexp_member('name', regexp)
##########################################################################
[docs]class SolubilityMeasurement(): '''A solubility measurement. :param solubility: a solubilty value, range tuple or string, see :class:`ccdc.entry.SolubilityMeasurement.Solubility` :param temperature: a measurement temperature value. :param solubility_unit: the solubility units, default mg/mL. :param temperature_unit: the temperature units, default deg.C. :param notes: notes for this measurement. '''
[docs] class Solubility(): '''A solubility value range. This can be created from a single value. Or from a tuple of (lower, upper) bound values. Or from a string describing the range such as "< 15" or "3 - 8". ''' def __init__(self, solubility): DR = SolubilityPlatformLib.DoubleRange if isinstance(solubility, SolubilityMeasurement.Solubility): self._sol = DR(solubility._sol) elif isinstance(solubility, DR): self._sol = DR(solubility) elif isinstance(solubility, tuple): if solubility[0] is None: self._sol = DR.maximum_only(float(solubility[1]), DR.INCLUSIVE) elif solubility[1] is None: self._sol = DR.minimum_only(float(solubility[0]), DR.INCLUSIVE) else: self._sol = DR.inclusive(float(solubility[0]), float(solubility[1])) elif isinstance(solubility, str): parser = SolubilityPlatformLib.RangeParser() parser.set_dash_range_condition(DR.INCLUSIVE) self._sol = parser.parse_as_double(solubility) if not bool(self) and solubility != "" and solubility != "null": raise ValueError(f'"{solubility}" is not recognised as a solubility range or value') else: self._sol = DR.single_value(float(solubility)) def __repr__(self): return self._sol.format("") def __contains__(self, value): return self._sol.contains(value) @property def min(self): '''The minimum solubility value, or None if there is no minimum value.''' if self._sol.minimum_condition() == SolubilityPlatformLib.DoubleRange.UNBOUNDED: return None else: return self._sol.minimum_value() @property def max(self): '''The maximum solubility value, or None if there is no maximum value.''' if self._sol.maximum_condition() == SolubilityPlatformLib.DoubleRange.UNBOUNDED: return None else: return self._sol.maximum_value() def __float__(self): if self.min is not None: return self.min else: return self.max def __bool__(self): return str(self) != "null"
def __init__(self, solubility, temperature, solubility_unit='mg/mL', temperature_unit=Entry._CENTIGRADE, notes=''): self.sol = SolubilityMeasurement.Solubility(solubility) self.temp = temperature self.sol_unit = solubility_unit self.temp_unit = temperature_unit self._notes = notes self.solvent_ratios = ()
[docs] def add_solvent(self, solvent, ratio): '''Add a solvent-ratio to the solubility measurement Run this method for each solvent. :param solvent: name of solvent :param ratio: percentage of solvent ''' s = (solvent, float(ratio)) new_ratios = self.solvent_ratios + (s,) self.solvent_ratios = new_ratios
@property def solubility(self): '''The solubility value, a :class:`ccdc.entry.SolubilityMeasurement.Solubility`''' return self.sol @property def temperature(self): '''The temperature value''' return self.temp @property def solubility_unit(self): '''The solubility unit''' return self.sol_unit @property def temperature_unit(self): '''The temperature unit''' return self.temp_unit @property def notes(self): '''The solubility measurement notes''' return self._notes @property def solvents(self): '''The list of solvents and percentages Each item is returned as a tuple of (name, percentage). ''' return self.solvent_ratios def __repr__(self): return f'SolubilityMeasurement({self.solubility}, {self.temperature}, "{self.solubility_unit}", "{self.temperature_unit}", {self.notes})' def __str__(self): ret = f'{self.solubility} {self.solubility_unit}, {self.temperature} {self.temperature_unit}, "{self.notes}"' for solvent_ratio in self.solvent_ratios: ret += f', {solvent_ratio[0]}, {solvent_ratio[1]}' return ret