Skip to content

Commit

Permalink
Merge pull request #399 from seoklab/stereochem
Browse files Browse the repository at this point in the history
feat(core): rework stereochemistry interface
  • Loading branch information
jnooree authored Nov 7, 2024
2 parents 4147feb + 782f768 commit cf07f12
Show file tree
Hide file tree
Showing 11 changed files with 690 additions and 99 deletions.
9 changes: 9 additions & 0 deletions cmake/FindSphinx.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ find_program(SPHINX_EXECUTABLE
NAMES sphinx-build)
mark_as_advanced(SPHINX_EXECUTABLE)

option(NURI_PYDOC_DEPEND_PYTHON
"Auto-rebuild python module before building docs" OFF)
mark_as_advanced(NURI_PYDOC_DEPEND_PYTHON)

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Sphinx DEFAULT_MSG SPHINX_EXECUTABLE)

Expand All @@ -30,6 +34,11 @@ function(add_sphinx_docs target)
add_dependencies("${target}" nuri_docs)
endif()

if(NURI_PYDOC_DEPEND_PYTHON)
message("${target} will rebuild python modules")
add_dependencies("${target}" nuri_python)
endif()

add_custom_target("${target}_doctest"
COMMAND ${CMAKE_COMMAND} -E env ${SANITIZER_ENVS}
${SPHINX_EXECUTABLE}
Expand Down
66 changes: 55 additions & 11 deletions include/nuri/core/molecule.h
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ enum class AtomFlags : std::uint32_t {
kConjugated = 0x2,
kRing = 0x4,
kChiral = 0x8,
kRightHanded = 0x10,
kClockWise = 0x10,
};

class AtomData {
Expand All @@ -135,7 +135,7 @@ class AtomData {
constants::Hybridization hyb = constants::kOtherHyb,
double partial_charge = 0.0, int mass_number = -1,
bool is_aromatic = false, bool is_in_ring = false,
bool is_chiral = false, bool is_right_handed = false);
bool is_chiral = false, bool is_clockwise = false);

/**
* @brief Get the atomic number of the atom.
Expand Down Expand Up @@ -273,20 +273,20 @@ class AtomData {
return internal::check_flag(flags_, AtomFlags::kChiral);
}

AtomData &set_right_handed(bool is_right_handed) {
internal::update_flag(flags_, is_right_handed, AtomFlags::kRightHanded);
AtomData &set_clockwise(bool is_clockwise) {
internal::update_flag(flags_, is_clockwise, AtomFlags::kClockWise);
return *this;
}

/**
* @brief Get handedness of a chiral atom.
*
* @pre is_chiral() == `true`, otherwise return value would be meaningless.
* @return Whether the chiral atom is "right-handed," i.e., `true` for (R)
* and `false` for (S).
* @return Whether the chiral atom is "clockwise." See stereochemistry
* definition of SMILES for more information.
*/
bool is_right_handed() const {
return internal::check_flag(flags_, AtomFlags::kRightHanded);
bool is_clockwise() const {
return internal::check_flag(flags_, AtomFlags::kClockWise);
}

AtomFlags flags() const { return flags_; }
Expand Down Expand Up @@ -372,7 +372,8 @@ enum class BondFlags : std::uint32_t {
kRing = 0x1,
kAromatic = 0x2,
kConjugated = 0x4,
kEConfig = 0x8,
kConfigSpecified = 0x8,
kTransConfig = 0x10,
};

class BondData {
Expand Down Expand Up @@ -436,12 +437,55 @@ class BondData {
return *this;
}

/**
* @brief Test if the bond configuration is explicitly specified.
*/
bool has_config() const {
return internal::check_flag(flags_, BondFlags::kConfigSpecified);
}

/**
* @brief Set whether the bond configuration is explicitly specified.
*/
BondData &set_config(bool config) {
internal::update_flag(flags_, config, BondFlags::kConfigSpecified);
return *this;
}

/**
* @brief Get the cis-trans configuration of the bond.
* @return Whether the bond is in trans configuration.
*
* @pre has_config(), otherwise return value would be meaningless.
* @note This flag is only meaningful for torsionally restricted bonds, such
* as double bonds.
*
* For bonds with more than 3 neighboring atoms, "trans" configuration is not
* a well defined term. In such cases, this will return whether the first two
* neighbors are on the same side of the bond. For example, in the following
* structure, the bond between atoms 0 and 1 is considered to be in a cis
* configuration (assuming the neighbors are ordered in the same way as the
* atoms).
*
* \code{.unparsed}
* 2 4
* \ /
* 0 = 1
* / \
* 3 5
* \endcode
*/
bool is_trans() const {
return internal::check_flag(flags_, BondFlags::kEConfig);
return internal::check_flag(flags_, BondFlags::kTransConfig);
}

/**
* @brief Set cis-trans configuration of the bond.
* @param trans Whether the bond is in trans configuration or not.
* @pre has_config()
*/
BondData &set_trans(bool trans) {
internal::update_flag(flags_, trans, BondFlags::kEConfig);
internal::update_flag(flags_, trans, BondFlags::kTransConfig);
return *this;
}

Expand Down
2 changes: 1 addition & 1 deletion python/docs/nuri.core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ nuri.core

.. autoclass:: Molecule
:exclude-members: atom, bond, neighbor, bonds, mutator,
num_atoms, num_bonds, get_conf, set_conf, num_confs
num_atoms, num_bonds, get_conf, set_conf, num_confs, has_bond

.. automethod:: __init__
.. automethod:: atom
Expand Down
122 changes: 88 additions & 34 deletions python/include/nuri/python/core/core_module.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,42 @@

namespace nuri {
namespace python_internal {
enum class Chirality : int {
kNone,
kCW,
kCCW,
};

inline Chirality chirality_of(const AtomData &data) {
if (!data.is_chiral())
return Chirality::kNone;

return data.is_clockwise() ? Chirality::kCW : Chirality::kCCW;
}

inline void update_chirality(AtomData &data, Chirality kind) {
data.set_chiral(kind != Chirality::kNone)
.set_clockwise(kind == Chirality::kCW);
}

enum class BondConfig : int {
kNone,
kCis,
kTrans,
};

inline BondConfig config_of(const BondData &data) {
if (!data.has_config())
return BondConfig::kNone;

return data.is_trans() ? BondConfig::kTrans : BondConfig::kCis;
}

inline void update_config(BondData &data, BondConfig kind) {
data.set_config(kind != BondConfig::kNone)
.set_trans(kind == BondConfig::kTrans);
}

inline int check_conf(const Molecule &mol, int idx) {
return py_check_index(static_cast<int>(mol.confs().size()), idx,
"conformer index out of range");
Expand Down Expand Up @@ -795,28 +831,19 @@ Whether the atom is a ring atom.
:meth:`update`
)doc");
cls.def_property(
"chiral", [](T &self) { return atom_prolog(self).is_chiral(); },
[](T &self, bool is_chiral) { atom_prolog(self).set_chiral(is_chiral); },
rvp::automatic,
R"doc(
:type: bool
Whether the atom is chiral.
.. seealso::
:meth:`update`
)doc");
cls.def_property(
"right_handed",
[](T &self) { return atom_prolog(self).is_right_handed(); },
[](T &self, bool is_right) {
atom_prolog(self).set_right_handed(is_right);
"chirality", [](T &self) { return chirality_of(atom_prolog(self)); },
[](T &self, std::optional<Chirality> kind) {
update_chirality(atom_prolog(self), kind.value_or(Chirality::kNone));
},
rvp::automatic,
R"doc(
:type: bool
:type: Chirality
Explicit chirality of the atom. Note that this does *not* imply the atom is a
stereocenter chemically.
Whether the atom is right-handed.
.. tip::
Assigning :obj:`None` clears the explicit chirality.
.. seealso::
:meth:`update`
Expand Down Expand Up @@ -987,8 +1014,8 @@ The name of the atom. Returns an empty string if the name is not set.
std::optional<int> formal_charge, std::optional<double> partial_charge,
std::optional<int> atomic_number, const Element *element,
std::optional<bool> ar, std::optional<bool> conj,
std::optional<bool> ring, std::optional<bool> chiral,
std::optional<bool> right, std::optional<std::string> name) -> T & {
std::optional<bool> ring, std::optional<Chirality> chirality,
std::optional<std::string> name) -> T & {
// Possibly throwing functions

AtomData &data = atom_prolog(self);
Expand Down Expand Up @@ -1028,10 +1055,8 @@ The name of the atom. Returns an empty string if the name is not set.
data.set_conjugated(*conj);
if (ring)
data.set_ring_atom(*ring);
if (chiral)
data.set_chiral(*chiral);
if (right)
data.set_right_handed(*right);
if (chirality)
update_chirality(data, *chirality);

log_aromatic_warning(data);

Expand All @@ -1050,8 +1075,7 @@ The name of the atom. Returns an empty string if the name is not set.
py::arg("aromatic") = py::none(), //
py::arg("conjugated") = py::none(), //
py::arg("ring") = py::none(), //
py::arg("chiral") = py::none(), //
py::arg("right_handed") = py::none(), //
py::arg("chirality") = py::none(), //
py::arg("name") = py::none(), //
R"doc(
Update the atom data. If any of the arguments are not given, the corresponding
Expand Down Expand Up @@ -1274,13 +1298,43 @@ Whether the atom is conjugated.
:meth:`update`
)doc");
cls.def_property(
"trans", [](T &self) { return bond_prolog(self).is_trans(); },
[](T &self, bool is_trans) { bond_prolog(self).set_trans(is_trans); },
"config", [](T &self) { return config_of(bond_prolog(self)); },
[](T &self, std::optional<BondConfig> cfg) {
update_config(bond_prolog(self), cfg.value_or(BondConfig::kNone));
},
rvp::automatic,
R"doc(
:type: bool
:type: BondConfig
The explicit configuration of the bond. Note that this does *not* imply the bond
is a torsionally restricted bond chemically.
Whether the bond is *(E)*-configured.
.. note::
For bonds with more than 3 neighboring atoms, :attr:`BondConfig.Cis` or
:attr:`BondConfig.Trans` configurations are not well defined terms. In such
cases, this will return whether **the first neighbors are on the same side of
the bond**. For example, in the following structure (assuming the neighbors
are ordered in the same way as the atoms), the bond between atoms 0 and 1 is
considered to be in a cis configuration (first neighbors are marked with angle
brackets)::
<2> <4>
\ /
0 = 1
/ \
3 5
On the other hand, when the neighbors are ordered in the opposite way, the
bond between atoms 0 and 1 is considered to be in a trans configuration::
<2> 5
\ /
0 = 1
/ \
3 <4>
.. tip::
Assigning :obj:`None` clears the explicit bond configuration.
.. seealso::
:meth:`update`
Expand All @@ -1301,7 +1355,7 @@ The name of the bond. Returns an empty string if the name is not set.
"update",
[](T &self, std::optional<constants::BondOrder> ord,
std::optional<bool> ar, std::optional<bool> conj,
std::optional<bool> ring, std::optional<bool> trans,
std::optional<bool> ring, std::optional<BondConfig> cfg,
std::optional<std::string> name) -> T & {
BondData &data = bond_prolog(self);

Expand All @@ -1315,8 +1369,8 @@ The name of the bond. Returns an empty string if the name is not set.
data.set_conjugated(*conj);
if (ring)
data.set_ring_bond(*ring);
if (trans)
data.set_trans(*trans);
if (cfg)
update_config(data, *cfg);

log_aromatic_warning(data);

Expand All @@ -1330,7 +1384,7 @@ The name of the bond. Returns an empty string if the name is not set.
py::arg("aromatic") = py::none(), //
py::arg("conjugated") = py::none(), //
py::arg("ring") = py::none(), //
py::arg("trans") = py::none(), //
py::arg("config") = py::none(), //
py::arg("name") = py::none(), //
R"doc(
Update the bond data. If any of the arguments are not given, the corresponding
Expand Down
2 changes: 2 additions & 0 deletions python/src/nuri/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
"PeriodicTable",
"Hyb",
"BondOrder",
"Chirality",
"BondConfig",
"SubstructureCategory",
"AtomData",
"BondData",
Expand Down
14 changes: 14 additions & 0 deletions python/src/nuri/core/molecule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,20 @@ void bind_enums(py::module &m) {
.value("Aromatic", constants::BondOrder::kAromaticBond);

py::implicitly_convertible<int, constants::BondOrder>();

py::enum_<Chirality>(m, "Chirality")
.value("Unknown", Chirality::kNone)
.value("CW", Chirality::kCW)
.value("CCW", Chirality::kCCW);

py::implicitly_convertible<int, Chirality>();

py::enum_<BondConfig>(m, "BondConfig")
.value("Unknown", BondConfig::kNone)
.value("Trans", BondConfig::kTrans)
.value("Cis", BondConfig::kCis);

py::implicitly_convertible<int, BondConfig>();
}

void bind_atom(py::class_<AtomData> &atom_data, py::class_<PyAtom> &atom) {
Expand Down
Loading

0 comments on commit cf07f12

Please sign in to comment.