#
# 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.
'''
#############################################################################
from ccdc.utilities import _private_importer
from ccdc.utilities import nested_class, _detect_format
from ccdc.molecule import Molecule, _file_object_factory
from ccdc.crystal import Crystal
from typing import Optional
import re
import functools
import datetime
import collections
import warnings
warnings.filterwarnings('always', '.*deprecated.*', DeprecationWarning, '.*', 0)
with _private_importer() as pi:
pi.import_ccdc_module('UtilitiesLib')
pi.import_ccdc_module('DatabaseEntryLib')
pi.import_ccdc_module('SolubilityPlatformLib')
pi.import_ccdc_module('ChemicalAnalysisLib')
pi.import_ccdc_module('ChemistryLib')
pi.import_ccdc_module('FileFormatsLib')
pi.import_ccdc_module('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 SemiconductorPredictedProperties:
'''A container for the predicted semiconductor properties of a database entry.
Each of the properties is optional, and will return ``None`` instead of a value if it has not been set.
Semiconductor property calculations are described in https://onlinelibrary.wiley.com/doi/10.1002/adfm.202001906
Breifly, HOMO_LUMO, reorganization energy, transfer integral, and dynamic disorder, were calculated at the B3LYP/3-21G* level, calibrated against
those at the B3LYP/6-31G* level, as described in this paper: [https://pubs.rsc.org/en/content/articlelanding/2019/ee/c9ee01508f].
Excited state data (singlet and triplet states, and oscillator strengths) are computed at the M06-2X/def2-SVP level of theory.
'''
def __init__(self, _semiconductor=None):
self._semiconductor = _semiconductor if _semiconductor else DatabaseEntryLib.SemiconductorPredictedProperties()
@property
def dynamic_disorder(self) -> Optional[float]:
'''A global measure of the fluctuations of the transfer integrals at room temperature, in kJ/mol.
A large (>10 kJ/mol) value is detrimental to charge mobility in organic semiconductors. More information on how this value was calculated is provided at
https://pubs.rsc.org/en/content/articlelanding/2020/mh/d0mh01159b, please cite this work if you find it useful in your
research'''
return self._semiconductor.dynamic_disorder()
@dynamic_disorder.setter
def dynamic_disorder(self, value: Optional[float]):
self._semiconductor.set_dynamic_disorder(value)
@property
def singlet_state_1_energy(self) -> Optional[float]:
'''Energy of first singlet (S1) state in kJ/mol.
This is the lowest energy excited electronic state with unpaired electrons having opposite spin. A requirement of singlet fission materials is for S1>=2*T1, whilst Thermally Activated
Delayed Fluorescence (TADF) materials require S1~T1. More information on how this value was calculated is provided at https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F,
please cite this work if you find it useful in your research. These values are the "as-calculated" values, the formula to calibrate calculations to experimental data is reported in the paper.
'''
return self._semiconductor.singlet_state_1_energy()
@singlet_state_1_energy.setter
def singlet_state_1_energy(self, value: Optional[float]):
self._semiconductor.set_singlet_state_1_energy(value)
@property
def singlet_state_2_energy(self) -> Optional[float]:
'''Energy of the second singlet (S2) state in kJ/mol.
This is the second-lowest energy excited electronic state with unpaired electrons having opposite spin. More information on how
this value was calculated is provided at https://www.nature.com/articles/s41597-022-01142-7 and https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F, please cite this work
if you find it useful in your research'''
return self._semiconductor.singlet_state_2_energy()
@singlet_state_2_energy.setter
def singlet_state_2_energy(self, value: Optional[float]):
self._semiconductor.set_singlet_state_2_energy(value)
@property
def triplet_state_1_energy(self) -> Optional[float]:
'''Energy of the first triplet state (T1) in kJ/mol.
This is the lowest energy excited electronic state with unpaired electrons having the same spin. A requirement of singlet fission materials is for S1>=2*T1, whilst Thermally Activated
Delayed Flourescsnce (TADF) materials require S1~T1. More information on how this value was calculated is provided at https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F,
please cite this work if you find it useful in your research
'''
return self._semiconductor.triplet_state_1_energy()
@triplet_state_1_energy.setter
def triplet_state_1_energy(self, value: Optional[float]):
self._semiconductor.set_triplet_state_1_energy(value)
@property
def triplet_state_2_energy(self) -> Optional[float]:
'''Energy of the second triplet state (T2) in kJ/mol.
This is the second-lowest energy excited electronic state with unpaired electrons having the same spin. A requirement of singlet fission materials is T2>2*T1 to avoid triplet-triplet fusion.
More information on how this value was calculated is provided at https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F, please cite this work if you find it useful in
your research '''
return self._semiconductor.triplet_state_2_energy()
@triplet_state_2_energy.setter
def triplet_state_2_energy(self, value: Optional[float]):
self._semiconductor.set_triplet_state_2_energy(value)
@property
def hole_reorganization_energy(self) -> Optional[float]:
'''The impact of vibrations from molecular relaxations (intra-molecular modes) on site energy in kJ/mol.
Specifically, this is the reorganization energy for the process of Neutral Molecule to Oxidized Molecule, meaning how much the energy is lowered when a hole is formed in a molecule and the
geometry relaxes from the neutral to the +1 charged equilibrium geometry. More information on how this value was calculated is provided at
https://pubs.aip.org/aip/jcp/article/152/19/190902/199058/Modeling-charge-transport-in-high-mobility, please cite this work if you find it useful in your research'''
return self._semiconductor.reorganization_energy()
@hole_reorganization_energy.setter
def hole_reorganization_energy(self, value: Optional[float]):
self._semiconductor.set_reorganization_energy(value)
@property
def transfer_integral(self) -> Optional[float]:
''' The largest transfer integral between two HOMO orbitals localized on to two neighboring molecules, (J1), in kJ/mol.
This is the extent to which charge is able to be "transferred" from one molecule to another. A requirement of high mobility materials is
that J1>10kJ/mol. More information on how this value was calculated is provided at
https://pubs.acs.org/doi/epdf/10.1021/acs.chemmater.2c00281, please cite this work if you find it useful in your research
'''
return self._semiconductor.transfer_integral()
@transfer_integral.setter
def transfer_integral(self, value: Optional[float]):
self._semiconductor.set_transfer_integral(value)
@property
def homo_lumo_gap(self) -> Optional[float]:
'''The difference in energy between the Highest Occupied Molecular Orbital (HOMO), and Lowest Unoccupied Molecular Orbital (LUMO) in kJ/mol.
A requirement of visible light active materials is that the gap is < 385 kJ/mol. More information on how this value was calculated is provided at
https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F, please cite this work if you find it useful in your research.
'''
return self._semiconductor.homo_lumo_gap()
@homo_lumo_gap.setter
def homo_lumo_gap(self, value: Optional[float]):
self._semiconductor.set_homo_lumo_gap(value)
@property
def singlet_state_1_oscillator_strength(self) -> Optional[float]:
'''Oscillator strength of the first singlet state f(S1).
A requirement for many applications is that the material is optically bright, i.e. the computed oscillator strength is larger than 0.05. More information on how this value was
calculated is provided at https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F, please cite this work if you find it useful in your research'''
return self._semiconductor.singlet_state_1_oscillator_strength()
@singlet_state_1_oscillator_strength.setter
def singlet_state_1_oscillator_strength(self, value: Optional[float]):
self._semiconductor.set_singlet_state_1_oscillator_strength(value)
@property
def singlet_state_2_oscillator_strength(self) -> Optional[float]:
'''Oscillator strength of second singlet state f(S2). A requirement for many applications is that the material is optically bright, i.e. the computed oscillator strength is larger
than 0.05. More information on how this value was calculated is provided at https://pubs.rsc.org/en/content/articlelanding/2019/EE/C9EE01508F, please cite this work if you find it useful
in your research'''
return self._semiconductor.singlet_state_2_oscillator_strength()
@singlet_state_2_oscillator_strength.setter
def singlet_state_2_oscillator_strength(self, value: Optional[float]):
self._semiconductor.set_singlet_state_2_oscillator_strength(value)
[docs]class CrystalPredictedProperties:
'''A container for different types of predicted properties of a database entry.
Currently :class:`ccdc.entry.SemiconductorPredictedProperties` is the only type implemented.
'''
def __init__(self, _properties=None):
self._properties = _properties if _properties else DatabaseEntryLib.CrystalPredictedProperties()
@property
def semiconductor_properties(self) -> Optional[SemiconductorPredictedProperties]:
'''Returns the predicted semiconductor properties for the entry, or None if they have not been provided.'''
if self._properties.semiconductor():
return SemiconductorPredictedProperties(self._properties.semiconductor())
return None
@semiconductor_properties.setter
def semiconductor_properties(self, value: Optional[SemiconductorPredictedProperties]):
'''Sets the predicted semiconductor properties for the entry.'''
self._properties.set_semiconductor(value._semiconductor if value else None)
[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 component_inchis(self):
'''The list of component InChIs of this entry
Where available, the InChIs of the entry components are returned. Each
InChI includes the following attributes:
:ivar inchi: the InChI string
:ivar key: the InChI key
'''
InChI = collections.namedtuple('InChI', ['inchi', 'key'])
inchi_list = []
entry_inchis = self._entry.inchi()
if entry_inchis:
component_inchis = entry_inchis.component_inchis()
for inchi in component_inchis:
inchi_list.append(InChI(inchi.inchi(), inchi.inchi_key()))
return inchi_list
@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)
408.5 K
'''
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
@property
def predicted_properties(self) -> Optional[CrystalPredictedProperties]:
'''Returns the predicted properties for the entry, or None if they have not been provided.'''
properties = self._entry.predicted_properties()
return CrystalPredictedProperties(properties) if properties is not None else None
@predicted_properties.setter
def predicted_properties(self, value: Optional[CrystalPredictedProperties]):
'''Sets the predicted properties for the entry.'''
self._entry.set_predicted_properties(value._properties if value else None)
@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 BaseException:
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)
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', 'cif', 'mmcif' or 'smiles'
:rtype: string
:raises: TypeError if the format is not 'mol2', 'sdf', 'mol', 'cif', 'mmcif' or 'smiles'
'''
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', 'cif', 'mmcif' or 'smiles'
:returns: a :class:`ccdc.entry.Entry`
:raises: TypeError if the format string is not '', 'mol2', 'sdf', 'mol', 'cif', 'mmcif' 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' or format == 'mmcif':
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] def simple_search(self, pattern, max_hits=None):
'''Those journals any of whose names contain the given pattern.'''
p = self._sanitise(pattern)
ret = []
count = 0
for j in self._journals:
if max_hits and count == max_hits:
break
if p in j._sanitised_names:
ret.append(j)
count += 1
return ret
##########################################################################
[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