Skip to content

Commit b832505

Browse files
committed
reporting
1 parent 54d769a commit b832505

2 files changed

Lines changed: 78 additions & 22 deletions

File tree

arc/level/reporting.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -322,13 +322,36 @@ def _toc_cell(sections: list[SpeciesSection]):
322322

323323
_SETUP_SOURCE = '''\
324324
# Shared setup — imports + helpers used by every species/TS section below.
325+
import os as _os
326+
327+
import arc.common as arc_common
325328
import arc.parser.parser as arc_parser
326329
from arc.constants import E_h_kJmol
327330
from arc.level.protocol import CompositeProtocol
328331
329332
# Accumulates per-section results so the Project summary can aggregate them.
330333
_RESULTS = {}
331334
335+
# Determine the project directory from this notebook's location at runtime.
336+
# Notebook lives at ``<project>/output/sp_composite.ipynb``, so the project
337+
# directory is the parent of the notebook's directory. Jupyter / VS Code run
338+
# kernels with the notebook file's directory as cwd, so ``os.getcwd()``
339+
# resolves to ``<project>/output/`` and its parent is what we need.
340+
_NB_PROJECT_DIRECTORY = _os.path.dirname(_os.path.abspath(_os.curdir))
341+
342+
343+
def _resolve_path(p):
344+
"""Rebase a stored absolute path on the current project directory.
345+
346+
When the project folder is moved (or this notebook is opened on a different
347+
machine), the originally-stored absolute paths under
348+
``<project>/calcs/Species/...`` and ``<project>/calcs/TSs/...`` no longer
349+
point anywhere. ``arc.common.globalize_path`` recognises those prefixes and
350+
rewrites the leading project-directory portion to match the current
351+
notebook location. No-op when the project hasn't moved.
352+
"""
353+
return arc_common.globalize_path(p, _NB_PROJECT_DIRECTORY)
354+
332355
333356
def _format_breakdown(protocol, energies_kJmol):
334357
"""Render a fixed-width per-term breakdown table as a string."""
@@ -405,26 +428,28 @@ def _recipe_code(section: SpeciesSection) -> str:
405428

406429

407430
def _paths_code(section: SpeciesSection, notebook_dir: str) -> str:
431+
"""Render the per-section ``paths`` dict as a code-cell source.
432+
433+
Each value is wrapped in ``_resolve_path(...)`` (defined in the shared
434+
setup cell, which calls ``arc.common.globalize_path``) so absolute paths
435+
pointing under ``<project>/calcs/Species|TSs/`` get auto-rebased when the
436+
project directory is moved or the notebook is opened on a different
437+
machine. Paths that don't match those prefixes flow through unchanged.
438+
439+
The ``notebook_dir`` parameter is kept on the signature for API stability
440+
(callers in the writer pass it positionally) but is no longer used to
441+
pre-render relative paths — runtime rebase via ``_resolve_path`` is more
442+
robust because it handles moves the relative-path approach can't (e.g.,
443+
paths outside the project tree, paths whose relative offset to the
444+
notebook changes when the project tree is restructured).
445+
"""
446+
del notebook_dir # no longer used; kept on the signature for API stability
408447
entries = []
409448
for sub_label, abs_path in sorted(section.sub_job_paths.items()):
410-
rendered = _render_path(abs_path, notebook_dir)
411-
entries.append(f" {sub_label!r}: {rendered!r},")
449+
entries.append(f" {sub_label!r}: _resolve_path({abs_path!r}),")
412450
return "paths = {\n" + "\n".join(entries) + "\n}"
413451

414452

415-
def _render_path(abs_path: str, notebook_dir: str) -> str:
416-
"""Render ``abs_path`` relative to ``notebook_dir`` if under it, else absolute."""
417-
try:
418-
common = os.path.commonpath([os.path.abspath(abs_path),
419-
os.path.abspath(notebook_dir)])
420-
except ValueError:
421-
return abs_path
422-
if common == os.path.abspath(notebook_dir):
423-
rel = os.path.relpath(abs_path, notebook_dir)
424-
return rel if rel.startswith((".", os.sep)) else f"./{rel}"
425-
return abs_path
426-
427-
428453
def _parse_code(section: SpeciesSection) -> str:
429454
return (
430455
"# Parse the electronic energy from each sub-job's QM output file.\n"

arc/level/reporting_test.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -299,17 +299,32 @@ def test_section_markdown_includes_formula(self):
299299

300300
# --- path rendering ----------------------------------------------------- #
301301

302-
def test_paths_rendered_relative_when_under_notebook_dir(self):
302+
def test_paths_kept_absolute_and_wrapped_in_resolve_path(self):
303+
"""Absolute paths survive into the paths-dict cell and each is wrapped
304+
in ``_resolve_path(...)`` so they're rebased through
305+
``arc.common.globalize_path`` at notebook execution time. This makes
306+
the notebook robust to the user moving the project directory or
307+
running on a different machine — paths under ``<project>/calcs/Species/``
308+
or ``<project>/calcs/TSs/`` get auto-rebased to the new project root."""
303309
write_composite_notebook(**self.kwargs)
304310
paths_cell = next(c for c in self._cells()
305311
if c.cell_type == "code" and "paths = " in c.source
306312
and "parse_e_elect" not in c.source)
307-
# Relative paths should start with './' and not have the tmp absolute prefix.
308-
self.assertNotIn(self.tmp, paths_cell.source)
309-
self.assertIn("./base.out", paths_cell.source)
310-
311-
def test_paths_rendered_absolute_when_outside(self):
312-
# A path outside the notebook dir must appear as absolute.
313+
# Each path value must be wrapped in _resolve_path(...).
314+
self.assertIn("_resolve_path(", paths_cell.source)
315+
# Paths are stored as absolute strings so globalize_path can recognise
316+
# the project-directory prefix and rewrite it. The rewrite is a no-op
317+
# if the project hasn't moved.
318+
self.assertIn(self.base_path, paths_cell.source)
319+
self.assertIn(self.hi_path, paths_cell.source)
320+
self.assertIn(self.lo_path, paths_cell.source)
321+
322+
def test_paths_outside_project_survive_resolve_unchanged(self):
323+
"""Paths that don't match the ``/calcs/Species|TSs/`` pattern flow
324+
through ``globalize_path`` unchanged (it only rebases project-tree
325+
QM-output paths). They must still appear as absolutes wrapped in
326+
``_resolve_path(...)`` — the notebook can't pre-judge what's
327+
inside vs outside the project."""
313328
outside_dir = tempfile.mkdtemp()
314329
outside_path = os.path.join(outside_dir, "far.out")
315330
_write_gaussian_fixture(outside_path, -1.0)
@@ -325,10 +340,26 @@ def test_paths_rendered_absolute_when_outside(self):
325340
if c.cell_type == "code" and "paths = " in c.source
326341
and "parse_e_elect" not in c.source)
327342
self.assertIn(outside_path, paths_cell.source)
343+
self.assertIn("_resolve_path(", paths_cell.source)
328344
finally:
329345
os.unlink(outside_path)
330346
os.rmdir(outside_dir)
331347

348+
def test_setup_cell_defines_resolve_path_and_imports_arc_common(self):
349+
"""The setup cell must define ``_resolve_path`` and import
350+
``arc.common`` so the per-section paths-dict cells can call
351+
``_resolve_path(...)`` on each absolute path."""
352+
write_composite_notebook(**self.kwargs)
353+
# Setup cell is the first code cell after the title/banner markdown.
354+
setup_cells = [c for c in self._cells()
355+
if c.cell_type == "code" and "_resolve_path" in c.source]
356+
self.assertGreaterEqual(len(setup_cells), 1,
357+
"Expected at least one code cell defining _resolve_path.")
358+
setup_src = setup_cells[0].source
359+
self.assertIn("arc.common", setup_src)
360+
self.assertIn("globalize_path", setup_src)
361+
self.assertIn("def _resolve_path", setup_src)
362+
332363
# --- determinism -------------------------------------------------------- #
333364

334365
def test_deterministic_byte_identical_across_runs(self):

0 commit comments

Comments
 (0)