Skip to main content

gtpyhop-diagnostics

Failure attribution for GTPyhop plans: when find_plan fails, name the precondition that blocked it.

PlanTrace (in gtpyhop-core 2.0.0) tells you which action the search died on. This package tells you why:

pickup('a') was blocked by: s.clear[x] == True

Install

pip install gtpyhop-diagnostics

It depends only on gtpyhop-core>=2.0.0 — no bundled examples, no third-party dependencies, and the analysis uses the standard library's ast.

Usage

Plan with both trace=True and trace_state=True, then explain the result:

import gtpyhop
from gtpyhop.diagnostics import explain_dead_end

with gtpyhop.PlannerSession(domain=my_domain, verbose=0) as session:
    result = session.find_plan(state, tasks, trace=True, trace_state=True)

report = explain_dead_end(result.trace, "path/to/domain.py")

print(report.summary())        # "pickup('a') was blocked by: s.clear[x] == True"
report.action                  # 'pickup'
report.args                    # ('a',)
report.blocking                # atoms that evaluated false
report.blocking_vars           # ['clear']
report.candidates              # every guard atom found
report.unevaluated             # atoms that could not be decided, each with a reason

explain_dead_end accepts a .py file or a directory to search, so you can point it at a whole example collection without knowing which file defines the action.

If an action's guard calls a helper (simple_htn's is_a, say), pass the defining module's globals so those calls can be resolved:

import sys
ns = vars(sys.modules[my_domain.__module__])
report = explain_dead_end(result.trace, "domain.py", namespace=ns)

Without it, atoms calling that helper are reported in unevaluated — not guessed at.

How it works

Neither half is sufficient alone:

  • Static analysis of the domain source yields an action's candidate preconditions. For pickup that is three guards, with no way to tell which one blocked the plan.
  • TraceEvent.state — the snapshot recorded when planning with trace_state=True — is the state the action was actually evaluated on.

Each guard is split into atoms and evaluated against that snapshot. The atoms that come out false are the blocking preconditions.

Evaluation rather than pattern-matching is what makes this accurate: a TraceEvent's item tuple carries the action's actual arguments (('pickup', 'a')), and the function's AST carries its parameter names. Binding them together lets each conjunct run exactly as the planner would have, which covers comparisons, helper calls and arbitrary expressions that pattern-matching could not.

Both precondition idioms

GTPyhop domains guard preconditions in two structurally inverted shapes, and both are handled:

# Negative guard-and-bail -- what the domain style guide teaches
def a_open_door(state, door):
    if not (hasattr(state, 'door_unlocked') and state.door_unlocked[door]):
        return False          # the `if` bails, so the requirement is its negation
    ...

# Positive wrapping guard -- GTPyhop's original idiom
def pickup(s, x):
    if s.pos[x] == 'table' and s.clear[x] and s.holding['hand'] == False:
        ...                   # the `if` wraps the effects, so the test IS the requirement
        return s

not (A and B) is unwrapped before splitting, so a report names the single conjunct that failed rather than blaming the whole condition.

or is deliberately not split: with a disjunction it is the combination that fails, so naming one side as "the blocking precondition" would be false. Such an expression is kept whole.

Coverage

Measured against the 293 actions declared across all bundled gtpyhop-examples collections: 285 (97%) have their guards recognised. The other 8 have no precondition guard to find — drive_truck, load_truck, fly_plane, load_plane and putv are unconditional, and the three c_pay_driver commands delegate to another function. An action whose check lives in a delegate is a known limitation.

Honest degradation

A diagnostic tool that blows up on an unusual domain is worse than one that says what it could not determine, so every degraded case is reported rather than raised:

Situation What you get
trace=False / no trace a report saying which flags to pass
trace_state=False candidate preconditions only, with that stated — never a claimed culprit
plan succeeded "no dead end recorded"
dead end is task_exhausted / goal_exhausted / multigoal_exhausted reported as a refinement dead end; precondition attribution does not apply, since no single action's guard is at fault
dead end is malformed_return reported as such, with what the action actually returned
action's source not found says so, naming the action and where it looked
an atom raises, or calls an unresolvable helper that atom lands in unevaluated with the reason

A note on executing domain code

Evaluating a guard means executing an expression from the domain. find_plan has already called those very functions, so this adds no exposure that planning did not. This is not a sandbox and is not meant to be one: if a domain is untrusted, it was already untrusted when it was planned with.

Scope

Deliberately outside gtpyhop-core — the planner should not carry a source-analysis layer, and this is an optional add-on with its own release cadence. It reads only the public PlanTrace API and never touches Domain's private action dictionary.

Not in v0.1: producer/effect analysis (which action could set the blocking variable), and attribution for refinement dead ends.

License

Clear BSD License, matching the rest of GTPyhop.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gtpyhop_diagnostics-0.1.0.tar.gz (11.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gtpyhop_diagnostics-0.1.0-py3-none-any.whl (12.4 kB view details)

Uploaded Python 3

File details

Details for the file gtpyhop_diagnostics-0.1.0.tar.gz.

File metadata

  • Download URL: gtpyhop_diagnostics-0.1.0.tar.gz
  • Upload date:
  • Size: 11.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for gtpyhop_diagnostics-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f1af0900abeffbd37720bcadbcfadaa97e073d656fe63d837fb67a1e9465bb50
MD5 e8fef94d9af3bf4e27e32bee8933631b
BLAKE2b-256 8fb7091bd6a20d8508e6fe2fad159f8f64ca3468e10356bee14ea84ef0e4f92f

See more details on using hashes here.

Provenance

The following attestation bundles were made for gtpyhop_diagnostics-0.1.0.tar.gz:

Publisher: publish-diagnostics-to-pypi.yml on PCfVW/GTPyhop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file gtpyhop_diagnostics-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for gtpyhop_diagnostics-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2dfb47eba4fcea8d4a0c2392073f4671d008b433dc977f2b6a54488e42eca034
MD5 dc5bb7fc26099b7e5eef4c23a255dfed
BLAKE2b-256 c5624d95d4b5c61fc93abebb05b7a8dbe517183d733390f4c6cbe38f876e505e

See more details on using hashes here.

Provenance

The following attestation bundles were made for gtpyhop_diagnostics-0.1.0-py3-none-any.whl:

Publisher: publish-diagnostics-to-pypi.yml on PCfVW/GTPyhop

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page