Skip to content

metadata for sub-chains - #13

Merged
r-fedorov merged 4 commits into
refactor_namerfrom
dev
Jun 17, 2026
Merged

metadata for sub-chains#13
r-fedorov merged 4 commits into
refactor_namerfrom
dev

Conversation

@r-fedorov

@r-fedorov r-fedorov commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR enriches naming trace output with a structured substituent_tree, allowing recursive substituent hierarchy, nested branch decisions, functional-prefix metadata, and ligand subtrees to be surfaced through the public analysis/API payloads.

It also improves nested decision tracing for recursive substituent naming, preserves that metadata through assembly trace segments, and adds tests covering the new hierarchy and trace behavior.

Changes

  • Add substituent_tree support to:

    • NameAnalysis
    • NamingResult
    • NamingEngine.analyze() / analyze_smiles()
    • JSON payloads emitted when include_trace=True
  • Extend component and subgraph naming to optionally return both:

    • flat trace segments
    • nested substituent tree structures
  • Preserve recursive branch metadata through:

    • SubstituentItem
    • add_substituent_trace
    • assembly_trace_segments
    • new assembly_substituent_tree helpers
  • Add nested DecisionTrace capture for recursive substituents, including:

    • selected substituent subgraph
    • parent skeleton selection
    • numbering selection
    • final substituent assembly
  • Introduce structured handling for direct functional-prefix subgraphs via DirectSubgraphPrefix, including functional-prefix atoms, bonds, group keys, attachment atoms, and ligand subtrees.

  • Add regression coverage for:

    • nested decision preservation in trace segments
    • recursive substituent decision traces
    • public API exposure of substituent_tree
    • preservation of nested branch hierarchy alongside flat trace output
    • CLI JSON output including substituent_tree
  • Fix example scripts:

    • Separate OPSIN failure output directories for ZINC22 and PubChem
    • Correct PubChem example import to use bluenamer.utils.standardize_mol
    • Fix QM9 batch example dataset extraction from the selected SMILES column

Testing

  • Added and updated tests in:
    • src/bluenamer/tests/test_analysis.py
    • src/bluenamer/tests/test_public_api.py

Summary by Sourcery

Expose structured substituent hierarchy and nested decision metadata through naming analysis and public trace output.

New Features:

  • Add a JSON-serializable substituent_tree structure to NameAnalysis, NamingResult, and CLI JSON output, capturing component and recursive substituent hierarchies.
  • Introduce DirectSubgraphPrefix to represent direct functional-prefix subgraphs with associated atoms, bonds, attachment metadata, and ligand subtrees.
  • Capture nested DecisionTrace data for recursive substituent and functional-prefix naming, including component selection, parent skeleton, numbering, and final assembly steps.

Bug Fixes:

  • Correct example scripts by fixing OPSIN evaluation output directories, importing standardize_mol from the proper package, and using the correct SMILES column when constructing QM9 datasets.

Enhancements:

  • Extend component, branch, and subgraph naming flows to optionally return both flat trace segments and nested substituent trees while preserving compatibility with existing call sites.
  • Augment assembly trace segments to carry nested_decisions for grouped substituents and merge multiple same-name substituent tree instances into grouped structures.

Tests:

  • Add regression tests ensuring nested decision metadata is preserved through substituent traces and assembly segments and that substituent_tree is exposed via the public API and CLI JSON output.
  • Add tests validating grouping behavior for multiple same-name substituent tree instances and coverage for recursive substituent nested decision capture.

@sourcery-ai

sourcery-ai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds structured substituent_tree metadata and nested decision traces to recursive substituent and component naming, threads this through analysis/engine/public APIs and CLI JSON, and fixes several example scripts and regression tests.

Flow diagram for substituent_tree construction and exposure

flowchart LR
    EngineAnalyze[Engine.analyze / analyze_smiles] --> EngineAnalyzeInner[Engine._analyze]
    EngineAnalyzeInner --> NameComponents["Engine._name_component(return_trace, return_tree)"]
    NameComponents --> ComponentName["component_namer.name_component(return_trace, return_tree)"]
    ComponentName -->|recursive branches| NameSubgraph["namer.name_subgraph(return_trace, return_tree, decision_trace)"]
    NameSubgraph --> ShortcutTree[_shortcut_substituent_tree]
    NameSubgraph --> AssemblyTree[assembly_substituent_tree]
    ShortcutTree --> SubstItemTree[SubstituentItem.substituent_tree]
    AssemblyTree --> SubstItemTree
    SubstItemTree --> PartsTree["assembly_substituent_tree(parts,...)"]
    PartsTree --> AnalysisTree[NameAnalysis.substituent_tree]
    EngineAnalyzeInner --> AnalysisTree
    EngineAnalyze --> Result[ NamingResult ]
    AnalysisTree --> Result
    Result --> ToDict["NamingResult.to_dict(include_trace=True)"]
    ToDict --> JsonOut["CLI / API JSON payload with substituent_tree"]
Loading

File-Level Changes

Change Details Files
Introduce structured functional-prefix representation and capture ligand subtree metadata for direct subgraph prefixes.
  • Add DirectSubgraphPrefix dataclass with ligand/tree/trace/decision metadata and JSON-safe trace_data helper.
  • Change _direct_subgraph_prefix and _direct_amide_subgraph_prefix to return DirectSubgraphPrefix objects (or None) and populate core/group atom/bond fields.
  • In amide prefix handling, name N-ligand subgraphs with decision traces and trees, accumulating ligand_trees, ligand_trace_segments, and ligand_decisions on the prefix object.
  • Add _shortcut_substituent_tree to build minimal substituent tree nodes for shortcut/functional-prefix branches, including embedded functional_prefix metadata and attached ligand trees/trace segments.
src/bluenamer/namer.py
Extend recursive substituent and component naming to build and propagate nested substituent trees and decision traces.
  • Extend name_subgraph signature to support return_tree and an optional DecisionTrace, and add decision-tracked phases for component selection, parent skeleton selection, numbering, and final substituent assembly.
  • Use _assembly_substituent_tree to build full substituent tree nodes from AssemblyParts, or _shortcut_substituent_tree for shortcut branches, and return these alongside names/trace_segments when requested.
  • Thread decision_trace and return_tree flags through name_component, component_namer.name_component, and helper flows so that both components and recursive substituents can emit trees as well as flat traces.
  • Update all recursive branch collectors (e.g., _collect_subgraph_substituents and collect_component_branch_substituents) to request branch trees and nested decision data, and store them on SubstituentItem instances via add_substituent_trace.
src/bluenamer/namer.py
src/bluenamer/component_namer.py
Augment trace helpers and assembly structures to carry nested decision traces and tree instances, and to derive substituent-level trees from AssemblyParts.
  • Add nested_decisions and substituent_tree fields to SubstituentItem and preserve them when merging/grouping substituents in add_substituent_trace.
  • Introduce decision_trace_data utility to convert DecisionTrace objects (or lists) into JSON-safe decision step dicts, and use it throughout nested naming flows.
  • Enhance assembly_trace_segments to propagate nested_decisions into per-substituent trace segments and to include them on synthetic substituent segments when no branch-specific trace exists.
  • Add assembly_substituent_tree plus private helpers (_merge_substituent_tree_instances, _parent_tree_node, _principal_group_tree_node, _simple_item_tree_nodes, _substituent_tree_nodes) to build nested component/substituent trees from AssemblyParts and to handle grouped same-name substituent instances.
src/bluenamer/trace_helpers.py
src/bluenamer/assembly_parts.py
src/bluenamer/namer.py
src/bluenamer/component_namer.py
Expose substituent_tree metadata through the analysis/engine/public API and CLI JSON payloads.
  • Extend NameAnalysis to carry a substituent_tree list and ensure operations are inferred against the same trace segments.
  • Update NamingResult to store substituent_tree, include it in to_dict when include_trace=True, and thread it through Engine.run and Engine.analyze/analyze_smiles.
  • Change Engine._analyze to request trees from _name_component, accumulate per-component trees, and return them as the top-level substituent_tree array on NameAnalysis.
  • Update tests for name(..., include_trace=True) and CLI JSON output to assert substituent_tree presence and basic structure for complex examples.
src/bluenamer/engine.py
src/bluenamer/molecule.py
src/bluenamer/tests/test_public_api.py
Make branch/recursive namer signatures protocol-based and tree-aware for component modifiers and N-substituents.
  • Replace simple Callable-based SubgraphNamer and BranchNamer types with Protocols that describe all combinations of return_trace/return_tree outputs for better type safety.
  • Update component_namer and component_modifiers flows to pass decision_trace objects into branch namers and to accept and propagate returned branch trees.
  • Extend N-substituent handling (_nitrogen_substituent_name and add_component_n_substituents) to capture branch trees and nested decisions for N-ligands and to attach them either as principal suffix modifiers or ordinary substituents.
src/bluenamer/component_namer.py
src/bluenamer/component_modifiers.py
Add regression tests for nested decision preservation, substituent_tree exposure, and tree/trace consistency, plus fix example scripts.
  • Add tests validating nested_decisions propagation through add_substituent_trace and assembly_trace_segments, and grouped tree instance merging when multiple same-name substituents are present.
  • Add analysis/engine-level tests confirming recursive substituent trace segments include nested decisions for all key phases, and that substituent_tree preserves nested branch hierarchy alongside flat trace output.
  • Ensure public API tests assert substituent_tree presence and that CLI JSON includes a substituent_tree field when include_trace is used.
  • Fix example scripts: adjust OPSIN eval output directories for PubChem/ZINC22, import standardize_mol from bluenamer.utils, and correct QM9 batch example to select SMILES via ds[smiles_col].
src/bluenamer/tests/test_analysis.py
src/bluenamer/tests/test_public_api.py
examples/opsin_eval_pubchem.py
examples/opsin_eval_ZINC22.py
examples/test_qm9_opsin_batch.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The BranchNamer type alias in component_modifiers.py still suggests a simple Callable[..., str], but _nitrogen_substituent_name now passes return_trace, return_tree, and decision_trace, and expects a (name, trace, tree) tuple; updating this type alias (or introducing a dedicated protocol) would make the new calling convention clearer and type-checkable.
  • In decision_trace_data, atoms and bonds are converted to plain lists without sorting, whereas most other places in the new tree/trace helpers use sorted(...); consider sorting here as well to keep nested decision payloads deterministic across runs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `BranchNamer` type alias in `component_modifiers.py` still suggests a simple `Callable[..., str]`, but `_nitrogen_substituent_name` now passes `return_trace`, `return_tree`, and `decision_trace`, and expects a `(name, trace, tree)` tuple; updating this type alias (or introducing a dedicated protocol) would make the new calling convention clearer and type-checkable.
- In `decision_trace_data`, `atoms` and `bonds` are converted to plain lists without sorting, whereas most other places in the new tree/trace helpers use `sorted(...)`; consider sorting here as well to keep nested decision payloads deterministic across runs.

## Individual Comments

### Comment 1
<location path="src/bluenamer/component_modifiers.py" line_range="15" />
<code_context>
-from .trace_helpers import add_substituent_trace, bond_ids_within
+from .trace_helpers import add_substituent_trace, bond_ids_within, decision_trace_data

 BranchNamer = Callable[..., str]

</code_context>
<issue_to_address>
**suggestion:** Update `BranchNamer` type alias to match the new `name_subgraph` return type.

`BranchNamer` is still typed as `Callable[..., str]`, but `_nitrogen_substituent_name` now calls `branch_namer` with `return_trace=True` and `return_tree=True`, expecting `tuple[str, list, dict | None]` (matching `name_subgraph`). This type mismatch can mislead type checkers and readers; consider updating `BranchNamer` to the richer callable signature or introducing a separate alias for that form.

Suggested implementation:

```python
BranchNamer = Callable[..., tuple[str, list, dict | None]]

```

You should also:
1. Update any function signatures that accept `branch_namer: BranchNamer` (for example `_nitrogen_substituent_name` or callers around `name_subgraph`) so their return type hints and docstrings, if any, are consistent with `tuple[str, list, dict | None]`.
2. If there are still call sites that only use the simple `str` return form of `branch_namer`, consider introducing a second alias, e.g. `SimpleBranchNamer = Callable[..., str]`, and typing those parameters/variables accordingly to avoid over-constraining them.
</issue_to_address>

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/bluenamer/component_modifiers.py Outdated
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Free

Run ID: 8b67c672-d2fa-4185-927f-ba7ea35bfbdf

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Pro by visiting https://app.coderabbit.ai/login.

Comment @coderabbitai help to get the list of available commands and usage tips.

@r-fedorov

Copy link
Copy Markdown
Collaborator Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The new nested_decisions payloads are copied into multiple places (SubstituentItem, trace segments, tree nodes), which could make trace-enabled outputs quite large for complex molecules; consider whether some of these can be referenced or summarized instead of duplicated to keep JSON size manageable.
  • In assembly_trace_segments, when grouping substituents you overwrite target.substituent_tree with item.substituent_tree for later items with the same name/spiro; if multiple grouped substituents can legitimately have different trees, you may want to either prohibit that or merge/preserve them explicitly rather than silently favoring the last one.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `nested_decisions` payloads are copied into multiple places (SubstituentItem, trace segments, tree nodes), which could make trace-enabled outputs quite large for complex molecules; consider whether some of these can be referenced or summarized instead of duplicated to keep JSON size manageable.
- In `assembly_trace_segments`, when grouping substituents you overwrite `target.substituent_tree` with `item.substituent_tree` for later items with the same name/spiro; if multiple grouped substituents can legitimately have different trees, you may want to either prohibit that or merge/preserve them explicitly rather than silently favoring the last one.

## Individual Comments

### Comment 1
<location path="src/bluenamer/namer.py" line_range="1254" />
<code_context>
+def name_subgraph(
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid repeated assembly work in the `return_trace and return_tree` branch of `name_subgraph`.

In the `return_trace and return_tree` path, `_assembly_trace_segments(parts)` is called once for the trace count and again to produce the returned segments, and `_assembly_substituent_tree` is only called in the final return. To avoid repeated work over `parts`, especially for heavily branched substituents, compute `trace_segments = _assembly_trace_segments(parts)` once, build the tree once, and reuse both across the return branches, as in `component_namer.name_component`.
</issue_to_address>

### Comment 2
<location path="src/bluenamer/trace_helpers.py" line_range="116" />
<code_context>
+def add_substituent_trace(
</code_context>
<issue_to_address>
**issue (bug_risk):** Merging substituents may silently overwrite an existing `substituent_tree`.

When a matching substituent already exists, you merge atom/bond/trace data but always overwrite `existing.substituent_tree` if a new tree is provided. If multiple occurrences of the same substituent can each have their own tree, this will silently discard earlier ones.

If that behavior is intended, consider a brief comment indicating that the tree represents only one instance. If not, you could either assert that `existing.substituent_tree` is `None` before assignment, or define a proper merge strategy for trees (e.g., store them in a collection or otherwise aggregate them).
</issue_to_address>

### Comment 3
<location path="src/bluenamer/trace_helpers.py" line_range="301-310" />
<code_context>
+def assembly_substituent_tree(
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid recomputing `assembly_trace_segments(parts)` when both tree and trace are requested.

`assembly_substituent_tree` always computes `assembly_trace_segments(parts)` for `"trace_segments"`, and `component_namer.name_component` recomputes it when `return_trace` is true, so `return_trace and return_tree` does the work twice. Consider computing `trace_segments` once (e.g., in `name_component`) and either passing it into `assembly_substituent_tree` or reusing it for both the return value and the tree to avoid duplicate work.

Suggested implementation:

```python
def assembly_substituent_tree(
    parts: AssemblyParts,
    *,
    name: str,
    atom_ids=None,
    bond_ids=None,
    decisions=None,
    trace_segments=None,
) -> dict:
    """Return a nested substituent tree from the graph-bound assembly parts."""

    # Allow callers that already computed trace_segments to pass them in, to avoid
    # recomputing assembly_trace_segments(parts) when both tree and trace are requested.
    if trace_segments is None:
        trace_segments = assembly_trace_segments(parts)

    component_atoms = set(atom_ids or parts.parent_atom_ids)

```

1. Inside `assembly_substituent_tree` later in the function, remove any existing call of `assembly_trace_segments(parts)` and use the `trace_segments` variable instead (the new initialization at the top now handles computing it once when not provided).
2. Update the caller in whatever component (likely `component_namer.name_component`) that currently:
   - calls `assembly_trace_segments(parts)` to build the trace to return, and
   - separately calls `assembly_substituent_tree(parts, ...)`

   to instead:
   - compute `trace_segments = assembly_trace_segments(parts)` once,
   - pass `trace_segments=trace_segments` into `assembly_substituent_tree(...)`, and
   - reuse the same `trace_segments` object for the returned trace value.
3. Ensure all other call sites of `assembly_substituent_tree` are updated if they relied on it computing `assembly_trace_segments(parts)` internally; if they do not need trace information, they can ignore the new parameter since it is optional.
</issue_to_address>

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/bluenamer/namer.py
Comment thread src/bluenamer/trace_helpers.py Outdated
Comment thread src/bluenamer/trace_helpers.py
…ts, so callers do not recompute assembly_trace_segments(parts) when both trace and tree are requested, component_namer.name_component() and recursive name_subgraph() now compute trace segments once and reuse them for both return value and tree. Grouped same-name substituents no longer silently overwrite substituent_tree. If multiple instances have different trees, they are preserved
@r-fedorov

Copy link
Copy Markdown
Collaborator Author

@sourcery-ai review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The SubgraphNamer and BranchNamer protocols are almost identical; consider factoring out a shared callable protocol or helper type to avoid duplication and keep their signatures in sync more easily.
  • The return-path handling in name_subgraph (combinations of return_trace / return_tree) is quite branchy and repetitive; extracting a small helper to package (name, trace, tree) consistently would simplify the function and reduce the risk of inconsistent behavior between early exits and the main path.
  • The various shortcut-tree builders (_shortcut_substituent_tree, _shortcut_tree, and assembly_substituent_tree) have overlapping responsibilities and structures; it may be worth unifying them behind a single shared constructor or normalizer to ensure consistent node shapes and reduce maintenance overhead as the tree schema evolves.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `SubgraphNamer` and `BranchNamer` protocols are almost identical; consider factoring out a shared callable protocol or helper type to avoid duplication and keep their signatures in sync more easily.
- The return-path handling in `name_subgraph` (combinations of `return_trace` / `return_tree`) is quite branchy and repetitive; extracting a small helper to package `(name, trace, tree)` consistently would simplify the function and reduce the risk of inconsistent behavior between early exits and the main path.
- The various shortcut-tree builders (`_shortcut_substituent_tree`, `_shortcut_tree`, and `assembly_substituent_tree`) have overlapping responsibilities and structures; it may be worth unifying them behind a single shared constructor or normalizer to ensure consistent node shapes and reduce maintenance overhead as the tree schema evolves.

Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@r-fedorov
r-fedorov merged commit 61f9bf3 into refactor_namer Jun 17, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant