diff --git a/docs/Makefile b/docs/Makefile index d371e3710..6c376b973 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -8,6 +8,18 @@ SPHINXBUILD ?= sphinx-build SOURCEDIR = source BUILDDIR = build +# A full build prints roughly three thousand lines of Doxygen and Sphinx +# progress, which buries the handful of warnings we actually care about. So we +# build quietly by default: -q drops Sphinx's progress log while leaving +# warnings and errors on stderr, and conf.py passes the same flag to Doxygen. +# Set DOCS_VERBOSE=1 when a build misbehaves and you want the play-by-play: +# +# make html DOCS_VERBOSE=1 +# +ifeq ($(DOCS_VERBOSE),) +QUIET = -q +endif + # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) @@ -16,15 +28,15 @@ help: clean: rm -rf source/api/ - @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @$(SPHINXBUILD) -M clean "$(SOURCEDIR)" "$(BUILDDIR)" $(QUIET) $(SPHINXOPTS) $(O) # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(QUIET) $(SPHINXOPTS) $(O) xml: - cd "$(SOURCEDIR)" && doxygen Doxyfile + cd "$(SOURCEDIR)" && doxygen $(QUIET) Doxyfile livehtml: sphinx-autobuild --host 0.0.0.0 -j auto -a -q "$(SOURCEDIR)" "$(BUILDDIR)/html" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat index 6247f7e23..2203a0a7f 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -10,6 +10,13 @@ if "%SPHINXBUILD%" == "" ( set SOURCEDIR=source set BUILDDIR=build +REM Build quietly by default so only warnings and errors reach the console. +REM Set DOCS_VERBOSE=1 to get Sphinx's and Doxygen's progress logs back. +set QUIET=-q +if not "%DOCS_VERBOSE%" == "" ( + set QUIET= +) + if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL @@ -25,7 +32,7 @@ if errorlevel 9009 ( exit /b 1 ) -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %QUIET% %SPHINXOPTS% %O% goto end :help diff --git a/docs/requirements.txt b/docs/requirements.txt index 67cb39676..f3f780358 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -12,4 +12,5 @@ myst_parser sphinxcontrib-bibtex sphinxemoji sphinx-last-updated-by-git -sphinx-autobuild \ No newline at end of file +sphinx-autobuild +yaspin \ No newline at end of file diff --git a/docs/source/Doxyfile b/docs/source/Doxyfile index 4c881b0da..f940ab3e7 100644 --- a/docs/source/Doxyfile +++ b/docs/source/Doxyfile @@ -2274,7 +2274,7 @@ ENABLE_PREPROCESSING = YES # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -MACRO_EXPANSION = NO +MACRO_EXPANSION = YES # If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then # the macro expansion is limited to the macros specified with the PREDEFINED and @@ -2282,7 +2282,7 @@ MACRO_EXPANSION = NO # The default value is: NO. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -EXPAND_ONLY_PREDEF = NO +EXPAND_ONLY_PREDEF = YES # If the SEARCH_INCLUDES tag is set to YES, the include files in the # INCLUDE_PATH will be searched if a #include is found. @@ -2315,7 +2315,7 @@ INCLUDE_FILE_PATTERNS = # recursively expanded use the := operator instead of the = operator. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. -PREDEFINED = IPC_TOOLKIT_WITH_CUDA IPC_TOOLKIT_WITH_INEXACT_CCD IPC_TOOLKIT_WITH_ROBIN_MAP IPC_TOOLKIT_WITH_ABSEIL IPC_TOOLKIT_WITH_FILIB IPC_TOOLKIT_WITH_MESHFEM_SPARSE +PREDEFINED = IPC_TOOLKIT_WITH_CUDA IPC_TOOLKIT_WITH_INEXACT_CCD IPC_TOOLKIT_WITH_ROBIN_MAP IPC_TOOLKIT_WITH_ABSEIL IPC_TOOLKIT_WITH_FILIB IPC_TOOLKIT_WITH_MESHFEM_SPARSE IPC_TOOLKIT_HOST_DEVICE= # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The @@ -2678,7 +2678,7 @@ PLANTUML_INCLUDE_PATH = # Minimum value: 0, maximum value: 10000, default value: 50. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_GRAPH_MAX_NODES = 50 +DOT_GRAPH_MAX_NODES = 100 # The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs # generated by dot. A depth value of 3 means that only nodes reachable from the diff --git a/docs/source/conf.py b/docs/source/conf.py index 5b4b82cd4..3be456b3d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -4,10 +4,87 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import contextlib +import io +import logging +import os import pathlib import subprocess import sys +# A full build narrates itself for a few thousand lines, which buries the +# handful of warnings worth acting on. We keep it quiet by default and let +# DOCS_VERBOSE=1 bring the whole log back when a build needs debugging. +VERBOSE = bool(os.environ.get("DOCS_VERBOSE")) + + +def quiet_stdout(): + """Swallow stdout from code that writes to it directly. + + Sphinx's own -q only silences messages that go through its status stream, + so anything using a bare print() needs to be wrapped by hand. + """ + if VERBOSE: + return contextlib.nullcontext() + return contextlib.redirect_stdout(io.StringIO()) + + +# -- Progress reporting ------------------------------------------------------ +# A quiet build says nothing for the half minute or so it runs, which looks +# exactly like a hung one. So each slow stage spins a single self-erasing line +# naming what it is doing, and stamps that line with a result when it finishes. +# +# This only happens on an interactive terminal. Redirected output (CI, a log +# file) stays as quiet as it was, and a verbose build skips the spinner because +# its own logs already show progress. +try: + from yaspin import yaspin +except ImportError: # the docs still build without it, just without progress + yaspin = None + +SHOW_PROGRESS = yaspin is not None and not VERBOSE and sys.stdout.isatty() + + +class SpinnerSafeStream: + """Keep the spinner from scribbling over whatever is written to a stream. + + The spinner owns the last line of the terminal and redraws it on a timer, + so a warning written underneath lands in the middle of that line. We clear + the spinner first, write, then let it resume on a fresh line. + """ + + def __init__(self, stream, spinner): + self._stream = stream + self._spinner = spinner + + def write(self, text): + try: + self._spinner.hide() + written = self._stream.write(text) + self._stream.flush() + finally: + self._spinner.show() + return written + + def __getattr__(self, name): + return getattr(self._stream, name) + + +@contextlib.contextmanager +def stage(text): + """Spin while one build stage runs, then stamp the line with its result.""" + if not SHOW_PROGRESS: + yield None + return + with yaspin(text=text, color="yellow") as spinner: + try: + yield spinner + except BaseException: + spinner.fail("\U0001f4a5 ") + raise + spinner.ok("\u2705 ") + + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -20,20 +97,36 @@ from datetime import datetime sys.path.append(str(pathlib.Path(__file__).parents[2] / "python")) -from _find_ipctk import ipctk # noqa +with quiet_stdout(): + from _find_ipctk import ipctk # noqa project = "IPC Toolkit" -copyright = f"2020-{datetime.now().year}, IPC-Sim Organization; MIT License" +copyright = f"2020-{datetime.now().year}, Zachary Ferguson; MIT License" author = "Zachary Ferguson" version = ipctk.__version__ # -- General configuration --------------------------------------------------- # Doxygen +# -q silences Doxygen's per-file progress. Its warnings go to stderr and are +# unaffected, so we still hear about anything that is actually wrong. While the +# spinner is up we hold those warnings in a pipe and replay them once the +# spinner has cleared its line, so they arrive intact rather than interleaved. pathlib.Path("../build/doxyoutput").mkdir(parents=True, exist_ok=True) -if not subprocess.run(["doxygen", "Doxyfile"]): - print("Doxygen failed! Exiting") - exit(1) +doxygen_flags = [] if VERBOSE else ["-q"] + +with stage("Doxygen") as doxygen_spinner: + doxygen = subprocess.run( + ["doxygen", *doxygen_flags, "Doxyfile"], + stderr=subprocess.PIPE if doxygen_spinner is not None else None, + ) + if doxygen.stderr: + doxygen_spinner.hide() + sys.stderr.buffer.write(doxygen.stderr) + sys.stderr.flush() + doxygen_spinner.show() + if doxygen.returncode != 0: + raise SystemExit("Doxygen failed! Exiting") # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named "sphinx.ext.*") or your custom @@ -63,6 +156,27 @@ "sphinx_last_updated_by_git", ] +# sphinx_immaterial announces where it wrote the sitemap with a bare print(), +# so -q never reaches it. We swap in a wrapper around the real handler here, +# at config-read time, because that happens before Sphinx loads the extension +# and binds the handler by name. If the theme ever renames the function the +# getattr below just returns None, and the worst case is that one line comes +# back rather than the build failing. +if not VERBOSE: + try: + from sphinx_immaterial import postprocess_html + except ImportError: + postprocess_html = None + + create_sitemap = getattr(postprocess_html, "create_sitemap", None) + if create_sitemap is not None: + + def quiet_create_sitemap(app, exception, _wrapped=create_sitemap): + with quiet_stdout(): + _wrapped(app, exception) + + postprocess_html.create_sitemap = quiet_create_sitemap + bibtex_bibfiles = ["references.bib"] bibtex_reference_style = "author_year" bibtex_default_style = "plain" @@ -209,10 +323,103 @@ # html_last_updated_fmt = "%B %d, %Y" +# -- Progress reporting for Sphinx's own stages ------------------------------- + + +def connect_progress(app): + """Drive one spinner through Sphinx's read and write passes. + + Those two passes are the slow half of the build and -q makes them silent, + so we name the running phase and count the documents as they go by. + + Warnings have to keep printing cleanly underneath all that. Sphinx binds + its warning stream when the application is constructed, which is before any + event we can hook, so swapping sys.stderr is not enough on its own -- we + also wrap the stream on Sphinx's own warning handler. Both wrappers clear + the spinner before writing, so a warning never lands mid-frame. + """ + state = { + "spinner": None, + "stderr": None, + "handlers": [], + "phase": "", + "done": 0, + "total": 0, + } + + def retitle(): + spinner = state["spinner"] + if spinner is None: + return + progress = "" + if state["total"]: + progress = f" ({state['done']}/{state['total']})" + elif state["done"]: + progress = f" ({state['done']})" + spinner.text = f"Sphinx{state['phase']}{progress}" + + def warning_handlers(): + """Sphinx's warning handler, the one thing above the WARNING level.""" + return [ + handler + for handler in logging.getLogger("sphinx").handlers + if handler.level >= logging.WARNING and hasattr(handler, "stream") + ] + + def on_start(_app): + spinner = yaspin(text="Sphinx", color="yellow") + spinner.start() + state["spinner"] = spinner + state["stderr"] = sys.stderr + sys.stderr = SpinnerSafeStream(sys.stderr, spinner) + for handler in warning_handlers(): + state["handlers"].append((handler, handler.stream)) + handler.stream = SpinnerSafeStream(handler.stream, spinner) + + def on_read_start(_app, _env, docnames): + state.update(phase=": reading sources", done=0, total=len(docnames)) + retitle() + + def on_read_doc(_app, _docname, _source): + state["done"] += 1 + retitle() + + def on_write_page(_app, _pagename, _templatename, _context, _doctree): + if not state["phase"].endswith("writing output"): + state.update(phase=": writing output", done=0, total=0) + state["done"] += 1 + retitle() + + def on_finish(_app, exception): + spinner = state["spinner"] + if spinner is None: + return + state["spinner"] = None + sys.stderr = state["stderr"] + for handler, stream in state["handlers"]: + handler.stream = stream + state["handlers"].clear() + spinner.text = "Sphinx" + if exception is None: + spinner.ok("\u2705 ") + else: + spinner.fail("\U0001f4a5 ") + + app.connect("builder-inited", on_start) + app.connect("env-before-read-docs", on_read_start) + app.connect("source-read", on_read_doc) + app.connect("html-page-context", on_write_page) + # Last, so the spinner covers everything else that runs at the end. + app.connect("build-finished", on_finish, priority=900) + + # -- Custom skip logic for autodoc -------------------------------------------- def setup(app): + if SHOW_PROGRESS: + connect_progress(app) + def skip(app, what, name, obj, skip, options): # Skip the specific private attribute that is causing the crash if name == "__entries": diff --git a/docs/source/index.rst b/docs/source/index.rst index 8b559785f..9e782e745 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -133,6 +133,11 @@ We provide Python bindings for functions in the toolkit using `pybind11 edges, @@ -101,12 +102,11 @@ class SpatialHash : public BroadPhase { double inflation_radius, double voxel_size); - /// @brief Build the spatial hash for static collision detection. - /// @brief Build the spatial hash from vertex boxes. /// @param vertex_boxes AABB boxes for each vertex. /// @param edges Collision mesh edges /// @param faces Collision mesh faces + /// @param dim Dimension of the simulation (2D or 3D) /// @param voxel_size Size of the voxels used in the spatial hash. void build( const AABBs& vertex_boxes, diff --git a/src/ipc/broad_phase/sweep_and_prune.hpp b/src/ipc/broad_phase/sweep_and_prune.hpp index 37f0abb73..5db33ed23 100644 --- a/src/ipc/broad_phase/sweep_and_prune.hpp +++ b/src/ipc/broad_phase/sweep_and_prune.hpp @@ -44,6 +44,7 @@ class SweepAndPrune : public BroadPhase { /// @param vertex_boxes Precomputed vertex AABBs /// @param edges Collision mesh edges /// @param faces Collision mesh faces + /// @param dim Dimension of the simulation (2D or 3D) void build( const AABBs& vertex_boxes, Eigen::ConstRef edges, diff --git a/src/ipc/ccd/nonlinear_ccd.hpp b/src/ipc/ccd/nonlinear_ccd.hpp index 499e5ac7b..dee5c1ffa 100644 --- a/src/ipc/ccd/nonlinear_ccd.hpp +++ b/src/ipc/ccd/nonlinear_ccd.hpp @@ -96,7 +96,6 @@ class NonlinearCCD { const double tmax = 1.0) const; /// @brief Perform nonlinear CCD between two linear edges moving along nonlinear trajectories. - /// @ingroup ccd /// @param[in] ea0 First edge's first endpoint's trajectory /// @param[in] ea1 First edge's second endpoint's trajectory /// @param[in] eb0 Second edge's first endpoint's trajectory diff --git a/src/ipc/collision_filter.hpp b/src/ipc/collision_filter.hpp index a4d65d60f..f2bd863a7 100644 --- a/src/ipc/collision_filter.hpp +++ b/src/ipc/collision_filter.hpp @@ -186,7 +186,7 @@ inline CollisionFilter make_codim_cross_filter(size_t n_codim_vertices) /// @brief Create a filter that prevents self-collisions within a connected /// component of the face mesh. Two vertices in the same connected /// component are blocked; cross-component pairs are allowed. -/// @param faces Face index matrix (#F × 3). +/// @param faces Face index matrix (|F| × 3). /// @return A CollisionFilter that blocks intra-component pairs. /// @note Implemented in collision_filter.cpp (requires libigl internally). CollisionFilter diff --git a/src/ipc/distance/point_point.hpp b/src/ipc/distance/point_point.hpp index b4e2006fc..7d7291c3f 100644 --- a/src/ipc/distance/point_point.hpp +++ b/src/ipc/distance/point_point.hpp @@ -54,8 +54,8 @@ namespace detail { template IPC_TOOLKIT_HOST_DEVICE inline Eigen::Matrix point_point_distance_hessian( - Eigen::ConstRef> /*p0*/, - Eigen::ConstRef> /*p1*/) + [[maybe_unused]] Eigen::ConstRef> p0, + [[maybe_unused]] Eigen::ConstRef> p1) { static_assert(dim == 2 || dim == 3, "point-point is only 2D or 3D"); const Eigen::Matrix I2 = diff --git a/src/ipc/geometry/normal.hpp b/src/ipc/geometry/normal.hpp index 5c59478b6..39d1c6d5b 100644 --- a/src/ipc/geometry/normal.hpp +++ b/src/ipc/geometry/normal.hpp @@ -217,7 +217,7 @@ namespace detail { // ========================================================================= /** - * \defgroup geometry Point-line normal + * \defgroup point_line_normal Point-line normal * \brief Functions for computing a point-line normal and resp. Jacobians. * @{ */ @@ -306,7 +306,7 @@ namespace detail { // ========================================================================= /** - * \defgroup geometry Triangle normal + * \defgroup triangle_normal Triangle normal * \brief Functions for computing a triangle's normal and resp. Jacobians. * @{ */ @@ -406,7 +406,7 @@ namespace detail { // ========================================================================= /** - * \defgroup geometry Line-line normal + * \defgroup line_line_normal Line-line normal * \brief Functions for computing a line-line normal and resp. Jacobians. * @{ */ @@ -515,9 +515,9 @@ namespace detail { Eigen::ConstRef> eb0, Eigen::ConstRef> eb1); -} // namespace detail + /** @} */ -/** @} */ +} // namespace detail // --- EigenExpression wrappers --- diff --git a/src/ipc/math/interval.hpp b/src/ipc/math/interval.hpp index d432371c8..cedca1bbf 100644 --- a/src/ipc/math/interval.hpp +++ b/src/ipc/math/interval.hpp @@ -43,7 +43,9 @@ class Interval : public interval { }; } // namespace filib +/// @cond DOXYGEN_SKIP template <> struct fmt::formatter : ostream_formatter { }; +/// @endcond namespace ipc { diff --git a/src/ipc/smooth_contact/primitives/point3.hpp b/src/ipc/smooth_contact/primitives/point3.hpp index 015a70f00..6e9a76c36 100644 --- a/src/ipc/smooth_contact/primitives/point3.hpp +++ b/src/ipc/smooth_contact/primitives/point3.hpp @@ -39,14 +39,14 @@ class Point3 : public Primitive { const Eigen::Vector& d, const VectorMax& x) const; - /// @brief - /// @tparam scalar - /// @param direc normalized - /// @param v - /// @param direc points from v to the other point - /// @param neighbors follow counter-clockwise order - /// @param params - /// @return + /// @brief Compute the smooth point term for this vertex. + /// @tparam scalar The scalar type. + /// @tparam n_verts The compile-time row count of X, or -1 if dynamic. + /// @param X Local vertex positions, one per row: this vertex first, + /// followed by its one-ring neighbors in counter-clockwise order. + /// @param direc Direction pointing from this vertex to the other point. + /// It is normalized internally, so it need not arrive normalized. + /// @return The product of the weight, normal, and tangent terms. template scalar smooth_point3_term( const Eigen::Matrix& X, diff --git a/src/ipc/smooth_contact/smooth_collisions.hpp b/src/ipc/smooth_contact/smooth_collisions.hpp index a863ce445..cd1beaa1f 100644 --- a/src/ipc/smooth_contact/smooth_collisions.hpp +++ b/src/ipc/smooth_contact/smooth_collisions.hpp @@ -33,7 +33,11 @@ class SmoothCollisions { /// @brief Initialize the set of collisions used to compute the barrier potential. /// @param mesh The collision mesh. /// @param vertices Vertices of the collision mesh. - /// @param broad_phase_method Broad-phase method to use. + /// @param params The smooth contact parameters. + /// @param use_adaptive_dhat Use the per-element activation distances from a + /// prior call to compute_adaptive_dhat instead of a uniform + /// params.dhat. + /// @param broad_phase Broad-phase method to use. void build( const CollisionMesh& mesh, Eigen::ConstRef vertices, @@ -45,8 +49,12 @@ class SmoothCollisions { /// @param candidates Distance candidates from which the collision set is built. /// @param mesh The collision mesh. /// @param vertices Vertices of the collision mesh. + /// @param params The smooth contact parameters. + /// @param use_adaptive_dhat Use the per-element activation distances from a + /// prior call to compute_adaptive_dhat instead of a uniform + /// params.dhat. void build( - const Candidates& _candidates, + const Candidates& candidates, const CollisionMesh& mesh, Eigen::ConstRef vertices, const SmoothContactParameters params, diff --git a/src/ipc/smooth_contact/smooth_contact_potential.hpp b/src/ipc/smooth_contact/smooth_contact_potential.hpp index 6756aa8a2..5e755ecf3 100644 --- a/src/ipc/smooth_contact/smooth_contact_potential.hpp +++ b/src/ipc/smooth_contact/smooth_contact_potential.hpp @@ -71,6 +71,7 @@ class SmoothContactPotential { /// @brief Compute the hessian of the potential for a single collision. /// @param collision The collision. /// @param positions The collision stencil's positions. + /// @param project_hessian_to_psd Whether to project the hessian to the positive semi-definite cone. /// @return The hessian of the potential. Eigen::MatrixXd hessian( const SmoothCollision& collision,