autodiff-visualizer / text_trace_implementation_plan.md
Simon Clematide
Implement stepwise text trace, semantic diagram model, and quiet cluster styling
228e074
|
Raw History Blame Contribute Delete
20.3 kB

A newer version of the Gradio SDK is available: 6.30.0

Upgrade

Yes. I would implement the textual view as a second renderer of the same semantic computation model, not as an explanation independently reconstructed from the expression. That is the key architectural decision: graph and text must be two projections of exactly the same forward/backward events.

Implementation plan: Stepwise Text Trace

  1. Define the purpose precisely

The graph and trace answer different questions:

Graph: Where does information flow? Trace: What happens, step by step?

The textual trace should linearize the computation while preserving the visual grammarโ€™s concepts:

FORWARD input values โ†’ local operation โ†’ resulting quantity BACKWARD incoming adjoint โ†’ local derivative/rule โ†’ numerical application โ†’ contribution โ†’ accumulation if necessary โ†’ resulting adjoint

It must not introduce a second autodiff explanation system.

โธป

  1. Make DiagramModel the shared semantic source

Target architecture:

                Tensor/autodiff engine
                       โ”‚
                       โ–ผ
                 DiagramModel
                       โ”‚
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ”‚                         โ”‚
          โ–ผ                         โ–ผ
   Mermaid renderer           Trace renderer
          โ”‚                         โ”‚
          โ–ผ                         โ–ผ
   computation graph        stepwise text trace

The trace renderer must consume the same:

  • quantity IDs: x, y, n1, n2, โ€ฆ, out;
  • operation formulas;
  • operand occurrences;
  • primal values;
  • adjoints;
  • local derivative rules;
  • evaluated local derivatives;
  • gradient contributions;
  • accumulation relationships;
  • output seed.

Do not recompute derivatives inside the trace renderer.

โธป

  1. Add an explicit trace renderer

Create something such as:

trace_renderer.py

with a public entry point conceptually:

render_trace(model, notation, mode) -> str

where:

notation = mathbook | code mode = forward | forward_backward

This should parallel the existing diagram renderer.

โธป

  1. Preserve execution order explicitly

The graph uses spatial topology. The trace needs deterministic sequential order.

Forward

Operations appear in topological execution order:

n1 n2 n3 ... out

Backward

Operations appear in reverse topological order:

out ... n3 n2 n1

Within an operation, operand occurrences retain their original argument order.

For:

n1 = x * x

there are still two operand occurrences. Do not collapse them merely because they refer to the same variable.

โธป

  1. Forward-pass trace grammar

Every forward operation gets the same structure.

For example:

FORWARD PASS Inputs โ”€โ”€โ”€โ”€โ”€โ”€ x = 3 y = 4

  1. Multiply โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ n1 = x ยท x Inputs: x = 3 x = 3 Apply: n1 = 3 ยท 3 Result: n1 = 9
  2. Multiply โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ n2 = n1 ยท y Inputs: n1 = 9 y = 4 Apply: n2 = 9 ยท 4 Result: n2 = 36
  3. Add โ”€โ”€โ”€โ”€โ”€โ”€ out = 2 + y + n2 Inputs: 2 = 2 y = 4 n2 = 36 Apply: out = 2 + 4 + 36 Result: out = 42

The exact amount of repetition can later be tuned, but the schema should initially remain explicit.

That is consistent with the pedagogical goal of this application.

โธป

  1. Treat the Output boundary separately

As in the graph, do not conflate the final operation with the Output presentation quantity.

After the final forward operation:

OUTPUT โ”€โ”€โ”€โ”€โ”€โ”€ out = 42

In Forward+Backward mode this then becomes the starting point for reverse mode:

BACKWARD PASS Seed โ”€โ”€โ”€โ”€ outฬ„ = โˆ‚out/โˆ‚out = 1

This mirrors the established graph semantics exactly.

โธป

  1. Backward-pass trace grammar

Every local backward computation should follow one rigid rhetorical template:

Incoming adjoint โ†“ Local derivative / backward rule โ†“ Numerical application โ†“ Contribution

For a scalar multiplication:

  1. Backpropagate through n2 = n1 ยท y โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Incoming adjoint: nฬ„2 = โˆ‚out/โˆ‚n2 = 1 With respect to n1 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Local derivative: โˆ‚n2/โˆ‚n1 = y Apply: nฬ„2 ยท y = 1 ยท 4 = 4 Contribution to n1: 4 With respect to y โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Local derivative: โˆ‚n2/โˆ‚y = n1 Apply: nฬ„2 ยท n1 = 1 ยท 9 = 9 Contribution to y: 9

This is the textual equivalent of the two derivative trapezoids.

โธป

  1. Make the adjoint interpretation explicit

Whenever a new accumulated adjoint becomes available, state it explicitly:

Adjoint: nฬ„1 = โˆ‚out/โˆ‚n1 = 4

This is important because the adjoint is our notation for:

the chain rule accumulated so far from the output to this quantity.

The trace should therefore use exactly the same adjoint notation as the graph.

โธป

  1. Make accumulation an explicit textual step

Do not silently sum contributions.

For x*x, write:

Backpropagate through n1 = x ยท x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Incoming adjoint: nฬ„1 = โˆ‚out/โˆ‚n1 = 4 Argument 1: x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Local derivative: โˆ‚n1/โˆ‚x = x Apply: nฬ„1 ยท x = 4 ยท 3 = 12 Contribution to x: 12 Argument 2: x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Local derivative: โˆ‚n1/โˆ‚x = x Apply: nฬ„1 ยท x = 4 ยท 3 = 12 Contribution to x: 12 Accumulate contributions to x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ xฬ„ = 12 + 12 = 24 Therefore: xฬ„ = โˆ‚out/โˆ‚x = 24

This is the exact textual equivalent of:

12 โ”€โ”€โ” ฮฃ โ”€โ”€โ†’ xฬ„ = 24 12 โ”€โ”€โ”˜

The trace therefore preserves the core invariant:

Along one backward dependency: apply/multiply. Across multiple contributions: add.

โธป

  1. Distinguish contributions from adjoints

This distinction must be just as strict in text as in the graph.

Before accumulation:

Contribution to x: 12

not:

xฬ„ = 12

if another contribution to x still exists.

Only after all contributions have been collected may the trace say:

xฬ„ = 24

This prevents exactly the conceptual ambiguity the explicit ฮฃ nodes solved in the graph.

โธป

  1. Tensor operations use the same structure

Do not create a separate tensor trace grammar.

For:

n1 = W @ x

write:

Backpropagate through n1 = W @ x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Incoming adjoint: nฬ„1 = [-0.10, 0.24, -0.14] With respect to W โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Local backward rule: Wฬ„ contribution = nฬ„1 xแต€ Apply: [-0.10, 0.24, -0.14] [1, 2]แต€ Result: [[-0.10, -0.20], [ 0.24, 0.48], [-0.14, -0.28]] Contribution to W: [[-0.10, -0.20], [ 0.24, 0.48], [-0.14, -0.28]] With respect to x โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Local backward rule: xฬ„ contribution = Wแต€ nฬ„1 Apply: ... Result: [-0.12, 0.20] Contribution to x: [-0.12, 0.20]

If each target receives only one contribution, those contributions immediately establish the respective adjoints.

โธป

  1. Handle VJPs honestly

For tensor operations, forcing everything into scalar notation such as

\bar v,\frac{\partial v}{\partial x}

can obscure what is actually happening.

The trace should use the operation-specific local backward rule already represented by the model.

For softmax:

Backpropagate through n3 = softmax(n2) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Incoming adjoint: nฬ„3 = [0, 1, 0] Local backward rule: nฬ„2 = n3 โŠ™ (nฬ„3 - sum(nฬ„3 โŠ™ n3)) Values: n3 = [0.18, 0.59, 0.24] nฬ„3 = [0, 1, 0] Apply: ... Result: nฬ„2 = [-0.10, 0.24, -0.14]

This keeps the same conceptual ruleโ€”incoming adjoint transformed by the local backward operationโ€”without pretending every tensor derivative should be displayed as a huge Jacobian.

โธป

  1. Reuse the canonical tensor formatter

The trace must use the tensor-display policy we just defined.

Initially:

element budget = 16

or, if you deliberately want more detail in text:

graph budget = 16 trace budget = 32

But both must use the same formatter and semantics.

Thus a 3\times2 gradient must appear as its six actual values:

[[-0.10, -0.20], [ 0.24, 0.48], [-0.14, -0.28]]

Never:

shape (3, 2)

For a large tensor:

[[0.12, -0.31, โ€ฆ, 0.08], [ โ‹ฎ โ‹ฎ โ‹ฑ โ‹ฎ ], [-0.42, 0.06, โ€ฆ, 0.29]] shape (128, 64)

โธป

  1. Respect Forward-only mode strictly

If the user selects Forward only, the textual trace must end after the forward result.

For example:

FORWARD PASS Inputs โ”€โ”€โ”€โ”€โ”€โ”€ x = 3 y = 4 ... OUTPUT โ”€โ”€โ”€โ”€โ”€โ”€ out = 42

There must be no:

BACKWARD PASS outฬ„ gradient derivative contribution adjoint โ€”

This is the textual counterpart of the Forward-only purity invariant.

โธป

  1. Support Mathbook and Code notation

The trace should obey the existing notation toggle.

Mathbook

Local derivative: โˆ‚n2/โˆ‚y = n1 Apply: nฬ„2 ยท n1 = 1 ยท 9 = 9

Code

Local derivative: diff(n1 _ y, y) = n1 Apply: n2_bar _ n1 = 1 * 9 = 9

Do not implement separate mathematical semantics. Only notation changes.

Use the existing centralized formatting helpers wherever possible.

โธป

  1. Introduce structured trace events if DiagramModel is insufficient

This is the one architectural extension I would seriously consider.

Rendering directly by reverse-engineering edges can become fragile. If DiagramModel does not already make execution steps explicit enough, add semantic objects such as:

@dataclass class ForwardStep: result: QuantityState operation: OperationState operands: list[QuantityState] @dataclass class BackwardOperandStep: operation: OperationState operand_occurrence: int target: QuantityState incoming_adjoint: Any local_rule: ... local_value: Any contribution: Any @dataclass class AccumulationStep: target: QuantityState contributions: list[Any] resulting_adjoint: Any

Then both renderers can consume these.

The important architecture is:

autodiff computation โ”‚ โ–ผ semantic steps โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ” โ–ผ โ–ผ graph text

not:

autodiff โ†’ graph โ”‚ โ–ผ parse graph back into explanation

โธป

  1. Keep operation identity separate from pedagogical quantity names

The trace should use the established:

x, y, W, b n1, n2, n3, ... out

Do not reintroduce O1, O2, etc.

If an internal operation ID is required, keep it internal.

The learner should see:

  1. n2 = n1 + b

not:

Operation O3

unless there is eventually a specific debugging mode that exposes internal IDs.

โธป

  1. Do not overuse operation names

A heading like:

  1. Multiply

can be useful, but the actual formula should remain authoritative:

n2 = n1 ยท y

For less familiar operations:

  1. Softmax โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ n3 = softmax(n2)

This is clearer than generating verbose prose such as:

โ€œThe third operation applies the softmax function to the second intermediate node.โ€

The trace should remain computational rather than narrative.

โธป

  1. Suggested complete scalar fixture

Use the existing canonical expression:

out=y(x x)+(y+2),\quad x=3,;y=4.

A shortened target trace is:

FORWARD PASS

Inputs x = 3 y = 4

  1. n1 = x ยท x = 3 ยท 3 = 9
  2. n2 = n1 ยท y = 9 ยท 4 = 36
  3. out = n2 + y + 2 = 36 + 4 + 2 = 42 OUTPUT out = 42 BACKWARD PASS

    Seed outฬ„ = โˆ‚out/โˆ‚out = 1
  4. Backpropagate through out = n2 + y + 2 To n2: โˆ‚out/โˆ‚n2 = 1 outฬ„ ยท 1 = 1 ยท 1 = 1 contribution = 1 To y: โˆ‚out/โˆ‚y = 1 outฬ„ ยท 1 = 1 ยท 1 = 1 contribution = 1 Therefore: nฬ„2 = โˆ‚out/โˆ‚n2 = 1
  5. Backpropagate through n2 = n1 ยท y Incoming: nฬ„2 = 1 To n1: โˆ‚n2/โˆ‚n1 = y nฬ„2 ยท y = 1 ยท 4 = 4 contribution = 4 To y: โˆ‚n2/โˆ‚y = n1 nฬ„2 ยท n1 = 1 ยท 9 = 9 contribution = 9 Therefore: nฬ„1 = โˆ‚out/โˆ‚n1 = 4
  6. Backpropagate through n1 = x ยท x Incoming: nฬ„1 = 4 Argument 1: โˆ‚n1/โˆ‚x = x nฬ„1 ยท x = 4 ยท 3 = 12 contribution = 12 Argument 2: โˆ‚n1/โˆ‚x = x nฬ„1 ยท x = 4 ยท 3 = 12 contribution = 12 ACCUMULATION

    For x: xฬ„ = 12 + 12 = 24 For y: yฬ„ = 1 + 9 = 10 FINAL GRADIENTS

    โˆ‚out/โˆ‚x = xฬ„ = 24 โˆ‚out/โˆ‚y = yฬ„ = 10

There is one refinement I would make in the real implementation: perform each accumulation at the point where the resulting adjoint becomes available, rather than collecting all accumulation into a final section. That better reflects reverse-mode execution:

contributions to y: 1 + 9 โ†“ yฬ„ = 10

and then continue.

โธป

  1. Make accumulation timing semantically correct

This is important for shared intermediates.

The trace must not backpropagate through quantity v until all downstream contributions to v have been accumulated.

For example:

contribution to n1 from path A = ... contribution to n1 from path B = ... Accumulate: nฬ„1 = contribution A + contribution B

Only then:

Backpropagate through n1 = ... Incoming adjoint: nฬ„1 = ...

That reproduces the semantics of the graphโ€™s ฮฃ node.

โธป

  1. Add dedicated tests at the semantic level

Tests should verify more than strings.

For x*x:

two operand backward steps two contributions to x one accumulation step final xฬ„ = 2x

For x**2 + x:

two independent contributions to x one accumulation

For (x+y)*(x+y):

multiple contributions to shared intermediate accumulation occurs before backpropagating through that intermediate

For tensor cases:

actual tensor values present no shape substituted for value VJP dimensions correct

โธป

  1. Add rendering snapshot tests

Then test representative complete trace strings for:

  1. x*y
  2. x*x
  3. x**2 + x
  4. (x+y)*(x+y)
  5. (x*x)*y + (y+2)
  6. W @ x
  7. softmax(W @ x + b)[1]
  8. the larger loss example.

Test both Mathbook and Code modes where notation differs.

โธป

  1. Add mode-purity tests

For forward:

assert no_adjoint_symbols assert no_local_derivatives assert no_contributions assert no_accumulations assert no_backward_heading

For forward_backward:

assert seed_present assert incoming_adjoints_present assert local_rules_present assert numerical_applications_present assert accumulations_present_when_required

This should parallel the diagram mode-purity tests.

โธป

  1. UI integration

I would expose the trace as a view choice, not as another mathematical mode.

For example:

View: โ—‹ Graph โ—‹ Step-by-step

while keeping:

Pass: โ—‹ Forward โ—‹ Forward + Backward Notation: โ—‹ Mathbook โ—‹ Code

These dimensions should remain orthogonal:

         Graph       Step-by-step

Forward โœ“ โœ“ Forward+Back โœ“ โœ“ Mathbook โœ“ โœ“ Code โœ“ โœ“

A future third option could show both side by side, but I would not start there.

โธป

  1. Help text should explicitly connect the two views

A short explanation can say:

Graph view shows where values and gradients flow through the computation graph. Step-by-step view shows the same computation in execution order. During the backward pass, each step starts with the current adjoint, applies one local backward rule, and produces a contribution. Contributions to the same quantity are added to form its adjoint.

That explicitly teaches that these are two representations of one process.

โธป

  1. Final implementation sequence

I would implement in this order:

  1. Confirm DiagramModel exposes all required semantic data.
  2. Add structured ForwardStep, BackwardOperandStep, and AccumulationStep objects only where needed.
  3. Generate these steps deterministically from the computation model.
  4. Implement trace_renderer.py.
  5. Implement Forward-only trace first.
  6. Add output boundary and backward seed.
  7. Add scalar backward steps.
  8. Add explicit accumulation.
  9. Add tensor/VJP backward steps.
  10. Route all numerical values through the canonical tensor formatter.
  11. Add Mathbook/Code formatting.
  12. Add semantic tests.
  13. Add trace snapshot tests.
  14. Add mode-purity tests.
  15. Integrate the view selector into the app.
  16. Update VISUAL_LANGUAGE.md.
  17. Visually compare graph and trace for all canonical fixtures.

The final acceptance invariant should be:

For every piece of information visible in the stepwise trace, there is a corresponding semantic object in the computation model; and for every forward/backward computation represented in the graph, the trace can present the same computation sequentially without recomputing or reinterpreting it.

That will keep the textual view from becoming a parallel explanation system that gradually diverges from the graphโ€”the main implementation risk I would avoid.

Yes. I think two explicit action buttons are better than introducing another โ€œViewโ€ radio selector. The current UI already has two conceptual settingsโ€”computation mode and notationโ€”so output type can naturally be the action the user takes.

I would change the bottom row to:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Generate Diagram โ”‚ Generate Trace โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Both buttons consume exactly the same settings above them:

Expression Input Values Graph/Computation Mode Derivative Notation

I would probably rename โ€œGraph Modeโ€ โ†’ โ€œComputation Modeโ€ at the same time. Once the application produces text traces, โ€œGraph Modeโ€ is misleading because Forward Only versus Forward & Backward controls the semantics of both representations.

So the conceptual UI becomes:

Expression Input Values Computation Mode Derivative Notation โ—‹ Forward & Backward โ—‹ Mathbook โ—‹ Forward Only โ—‹ Code โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ Generate Diagram โ”‚ โ”‚ Generate Trace โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

And the behavior is particularly clean:

Computation mode Generate Diagram Generate Trace Forward Only pure forward graph pure forward stepwise trace Forward & Backward complete autodiff graph forward + backward stepwise trace

The notation selection applies identically to both.

I would also keep the buttons visually equal. โ€œGenerate Diagramโ€ should not look primary and โ€œGenerate Traceโ€ secondary, because these are two equivalent representations of the same computation, not a main action plus an auxiliary feature.

One small naming point: I prefer Generate Trace over โ€œGenerate Textโ€ or โ€œText View.โ€ โ€œTraceโ€ accurately communicates that this is the computation presented in execution order. The output itself can then be headed Step-by-step trace to make the term immediately understandable.

Architecturally, this fits the plan nicely:

                   current settings
                         โ”‚
             expression + inputs
             computation mode
             notation
                         โ”‚
               build semantic model
                         โ”‚
          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
          โ–ผ                             โ–ผ
  Generate Diagram               Generate Trace
          โ”‚                             โ”‚
          โ–ผ                             โ–ผ
 Mermaid renderer                Trace renderer

Most importantly, the buttons select the representation; they must not select different computation logic. That keeps the UI aligned with the architecture we have been defining.