@@ -439,21 +439,27 @@ def expand_probes( # noqa: PLR0915,C901
439439 points in the netlist. Each probe intercepts a connection and exposes forward
440440 and backward traveling wave ports.
441441
442+ Probes can target instance ports at any level of a recursive netlist using
443+ dot-separated paths. For example, ``"sub1.wg1,out0"`` targets port ``out0``
444+ of instance ``wg1`` inside the component used by instance ``sub1``.
445+
442446 Args:
443447 netlist: The netlist to expand probes in.
444448 probes: A mapping from probe names to instance ports where probes should
445- be inserted. If the instance port is part of an existing connection,
446- a 4-port probe is inserted. If the instance port is unconnected,
447- only the "X_fwd" port is created as a direct alias.
448- Example: ``{"mid": "wg1,out"}``
449+ be inserted. Instance ports can use dot-separated paths to target
450+ sub-circuits (e.g. ``"sub1.wg1,out0"``). If the instance port is
451+ part of an existing connection, a 4-port probe is inserted. If the
452+ instance port is unconnected, only the "X_fwd" port is created as a
453+ direct alias.
449454
450455 Returns:
451456 A new netlist with probe instances inserted and connections/ports updated.
452457 For probes on connected ports, two new ports are added: "X_fwd" and "X_bwd".
453458 For probes on unconnected ports, only "X_fwd" is added.
454459
455460 Raises:
456- ValueError: If probe ports would conflict with existing ports.
461+ ValueError: If probe ports would conflict with existing ports, or if a
462+ hierarchical path references a nonexistent instance or component.
457463
458464 Example:
459465 ```python
@@ -471,15 +477,9 @@ def expand_probes( # noqa: PLR0915,C901
471477 if not probes :
472478 return netlist
473479
474- # Handle recursive netlist: only expand probes in top-level
480+ # Handle recursive netlist
475481 if (recnet := sax .try_into [sax .RecursiveNetlist ](netlist )) is not None :
476- top_level_name = next (iter (recnet ))
477- top_level = recnet [top_level_name ]
478- expanded_top = expand_probes (top_level , probes )
479- return {
480- top_level_name : expanded_top ,
481- ** {k : v for k , v in recnet .items () if k != top_level_name },
482- }
482+ return _expand_probes_recursive (recnet , probes )
483483
484484 # It's a flat netlist
485485 net : sax .Netlist = deepcopy (sax .into [sax .Netlist ](netlist ))
@@ -571,6 +571,88 @@ def expand_probes( # noqa: PLR0915,C901
571571 return net
572572
573573
574+ def _expand_probes_recursive ( # noqa: C901
575+ netlist : sax .RecursiveNetlist ,
576+ probes : dict [str , str ],
577+ ) -> sax .RecursiveNetlist :
578+ """Expand probes in a recursive netlist, supporting hierarchical paths."""
579+ recnet = deepcopy (netlist )
580+ top_level_name = next (iter (recnet ))
581+
582+ # Separate top-level probes from hierarchical probes
583+ top_level_probes : dict [str , str ] = {}
584+ # Group hierarchical probes by target component
585+ # Maps: target_component -> {probe_name: instance_port}
586+ component_probes : dict [str , dict [str , str ]] = {}
587+ # Maps: probe_name -> [(instance_name, parent_component), ...]
588+ probe_paths : dict [str , list [tuple [str , str ]]] = {}
589+
590+ for probe_name , instance_port in probes .items ():
591+ parts = instance_port .split ("." )
592+ if len (parts ) == 1 :
593+ top_level_probes [probe_name ] = instance_port
594+ else :
595+ # Walk the hierarchy to find the target component
596+ current_component = top_level_name
597+ path : list [tuple [str , str ]] = []
598+ for instance_name in parts [:- 1 ]:
599+ current_netlist = recnet [current_component ]
600+ instances = current_netlist .get ("instances" , {})
601+ if instance_name not in instances :
602+ msg = (
603+ f"Hierarchical probe '{ probe_name } ': instance "
604+ f"'{ instance_name } ' not found in component "
605+ f"'{ current_component } '. "
606+ f"Available instances: { list (instances .keys ())} "
607+ )
608+ raise ValueError (msg )
609+ inst = instances [instance_name ]
610+ child_component = inst ["component" ] if isinstance (inst , dict ) else inst
611+ if child_component not in recnet :
612+ msg = (
613+ f"Hierarchical probe '{ probe_name } ': component "
614+ f"'{ child_component } ' (used by instance "
615+ f"'{ instance_name } ' in '{ current_component } ') "
616+ f"is not defined in the recursive netlist. "
617+ f"Only sub-circuits (not primitives) can be probed."
618+ )
619+ raise ValueError (msg )
620+ path .append ((instance_name , current_component ))
621+ current_component = child_component
622+
623+ target_instance_port = parts [- 1 ]
624+ if current_component not in component_probes :
625+ component_probes [current_component ] = {}
626+ component_probes [current_component ][probe_name ] = target_instance_port
627+ probe_paths [probe_name ] = path
628+
629+ # Expand top-level probes using existing flat-netlist logic
630+ if top_level_probes :
631+ recnet [top_level_name ] = expand_probes (recnet [top_level_name ], top_level_probes )
632+
633+ # Expand hierarchical probes at their target component level
634+ for component_name , comp_probes in component_probes .items ():
635+ recnet [component_name ] = expand_probes (recnet [component_name ], comp_probes )
636+
637+ # Bubble up: expose probe ports through each intermediate level
638+ for probe_name , path in probe_paths .items ():
639+ fwd_port = f"{ probe_name } _fwd"
640+ bwd_port = f"{ probe_name } _bwd"
641+ for instance_name , parent_component in reversed (path ):
642+ parent_ports = recnet [parent_component ].get ("ports" , {})
643+ if fwd_port in parent_ports or bwd_port in parent_ports :
644+ msg = (
645+ f"Hierarchical probe '{ probe_name } ' would create ports "
646+ f"'{ fwd_port } '/'{ bwd_port } ' which conflict with existing "
647+ f"ports in component '{ parent_component } '."
648+ )
649+ raise ValueError (msg )
650+ parent_ports [fwd_port ] = f"{ instance_name } ,{ fwd_port } "
651+ parent_ports [bwd_port ] = f"{ instance_name } ,{ bwd_port } "
652+
653+ return recnet
654+
655+
574656@overload
575657def extract_port_probes (
576658 netlist : sax .Netlist ,
0 commit comments