Skip to content

Commit

Permalink
finish and document Logger
Browse files Browse the repository at this point in the history
  • Loading branch information
wojdyr committed Oct 23, 2024
1 parent f04d533 commit 98bf846
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 51 deletions.
94 changes: 78 additions & 16 deletions docs/analysis.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,78 @@ Six of the resulting plots are shown here (click to enlarge):
:align: center
:scale: 60

.. _logger:

Logger
======

Gemmi Logger is a tiny helper class for passing messages from a gemmi function
to the calling function. It's not specific to structure analysis
and doesn't belong in this section, but it's documented here because
it's used in the next subsection and I haven't found a better spot for it.

The messages being passed are usually info or warnings that a command-line
program would print to stdout or stderr.

The Logger has two member variables:

.. literalinclude:: ../include/gemmi/logger.hpp
:language: c++
:start-at: ///
:end-at: int threshold

and a few member functions for sending messages.

When a function takes a Logger argument, we can pass:

**C++**

* `{&Logger::to_stderr}` to redirect messages to stderr
(to_stderr() calls fprintf),
* `{&Logger::to_stdout}` to redirect messages to stdout,
* `{&Logger::to_stdout, 3}` to print only warnings (threshold=3),
* `{nullptr, 0}` to disable all messages,
* `{}` to throw errors and ignore other messages (the default, see Quirk above),
* `{[](const std::string& s) { do_anything(s);}}` to do anything else.

**Python**

* `sys.stderr` or `sys.stdout` or any other stream (an object with `write`
and `flush` methods), to redirect messages to that stream,
* `(sys.stdout, 3)` to print only warnings (threshold=3),
* `(None, 0)` to disable all messages,
* `None` to throw errors and ignore other messages (the default, see Quirk above),
* a function that takes a message string as its only argument
(e.g. `lambda s: print(s.upper())`).

.. _monlib:

Monomer library
===============

Structural biologist routinely use prior knowledge about biomolecules
to augment the data obtained in an experiment. In macromolecular
crystallography and cryo-EM that prior knowledge is usually stored
in a monomer library that describes monomers and links between them.
In gemmi, such a library is represented by class MonLib.

TBC

For now, here is an example of how to read the CCP4 monomer library
(Refmac dictionary):

.. doctest::
:skipif: ccp4_path is None

>>> monlib_path = os.environ['CCP4'] + '/lib/data/monomers'
>>> resnames = st[0].get_all_residue_names()
>>> monlib = gemmi.MonLib()
>>> monlib.read_monomer_lib(monlib_path, resnames, logging=sys.stderr)
True

TBC


.. _topology:

Topology
Expand Down Expand Up @@ -1112,20 +1184,12 @@ And reorder atoms. In Python, we have one function that does it all:
model_index: int = 0,
h_change: gemmi.HydrogenChange = HydrogenChange.NoChange,
reorder: bool = False,
warnings: object = None,
logger: object = None,
ignore_unknown_links: bool = False) -> gemmi.Topo
where

* `monlib` is an instance of an undocumented MonLib class.
For now, here is an example of how to read the CCP4 monomer library
(Refmac dictionary):

.. code-block:: python
monlib_path = os.environ['CCP4'] + '/lib/data/monomers'
resnames = st[0].get_all_residue_names()
monlib = gemmi.read_monomer_lib(monlib_path, resnames)
* `monlib` is an instance of the :ref:`MonLib <monlib>` class,

* `h_change` can be one of the following:

Expand All @@ -1141,12 +1205,10 @@ where
* `reorder` -- changes the order of atoms within each residue
to match the order in the corresponding monomer cif file,

* `warnings` -- by default, an exception is raised when a chemical component
is missing in the monomer library, or when a link is missing,
or when the hydrogen adding procedure encounters an unexpected configuration.
You can set warnings=sys.stderr to only print a warning to stderr
and continue. sys.stderr can be replaced with any object that has
the methods `write(str)` and `flush()`.
* `logger` -- optional :ref:`Logger <logger>` configuration.
By default, an exception is raised when a chemical component
or a link is missing from the monomer library,
or when the hydrogen-adding procedure encounters an unexpected configuration.


TBC
Expand Down
12 changes: 6 additions & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,13 @@
except ImportError:
disabled_features.append('pynauty')
pynauty = None
mdm2_unmerged_mtz_path = os.getenv('CCP4')
if mdm2_unmerged_mtz_path:
mdm2_unmerged_mtz_path += ('/lib/python3.7/site-packages/' +
'ccp4i2/demo_data/mdm2/mdm2_unmerged.mtz')
ccp4_path = os.getenv('CCP4')
if ccp4_path:
mdm2_unmerged_mtz_path = (ccp4_path + '/lib/python3.9/site-packages/'
+ 'ccp4i2/demo_data/mdm2/mdm2_unmerged.mtz')
if not os.path.isfile(mdm2_unmerged_mtz_path):
mdm2_unmerged_mtz_path = None
if mdm2_unmerged_mtz_path is None:
ccp4_path = None
if ccp4_path is None:
disabled_features.append('$CCP4')
import gemmi
Expand Down
14 changes: 7 additions & 7 deletions docs/hkl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ Unmerged (multi-record) MTZ files store a list of batches::
std::vector<Mtz::Batch> Mtz::batches

.. doctest::
:skipif: mdm2_unmerged_mtz_path is None
:skipif: ccp4_path is None

>>> # here we use mdm2_unmerged.mtz file distributed with CCP4 7.1
>>> mdm2 = gemmi.read_mtz_file(mdm2_unmerged_mtz_path)
Expand All @@ -301,7 +301,7 @@ Unmerged (multi-record) MTZ files store a list of batches::
Each batch has a number of properties:

.. doctest::
:skipif: mdm2_unmerged_mtz_path is None
:skipif: ccp4_path is None

>>> batch = mdm2.batches[0]
>>> batch.number
Expand All @@ -319,7 +319,7 @@ Most of the batch header properties are not decoded,
but they can be accessed directly if needed:

.. doctest::
:skipif: mdm2_unmerged_mtz_path is None
:skipif: ccp4_path is None

>>> list(batch.ints)
[185, 29, 156, 0, -1, 1, -1, 0, 0, 0, 0, 0, 1, 0, 2, 1, 0, 1, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0]
Expand All @@ -332,7 +332,7 @@ indices it stores indices of the equivalent reflection in the ASU
It can be useful to switch between the two:

.. doctest::
:skipif: mdm2_unmerged_mtz_path is None
:skipif: ccp4_path is None

>>> mdm2.switch_to_original_hkl()
True
Expand Down Expand Up @@ -676,12 +676,12 @@ Reindexing, ASU, sorting, ...
The reindexing function changes:

* Miller indices of reflections
(if new indices would be fractional the reflection is removed),
(if the new indices would be fractional, the reflection is removed),
* space group,
* unit cell parameters (in MTZ records CELL and DCELL, and in batch headers).

Reindexing takes as an argument the operator that is to be applied
to Miller indices. In Python, it returns a textual message for the user:
Reindexing takes an operator that is applied to the Miller indices.
Information about the operation is passed to a :ref:`Logger <logger>`.

.. doctest::

Expand Down
22 changes: 9 additions & 13 deletions include/gemmi/logger.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,18 @@
namespace gemmi {

/// Passes messages (including warnings/errors) to a callback function.
/// Messages are passed as strings without a newline character.
/// Messages have severity levels similar syslog:
/// 7=debug, 6=info (all but debug), 5=notice, 3=error
/// A numeric threshold can be set to limit the messages (see below).
/// Quirk: if a callback is not set, errors are thrown as exceptions.
/// Messages are passed as strings without a trailing newline.
/// They have syslog-like severity levels: 7=debug, 6=info, 5=notice, 3=error,
/// allowing the use of a threshold to filter them.
/// Quirk: Errors double as both errors and warnings. Unrecoverable errors
/// don't go through this class; Logger only handles errors that can
/// be downgraded to warnings. If a callback is set, the error is passed
/// as a warning message. Otherwise, it's thrown as std::runtime_error.
struct Logger {
/// A function that handles messages.
std::function<void(const std::string&)> callback;
/// Pass messages of this level and all lower (more severe) levels:
/// 7=all messages, 6=all but debug, 0=none
/// 7=all, 6=all but debug, 5=notes and warnings, 3=warnings, 0=none
int threshold = 6;

/// suspend() and resume() are used internally to avoid duplicate messages
Expand All @@ -48,11 +50,7 @@ struct Logger {
callback(cat("Note: ", args...));
}

/// Send a warning/error. Unrecoverable errors are thrown directly and
/// don't go through this class, so here we're left with errors that
/// can be downgraded to warnings. If a callback is set, the message is
/// passed as a warning; otherwise it's thrown as a std::runtime_error.
/// (Admittedly, it's a questionable design.)
/// Send a warning/error (see Quirk above).
template<class... Args> GEMMI_COLD void err(Args const&... args) const {
if (threshold >= 3) {
std::string msg = cat(args...);
Expand All @@ -72,8 +70,6 @@ struct Logger {
static void to_stdout(const std::string& s) {
std::fprintf(stdout, "%s\n", s.c_str());
}
/// to be used as: logger.callback = Logger::nop;
static void nop(const std::string&) {}
};

} // namespace gemmi
Expand Down
4 changes: 2 additions & 2 deletions prog/convert.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -421,8 +421,8 @@ void convert(gemmi::Structure& st,
if (output_type == CoorFormat::Pdb)
how = HowToNameCopiedChain::Short;
if (options[AsAssembly]) {
auto callback = options[Verbose] ? &gemmi::Logger::to_stderr : &gemmi::Logger::nop;
gemmi::transform_to_assembly(st, options[AsAssembly].arg, how, {callback});
gemmi::Logger logger{&gemmi::Logger::to_stderr, options[Verbose] ? 6 : 3};
gemmi::transform_to_assembly(st, options[AsAssembly].arg, how, logger);
}

if (options[ExpandNcs]) {
Expand Down
22 changes: 15 additions & 7 deletions python/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,25 @@ namespace nanobind { namespace detail {
template <> struct type_caster<gemmi::Logger> {
NB_TYPE_CASTER(gemmi::Logger, const_name("object"))
bool from_python(handle src, uint8_t, cleanup_list *) noexcept {
if (src.is_none())
value = {nullptr};
else if (nb::hasattr(src, "write") && nb::hasattr(src, "flush"))
value = {[obj=nb::borrow(src)](const std::string& s) {
value = {};
if (PyTuple_Check(src.ptr()) && PyTuple_Size(src.ptr()) == 2) {
value.threshold = (int) PyLong_AsLong(PyTuple_GetItem(src.ptr(), 1));
if (value.threshold == -1 && PyErr_Occurred())
return false;
src = PyTuple_GetItem(src.ptr(), 0);
}
if (src.is_none()) {
// nothing
} else if (nb::hasattr(src, "write") && nb::hasattr(src, "flush")) {
value.callback = {[obj=nb::borrow(src)](const std::string& s) {
obj.attr("write")(s + "\n");
obj.attr("flush")();
}};
else if (PyCallable_Check(src.ptr()))
value = {[obj=nb::borrow(src)](const std::string& s) { obj(s); }};
else
} else if (PyCallable_Check(src.ptr())) {
value.callback = {[obj=nb::borrow(src)](const std::string& s) { obj(s); }};
} else {
return false;
}
return true;
}
};
Expand Down

0 comments on commit 98bf846

Please sign in to comment.