Back to skills
SkillHub ClubWrite Technical DocsFull StackTech WriterTesting

netgraph-dsl

NetGraph scenario DSL for defining network topologies, traffic demands, failure policies, and analysis workflows in YAML. Use when: creating or editing .yaml/.yml network scenarios, defining nodes/links/groups, writing link rules with patterns, configuring selectors or blueprints, setting up traffic demands or failure policies, running scenarios and interpreting results, debugging DSL syntax or validation errors, or asking about NetGraph scenario structure.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
0
Hot score
74
Updated
March 20, 2026
Overall rating
C0.5
Composite score
0.5
Best-practice grade
N/A

Install command

npx @skill-hub/cli install networmix-netgraph-netgraph-dsl
network-simulationyaml-dsltopology-modelingnetwork-testingconfiguration

Repository

networmix/NetGraph

Skill path: .claude/skills/netgraph-dsl

NetGraph scenario DSL for defining network topologies, traffic demands, failure policies, and analysis workflows in YAML. Use when: creating or editing .yaml/.yml network scenarios, defining nodes/links/groups, writing link rules with patterns, configuring selectors or blueprints, setting up traffic demands or failure policies, running scenarios and interpreting results, debugging DSL syntax or validation errors, or asking about NetGraph scenario structure.

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Tech Writer, Testing.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: networmix.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install netgraph-dsl into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/networmix/NetGraph before adding netgraph-dsl to shared team environments
  • Use netgraph-dsl for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: netgraph-dsl
description: >
  NetGraph scenario DSL for defining network topologies, traffic demands, failure policies,
  and analysis workflows in YAML. Use when: creating or editing .yaml/.yml network scenarios,
  defining nodes/links/groups, writing link rules with patterns, configuring selectors or blueprints,
  setting up traffic demands or failure policies, running scenarios and interpreting results,
  debugging DSL syntax or validation errors, or asking about NetGraph scenario structure.
---

# NetGraph DSL

Define network simulation scenarios in YAML format.

> **Quick Start**: See [Minimal Example](#minimal-example) below.
> **Complete Reference**: See [references/REFERENCE.md](references/REFERENCE.md) for full documentation.

## Instructions

When working with NetGraph scenarios:

1. **Creating new scenarios**: Start with the [Minimal Example](#minimal-example), then add sections as needed
2. **Editing existing scenarios**: Identify the relevant section (network, demands, failures, etc.)
3. **Understanding selection**: Review [Selection Models](#selection-models) to understand path-based vs condition-based selection
4. **Debugging issues**: Check [Common Pitfalls](#common-pitfalls) and [Validation Checklist](#validation-checklist)
5. **Complex topologies**: Use [Blueprints](#blueprints) for reusable patterns
6. **Failure simulation**: Define [Risk Groups](#risk-groups) before creating failure policies

Refer to specific sections below for detailed syntax and examples.

## Quick Reference

| Section | Purpose |
|---------|---------|
| `network` | Topology: nodes, links (required) |
| `blueprints` | Reusable topology templates |
| `components` | Hardware library for cost/power modeling |
| `risk_groups` | Failure correlation groups |
| `vars` | YAML anchors for value reuse |
| `demands` | Traffic demand definitions |
| `failures` | Failure simulation rules |
| `workflow` | Analysis execution steps |
| `seed` | Master seed for reproducibility |

## Minimal Example

```yaml
network:
  nodes:
    A: {}
    B: {}
  links:
    - source: A
      target: B
      capacity: 100
      cost: 1
```

## Core Patterns

### Selection Models

The DSL implements two distinct selection patterns:

**1. Path-based Node Selection** (link rules, traffic demands, workflow steps)

- Uses regex patterns on hierarchical node names
- Supports capture group-based grouping
- Supports attribute-based grouping (`group_by`)
- Supports attribute filtering (`match` conditions)
- Supports `active_only` filtering

**2. Condition-based Entity Selection** (failure rules, membership rules, risk group generation)

- Works on nodes/links; failure + membership can also target risk_groups
- Optional regex `path` filter on entity names/IDs (no capture grouping)
- Attribute filtering via `match.conditions` (`match.logic` defaults vary by context)

These patterns share common primitives (condition evaluation, match specification) but serve different purposes and should not be confused.

> **For comprehensive details** on entity creation flows, processing steps, and comparison tables, see the [Entity Creation Architecture](references/REFERENCE.md#entity-creation-architecture) section in the full reference.

### Nodes and Links

```yaml
network:
  nodes:
    Seattle:
      attrs:           # Custom attributes go here
        role: core
      risk_groups: ["RG1"]
      disabled: false
    Portland:
      attrs:
        role: edge

  links:
    - source: Seattle
      target: Portland
      capacity: 100    # Direct properties (no wrapper)
      cost: 10
      attrs:
        distance_km: 280
      count: 2         # Parallel links
```

### Node Groups

```yaml
network:
  nodes:
    leaf:
      count: 4
      template: "leaf-{n}"
      attrs:
        role: leaf
```

Creates: `leaf/leaf-1`, `leaf/leaf-2`, `leaf/leaf-3`, `leaf/leaf-4`

### Template Syntaxes

| Syntax | Example | Context |
|--------|---------|---------|
| `[1-3]` | `dc[1-3]/rack` | Group names, risk groups |
| `$var`/`${var}` | `pod${p}/leaf` | Links, rules, demands |
| `{n}` | `srv-{n}` | `template` field |

These are NOT interchangeable. See [REFERENCE.md](references/REFERENCE.md) for details.

### Bracket Expansion

```yaml
network:
  nodes:
    dc[1-3]/rack[a,b]:    # Cartesian product
      count: 4
      template: "srv-{n}"
```

Creates: `dc1/racka`, `dc1/rackb`, `dc2/racka`, `dc2/rackb`, `dc3/racka`, `dc3/rackb`

**Scope**: Bracket expansion works in group names, risk group definitions (including children), and risk group membership arrays. Component names and other fields treat brackets as literal characters.

### Link Patterns

```yaml
network:
  links:
    - source: /leaf
      target: /spine
      pattern: mesh        # Every source to every target
      capacity: 100

    - source: /group_a     # 4 nodes
      target: /group_b     # 2 nodes
      pattern: one_to_one  # Pairwise with modulo wrap (sizes must have multiple factor)
```

### Selectors with Conditions

```yaml
network:
  links:
    - source:
        path: "/datacenter"
        match:
          logic: and         # "and" or "or"; defaults vary by context (see below)
          conditions:
            - attr: role
              op: "=="
              value: leaf
      target: /spine
      pattern: mesh
```

**Operators**: `==`, `!=`, `<`, `<=`, `>`, `>=`, `contains`, `not_contains`, `in`, `not_in`, `exists`, `not_exists`

**Logic defaults by context (for `match.logic`)**:

| Context | Default `logic` | Rationale |
|---------|-----------------|-----------|
| Link `match` | `"or"` | Inclusive: match any condition |
| Demand `match` | `"or"` | Inclusive: match any condition |
| Membership rules | `"and"` | Precise: must match all conditions |
| Failure rules | `"or"` | Inclusive: match any condition |

### Capturing Groups for Grouping

```yaml
# Single capture group creates groups by captured value
source: "^(dc[1-3])/.*"     # Groups: dc1, dc2, dc3

# Multiple capture groups join with |
source: "^(dc\\d+)/(spine|leaf)/.*"  # Groups: dc1|spine, dc1|leaf, etc.
```

### Variable Expansion

```yaml
links:
  - source: "plane${p}/rack"
    target: "spine${s}"
    expand:
      vars:
        p: [1, 2]
        s: [1, 2, 3]
      mode: cartesian  # or "zip" (equal-length lists required)
    pattern: mesh
```

**Expansion limit**: Maximum 10,000 expansions per template. Exceeding this raises an error.

### Blueprints

```yaml
blueprints:
  clos_pod:
    nodes:
      leaf:
        count: 4
        template: "leaf-{n}"
      spine:
        count: 2
        template: "spine-{n}"
    links:
      - source: /leaf
        target: /spine
        pattern: mesh
        capacity: 100

network:
  nodes:
    pod[1-2]:
      blueprint: clos_pod
      params:
        leaf.count: 6  # Override defaults
```

**Alternative: Inline nested nodes** (no blueprint needed):

```yaml
network:
  nodes:
    datacenter:
      nodes:              # Inline hierarchy
        rack1:
          count: 2
          template: "srv-{n}"
```

### Node and Link Rules

Modify entities after creation with optional attribute filtering:

```yaml
network:
  node_rules:
    - path: "^pod1/.*"
      match:                      # Optional: filter by attributes
        conditions:
          - {attr: role, op: "==", value: compute}
      disabled: true

  link_rules:
    - source: "^pod1/.*"
      target: "^pod2/.*"
      link_match:                 # Optional: filter by link attributes
        conditions:
          - {attr: capacity, op: ">=", value: 400}
      cost: 99
```

Rules also support `expand` for variable-based application.

### Traffic Demands

```yaml
demands:
  production:
    - source: "^dc1/.*"
      target: "^dc2/.*"
      volume: 1000
      mode: pairwise       # or "combine"
      flow_policy: SHORTEST_PATHS_ECMP
```

**Flow policies**: `SHORTEST_PATHS_ECMP`, `SHORTEST_PATHS_WCMP`, `TE_WCMP_UNLIM`, `TE_ECMP_16_LSP`, `TE_ECMP_UP_TO_256_LSP`

### Failure Policies

```yaml
failures:
  single_link:
    expand_groups: false         # Expand to shared-risk entities
    modes:                       # Weighted modes (one selected per iteration)
      - weight: 1.0
        rules:
          - scope: link          # Required: node, link, or risk_group
            mode: choice         # all, choice, or random
            count: 1
            # Optional: weight_by: capacity  # Weighted sampling by attribute
```

**Rule modes**: `all` (select all matches), `choice` (sample `count`), `random` (each with `probability`)

### Risk Groups

Risk groups model failure correlation (shared infrastructure, geographic regions, vendor dependencies, or any custom domain). Three methods:

**Direct definition:**

```yaml
risk_groups:
  - name: "RG1"              # Full form
  - "RG2"                    # String shorthand (equivalent to {name: "RG2"})
```

**Membership rules** (assign entities by attribute matching; optional `path` filter):

```yaml
risk_groups:
  - name: HighCapacityLinks
    membership:
      scope: link            # Required: node, link, or risk_group
      match:
        logic: and           # "and" or "or" (default: "and" for membership)
        conditions:
          - attr: capacity
            op: ">="
            value: 1000
```

**Generate blocks** (create groups from unique attribute values; optional `path` filter):

```yaml
risk_groups:
  - generate:
      scope: node            # Required: node or link only
      path: "^prod_.*"       # Optional: narrow entities before grouping
      group_by: region       # Any attribute to group by
      name: "Region_${value}"
```

**Validation:** Risk group references are validated at load time (undefined references and circular hierarchies detected).

See [REFERENCE.md](references/REFERENCE.md) for complete details.

### Workflow

```yaml
workflow:
  - type: NetworkStats
    name: stats
  - type: MaximumSupportedDemand
    name: msd
    demand_set: production
    alpha_start: 1.0
    resolution: 0.05
  - type: TrafficMatrixPlacement
    name: placement
    demand_set: production
    failure_policy: single_link
    iterations: 1000
    alpha_from_step: msd          # Reference MSD result
    alpha_from_field: data.alpha_star
  - type: MaxFlow
    source: "^(dc[1-3])$"
    target: "^(dc[1-3])$"
    mode: pairwise
    failure_policy: single_link
    iterations: 1000
    seed: 42                      # Optional: for reproducibility
```

**Step types**: `BuildGraph`, `NetworkStats`, `MaxFlow`, `TrafficMatrixPlacement`, `MaximumSupportedDemand`, `CostPower`

## Common Pitfalls

### 1. Custom fields must go in `attrs`

```yaml
# WRONG
nodes:
  A:
    my_field: value    # Error!

# CORRECT
nodes:
  A:
    attrs:
      my_field: value
```

### 2. Link properties are flattened (no wrapper)

```yaml
# WRONG
links:
  - source: A
    target: B
    link_params:       # Error! No wrapper
      capacity: 100

# CORRECT
links:
  - source: A
    target: B
    capacity: 100      # Direct property
```

### 3. `one_to_one` requires compatible sizes

Sizes must have a multiple factor (4-to-2 OK, 3-to-2 ERROR).

### 4. Path patterns are anchored at start

```yaml
path: "leaf"       # Only matches names STARTING with "leaf"
path: ".*leaf.*"   # Matches "leaf" anywhere
```

**Note**: Leading `/` is stripped and has no effect. `/leaf` and `leaf` are equivalent. All paths are relative to the current scope (blueprint instantiation path or network root).

### 5. Variable syntax uses `$` prefix

```yaml
# WRONG (conflicts with regex {m,n})
source: "{dc}/leaf"

# CORRECT
source: "${dc}/leaf"
```

### 6. `zip` requires equal-length lists

```yaml
# WRONG
expand:
  vars:
    a: [1, 2]
    b: [x, y, z]     # Length mismatch!
  mode: zip
```

### 7. Processing order matters

1. Groups and direct nodes created
2. Node rules applied
3. Blueprint links expanded
4. Top-level links expanded (including direct links)
5. Link rules applied

Rules only affect entities that exist at their processing stage.

## Validation Checklist

- [ ] Custom fields inside `attrs`
- [ ] Link properties directly on link (no wrapper)
- [ ] Referenced blueprints exist
- [ ] Node names in direct links exist
- [ ] `one_to_one` sizes have multiple factor
- [ ] `zip` lists have equal length
- [ ] Selectors have at least one of: `path`, `group_by`, `match`

## More Information

- [Full DSL Reference](references/REFERENCE.md) - Complete field documentation, all operators, workflow steps
- [Working Examples](references/EXAMPLES.md) - 22 complete scenarios from simple to advanced

## Running Scenarios

**Always validate before running:**

```bash
./venv/bin/ngraph inspect scenario.yaml && ./venv/bin/ngraph run scenario.yaml
```

### CLI Commands

| Command | Purpose |
|---------|---------|
| `ngraph inspect <file>` | Validate and summarize scenario |
| `ngraph inspect -d <file>` | Detailed view with node/link tables |
| `ngraph run <file>` | Execute and write `<file>.results.json` |
| `ngraph run <file> --stdout` | Execute and print results to stdout |
| `ngraph run <file> --profile` | Execute with CPU profiling |

### Success Indicators

**inspect success:**
```
✓ YAML file loaded successfully
✓ Scenario structure is valid
```

**run success:**
```
✅ Scenario execution completed
✅ Results written to: scenario.results.json
```
Exit code 0, results JSON created.

### Failure Indicators

**Schema validation error:**
```
❌ ERROR: Failed to inspect scenario
  ValidationError: Additional properties are not allowed ('bad_field' was unexpected)
On instance['network']['nodes']['A']:
    {'bad_field': 'x'}
```
Fix: Move custom fields inside `attrs: {}`.

**Missing node reference:**
```
ValueError: Source node 'X' not found in network
```
Fix: Check node name spelling or pattern matching.

**Empty selector match:**
```
WARNING: No nodes matched selector
```
Fix: Verify regex pattern matches actual node names.

### Interpreting Results

Results JSON structure:
```json
{
  "steps": {
    "<step_name>": {
      "metadata": { "duration_sec": 0.5 },
      "data": { ... }
    }
  }
}
```

**Key metrics by step type:**

| Step | Key Field | Good Value | Problem |
|------|-----------|------------|---------|
| MaximumSupportedDemand | `alpha_star` | >= 1.0 | < 1.0 means network undersized |
| TrafficMatrixPlacement | `flow_results[].summary.total_dropped` | 0 | > 0 means congestion |
| MaxFlow | `flow_results[].summary.total_placed` | = total_demand | < demand means bottleneck |

**Quick validation after run:**

```bash
# Check alpha_star (should be >= 1.0)
grep -o '"alpha_star": [0-9.]*' scenario.results.json

# Check for dropped traffic (should be 0)
grep -o '"total_dropped": [0-9.]*' scenario.results.json | head -5
```

### Common Errors and Fixes

| Error | Cause | Fix |
|-------|-------|-----|
| `Additional properties are not allowed` | Custom field outside `attrs` | Move to `attrs: {field: value}` |
| `Source node 'X' not found` | Link references non-existent node | Fix node name or create the node |
| `one_to_one pattern requires sizes with multiple factor` | Mismatched group sizes | Use sizes like 4-to-2, not 3-to-2 |
| `Variable '$x' not found in expand.vars` | Missing variable definition | Add to `expand: {vars: {x: [...]}}` |
| `zip expansion requires equal-length lists` | Lists have different lengths | Make lists same length or use `cartesian` |

### Profiling Scenarios

When performance matters:

```bash
./venv/bin/ngraph run scenario.yaml --profile --profile-memory
```

Output shows:
- **Step timing**: Time per workflow step
- **Bottlenecks**: Steps taking >10% of total time
- **Memory**: Peak memory per step (with `--profile-memory`)
- **Recommendations**: Optimization suggestions

### Iteration Pattern

1. Write scenario YAML
2. `./venv/bin/ngraph inspect scenario.yaml` - fix any errors
3. `./venv/bin/ngraph run scenario.yaml`
4. Check results: `alpha_star`, `total_dropped`
5. If results bad -> adjust topology/demands -> repeat


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### references/REFERENCE.md

```markdown
# NetGraph DSL Reference

Complete reference documentation for the NetGraph scenario DSL.

> For a quick start guide and common patterns, see the main [SKILL.md](../SKILL.md).
> For complete working examples, see [EXAMPLES.md](EXAMPLES.md).

## Syntax Overview

### Template and Expansion Syntaxes

NetGraph DSL uses three distinct template syntaxes in different contexts:

| Syntax | Example | Where | Purpose |
|--------|---------|-------|---------|
| **Brackets** `[1-3]` | `dc[1-3]/rack[a,b]` | Group names, risk groups | Generate multiple entities |
| **Variables** `$var` | `pod${p}/leaf` | Links, rules, demands | Template expansion |
| **Format** `{n}` | `srv-{n}` | `template` | Node naming |

**Important**: These syntaxes are NOT interchangeable:

- `[1-3]` works in group names and risk groups (definitions and memberships), not components
- `${var}` requires `expand.vars` dict; works in link `source`/`target`, demand `source`/`target`, and node/link rules (via `expand` blocks)
- `{n}` is the only placeholder available in `template` (Python format syntax)

### Endpoint Naming Conventions

| Context | Fields | Terminology |
|---------|--------|-------------|
| Links, link_rules | `source`, `target` | Graph edge |
| Traffic demands, workflow steps | `source`, `target` | Max-flow |

### Expansion Controls in Traffic Demands

Traffic demands have three expansion-related fields:

| Field | Values | Default | Purpose |
|-------|--------|---------|---------|
| `mode` | `combine`, `pairwise` | `combine` | How source/target nodes pair |
| `group_mode` | `flatten`, `per_group`, `group_pairwise` | `flatten` | How grouped nodes expand |
| `expand.mode` | `cartesian`, `zip` | `cartesian` | How `expand.vars` combine |

See detailed sections below for each mechanism.

## Entity Creation Architecture

The DSL implements two fundamentally different selection patterns optimized for different use cases. Understanding these patterns is essential for effective scenario authoring.

### Two Selection Models

The DSL uses distinct selection strategies depending on the operation:

**1. Path-Based Node Selection** (link rules, traffic demands, workflow steps)

- Uses regex patterns on hierarchical node names
- Supports capture group-based grouping
- Supports attribute-based grouping (`group_by`)
- Supports attribute filtering (`match` conditions)
- Supports `active_only` filtering

**2. Condition-Based Entity Selection** (failure rules, membership rules, risk group generation)

- Works on nodes/links; failure + membership can also target risk_groups
- Optional regex `path` filter on entity names/IDs (no capture grouping)
- Attribute filtering via `match.conditions` for failure/membership; generate uses `group_by` only

These patterns share common primitives (condition evaluation, match specification) but serve different purposes and should not be confused.

### Link Creation Flow

Link definitions create links between nodes using path-based selection with optional filtering:

```mermaid
flowchart TD
    Start[Link Definition] --> VarExpand{Has expand.vars?}
    VarExpand -->|Yes| VarSubst[Variable Substitution]
    VarSubst --> PathFilter
    VarExpand -->|No| PathFilter[1. Path-Based Selection]
    PathFilter --> PathDesc[Select nodes via regex pattern<br/>Groups by capture groups]
    PathDesc --> MatchFilter{Has match conditions?}
    MatchFilter -->|Yes| AttrFilter[2. Attribute Filtering]
    MatchFilter -->|No| ActiveFilter
    AttrFilter --> AttrDesc[Filter by attribute conditions<br/>using logic and/or]
    AttrDesc --> ActiveFilter[3. Active/Excluded Filtering]
    ActiveFilter --> GroupBy{Has group_by?}
    GroupBy -->|Yes| Regroup[4. Re-group by Attribute]
    GroupBy -->|No| Pattern
    Regroup --> Pattern[5. Apply Pattern]
    Pattern --> PatternDesc[mesh or one_to_one<br/>Creates links between groups]
```

**Processing Steps:**

1. **Path Selection**: Regex pattern matches nodes by hierarchical name
   - Capture groups create initial grouping
   - If no path specified, selects all nodes
2. **Attribute Filtering**: Optional `match` conditions filter nodes
   - Uses `logic: "and"` or `"or"` (default: `"or"`)
   - Supports operators: `==`, `!=`, `<`, `>`, `contains`, `in`, etc.
3. **Active Filtering**: Filters disabled nodes based on context
   - Link default: `active_only=false` (creates links to disabled nodes)
4. **Attribute Grouping**: Optional `group_by` overrides regex capture grouping
5. **Pattern Application**: Creates links between selected node groups
   - `mesh`: Every source to every target
   - `one_to_one`: Pairwise with wrap-around

**Key Characteristics:**

- `default_active_only=False` (links are created to disabled nodes)
- `match.logic` defaults to `"or"` (inclusive matching)
- Supports variable expansion via `expand.vars`

### Traffic Demand Creation Flow

Traffic demands follow a similar pattern but with important differences:

```mermaid
flowchart TD
    Start[Traffic Demand Spec] --> VarExpand{Has expand.vars?}
    VarExpand -->|Yes| VarSubst[Variable Substitution<br/>Creates multiple demand specs]
    VarSubst --> Process
    VarExpand -->|No| Process[Process Single Demand]
    Process --> SrcSelect[1. Select Source Nodes]
    SrcSelect --> TgtSelect[2. Select Target Nodes]
    TgtSelect --> SrcDesc[Uses same path + match + group_by<br/>selection as links]
    SrcDesc --> Mode{Demand Mode?}
    Mode -->|pairwise| Pairwise[3a. Pairwise Expansion]
    Mode -->|combine| Combine[3b. Combine Expansion]
    Pairwise --> PairDesc[Create demand for each src-dst pair<br/>Volume distributed evenly<br/>No pseudo nodes]
    Combine --> CombDesc[Create pseudo-source and pseudo-target<br/>Single aggregated demand<br/>Augmentation edges connect real nodes]
```

**Key Differences from Links:**

1. **Active-only default**: `default_active_only=True` (only active nodes participate)
2. **Two selection phases**: Source nodes first, then target nodes (both use same selector logic)
3. **Expansion modes**:
   - **Pairwise**: Creates individual demands for each (source, target) pair
   - **Combine**: Creates pseudo nodes and a single aggregated demand
4. **Group modes**: Additional layer (`flatten`, `per_group`, `group_pairwise`) for handling grouped selections

**Processing Steps:**

1. Select source nodes using unified selector (path + match + group_by)
2. Select target nodes using unified selector
3. Apply mode-specific expansion:
   - **Pairwise**: Volume evenly distributed across all pairs
   - **Combine**: Single demand with pseudo nodes for aggregation

### Risk Group Creation Flow

Risk groups use the condition-based selection model:

```mermaid
flowchart TD
    Start[Risk Groups Definition] --> Three[Three Creation Methods]
    Three --> Direct[1. Direct Definition]
    Three --> Member[2. Membership Rules]
    Three --> Generate[3. Generate Blocks]

    Direct --> DirectDesc[Simply name the risk group<br/>Entities reference it explicitly]

    Member --> MemberScope[Specify scope<br/>node, link, or risk_group]
    MemberScope --> MemberCond[Optional path filter<br/>Match conditions (logic defaults to and)]
    MemberCond --> MemberExec[Scan entities of that scope<br/>Apply path + match]

    Generate --> GenScope[Specify scope<br/>node or link only]
    GenScope --> GenGroupBy[Specify group_by attribute]
    GenGroupBy --> GenExec[Collect unique values<br/>Create risk group for each value<br/>Add entities with that value]
```

**Creation Methods:**

1. **Direct Definition**: Explicitly name risk groups, entities reference them
2. **Membership Rules**: Auto-assign entities based on attribute matching
3. **Generate Blocks**: Auto-create risk groups from unique attribute values

**Key Characteristics:**

- **Optional path filter**: Regex `path` narrows entities before matching
- **Membership uses `match.conditions`**; generate uses `group_by` only
- **`match.logic` defaults to "and"** for membership (stricter matching)
- **Hierarchical support**: Risk groups can contain other risk groups as children

### Comparison Table

| Feature | Links | Traffic Demands | Risk Groups |
|---------|-------|----------------|-------------|
| Selection Type | Path-based | Path-based | Condition-based |
| Regex Patterns | Yes | Yes | Yes (path filter) |
| Capture Groups | Yes | Yes | No |
| `group_by` | Yes | Yes | Yes (generate only) |
| `match` Conditions | Yes | Yes | Yes (membership/failure) |
| `active_only` Default | False | True | N/A |
| `match.logic` Default | "or" | "or" | "and" (membership) |
| Variable Expansion | Yes | Yes | No |
| Entity Scope | Nodes only | Nodes only | Nodes, links, risk_groups (generate: node/link) |

### Shared Evaluation Primitives

All selection mechanisms share common evaluation primitives:

1. **Condition evaluation**: `evaluate_condition()` handles all operators
   - Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`
   - String/collection: `contains`, `not_contains`, `in`, `not_in`
   - Existence: `exists`, `not_exists`

2. **Condition combining**: `evaluate_conditions()` applies `"and"`/`"or"` logic

3. **Attribute flattening**: Unified access to entity attributes
   - `flatten_node_attrs()`: Merges node.attrs with top-level fields
   - `flatten_link_attrs()`: Merges link.attrs with top-level fields
   - `flatten_risk_group_attrs()`: Merges risk_group.attrs with top-level fields

4. **Dot-notation support**: `resolve_attr_path()` handles nested attributes
   - Example: `hardware.vendor` resolves to `attrs["hardware"]["vendor"]`

5. **Variable expansion**: `expand_templates()` handles `$var` and `${var}` substitution
   - Supports `cartesian` and `zip` expansion modes

### Context-Aware Defaults

The DSL uses context-aware defaults to optimize for common use cases:

| Context | Selection Type | Active Only | Match Logic | Rationale |
|---------|---------------|-------------|-------------|-----------|
| Links | Path-based | False | "or" | Create links to all nodes, including disabled |
| Demands | Path-based | True | "or" | Only route traffic through active nodes |
| Node Rules | Path-based | False | "or" | Modify all matching nodes |
| Workflow Steps | Path-based | True | "or" | Analyze only active topology |
| Membership Rules | Condition-based | N/A | "and" | Precise matching for risk assignment |
| Failure Rules | Condition-based | N/A | "or" | Inclusive matching for failure scenarios |
| Generate Blocks | Condition-based | N/A | N/A | No conditions, groups by values |

These defaults ensure intuitive behavior while remaining overridable when needed.

## Top-Level Keys

| Key | Required | Purpose |
|-----|----------|---------|
| `network` | Yes | Network topology (nodes, links) |
| `blueprints` | No | Reusable topology templates |
| `components` | No | Hardware component library |
| `risk_groups` | No | Failure correlation groups |
| `vars` | No | YAML anchors for value reuse |
| `demands` | No | Traffic demand definitions |
| `failures` | No | Failure simulation policies |
| `workflow` | No | Analysis execution steps |
| `seed` | No | Master seed for reproducible random operations |

## Network Metadata

```yaml
network:
  name: "My Network"       # Optional: network name
  version: "1.0"           # Optional: version string or number
  nodes: ...
  links: ...
```

## Network Topology

### Direct Node Definitions

```yaml
network:
  nodes:
    Seattle:
      disabled: false           # Optional: disable node
      risk_groups: ["RG1"]      # Optional: failure correlation
      attrs:                    # Optional: custom attributes
        coords: [47.6062, -122.3321]
        role: core
        hardware:
          component: "SpineRouter"
          count: 1
```

**Allowed node keys**: `disabled`, `attrs`, `risk_groups`, `count`, `template`, `blueprint`, `params`, `nodes`

### Direct Link Definitions

```yaml
network:
  links:
    - source: Seattle
      target: Portland
      capacity: 100.0          # Direct property
      cost: 10
      disabled: false
      risk_groups: ["RG_Seattle_Portland"]
      attrs:
        distance_km: 280
        media_type: fiber
        hardware:
          source:
            component: "800G-ZR+"
            count: 1
            exclusive: false   # Optional: unsharable usage (rounds up)
          target:
            component: "800G-ZR+"
            count: 1
      count: 2                 # Optional: parallel links
```

**Allowed link keys**: `source`, `target`, `pattern`, `count`, `capacity`, `cost`, `disabled`, `risk_groups`, `attrs`, `expand`

**Link hardware per-end fields**: `component`, `count`, `exclusive`

### Node Groups

Groups create multiple nodes from a template (distinguished by having `count` field):

```yaml
network:
  nodes:
    servers:
      count: 4
      template: "srv-{n}"
      disabled: false
      risk_groups: ["RG_Servers"]
      attrs:
        role: compute
```

Creates: `servers/srv-1`, `servers/srv-2`, `servers/srv-3`, `servers/srv-4`

**Group-specific keys**: `count`, `template`

### Nested Inline Nodes

Create hierarchical structures without blueprints using inline `nodes`:

```yaml
network:
  nodes:
    datacenter:
      attrs:
        region: west
      nodes:
        rack1:
          count: 2
          template: "srv-{n}"
          attrs:
            role: compute
        rack2:
          count: 2
          template: "srv-{n}"
```

Creates: `datacenter/rack1/srv-1`, `datacenter/rack1/srv-2`, `datacenter/rack2/srv-1`, `datacenter/rack2/srv-2`

**Key points:**

- Child nodes inherit parent `attrs`, `disabled`, and `risk_groups`
- Child-specific values override inherited ones
- Can be nested to any depth
- Useful for simple hierarchies without reusable blueprints

**Allowed keys for nested containers**: `nodes`, `attrs`, `disabled`, `risk_groups`

### Bracket Expansion

Create multiple similar groups using bracket notation:

```yaml
network:
  nodes:
    dc[1-3]/rack[a,b]:
      count: 4
      template: "srv-{n}"
```

**Expansion types**:

- Numeric ranges: `[1-4]` -> 1, 2, 3, 4
- Explicit lists: `[a,b,c]` -> a, b, c
- Mixed: `[1,3,5-7]` -> 1, 3, 5, 6, 7

Multiple brackets create Cartesian product.

**Scope**: Bracket expansion applies to:

- **Group names** under `network.nodes` and `blueprints.*.nodes`
- **Risk group names** in top-level `risk_groups` definitions (including children)
- **Risk group membership arrays** on nodes, links, and groups

It does NOT apply to: component names, direct node names without `count`, or other string fields.

**Risk group expansion examples**:

```yaml
# Definition expansion - creates DC1_Power, DC2_Power, DC3_Power
risk_groups:
  - name: "DC[1-3]_Power"

# Membership expansion - assigns to RG1, RG2, RG3
network:
  nodes:
    Server:
      risk_groups: ["RG[1-3]"]
```

### Path Patterns

Path patterns in selectors and rules are **regex patterns** matched against node names using `re.match()` (anchored at start).

**Key behaviors**:

- Paths are matched from the **start** of the node name (no implicit `.*` prefix)
- Node names are hierarchical: `group/subgroup/node1`
- Leading `/` is stripped before matching (has no functional effect)
- All paths are relative to the current scope

**Examples**:

| Pattern | Matches | Does NOT Match |
|---------|---------|----------------|
| `leaf` | `leaf/leaf1`, `leaf/leaf2` | `pod1/leaf/leaf1` |
| `pod1/leaf` | `pod1/leaf/leaf1` | `pod2/leaf/leaf1` |
| `.*leaf` | `leaf/leaf1`, `pod1/leaf/leaf1` | (matches any path containing "leaf") |
| `pod[12]/leaf` | `pod1/leaf/leaf1`, `pod2/leaf/leaf1` | `pod3/leaf/leaf1` |

**Path scoping**:

- **At top-level** (`network.links`): Parent path is empty, so patterns match against full node names. `/leaf` and `leaf` are equivalent.
- **In blueprints**: Paths are relative to instantiation path. If `pod1` uses a blueprint with `source: /leaf`, the pattern becomes `pod1/leaf`.

### Link Rules (with patterns)

```yaml
network:
  links:
    - source: /leaf
      target: /spine
      pattern: mesh
      capacity: 100
      cost: 1
      count: 1
```

**Patterns**:

- `mesh`: Full connectivity (every source to every target)
- `one_to_one`: Pairwise with modulo wrap. Sizes must have multiple factor (4-to-2 OK, 3-to-2 ERROR)

### Link Selectors

Filter nodes using attribute conditions:

```yaml
network:
  links:
    - source:
        path: "/datacenter"
        match:
          logic: and           # "and" or "or" (default varies by context)
          conditions:
            - attr: role
              op: "=="
              value: leaf
            - attr: tier
              op: ">="
              value: 2
      target:
        path: "/datacenter"
        match:
          conditions:
            - attr: role
              op: "=="
              value: spine
      pattern: mesh
      capacity: 100
```

**Condition operators**:

| Operator | Description |
|----------|-------------|
| `==` | Equal |
| `!=` | Not equal |
| `<`, `<=`, `>`, `>=` | Numeric comparison |
| `contains` | String/list contains value |
| `not_contains` | String/list does not contain |
| `in` | Value in list |
| `not_in` | Value not in list |
| `exists` | Attribute exists and is not None |
| `not_exists` | Attribute missing or None |

### Variable Expansion in Links

Use `$var` or `${var}` syntax in link `source`/`target` fields:

```yaml
network:
  links:
    - source: "plane${p}/rack"
      target: "spine${s}"
      expand:
        vars:
          p: [1, 2]
          s: [1, 2, 3]
        mode: cartesian
      pattern: mesh
      capacity: 100
```

**Expansion modes**:

- `cartesian` (default): All combinations (2 * 3 = 6 expansions)
- `zip`: Pair by index (lists must have equal length)

**Expansion limit**: Maximum 10,000 expansions per template. Exceeding this raises an error.

## Blueprints

Reusable topology templates:

```yaml
blueprints:
  clos_pod:
    nodes:
      leaf:
        count: 4
        template: "leaf-{n}"
        attrs:
          role: leaf
      spine:
        count: 2
        template: "spine-{n}"
        attrs:
          role: spine
    links:
      - source: /leaf
        target: /spine
        pattern: mesh
        capacity: 100
        cost: 1
```

### Blueprint Usage

```yaml
network:
  nodes:
    pod1:
      blueprint: clos_pod
      attrs:                    # Merged into all subgroup nodes
        location: datacenter_east
      params:                   # Override blueprint defaults
        leaf.count: 6
        spine.template: "core{n}"
        leaf.attrs.priority: high
```

Creates: `pod1/leaf/leaf1`, `pod1/spine/spine1`, etc.

**Parameter override syntax**: `<group>.<field>` or `<group>.attrs.<nested_key>`

### Blueprint Path Scoping

All paths are relative to the current scope. In blueprints, paths resolve relative to the instantiation path:

```yaml
blueprints:
  my_bp:
    links:
      - source: /leaf   # Becomes pod1/leaf when instantiated as pod1
        target: spine   # Also becomes pod1/spine (leading / is optional)
        pattern: mesh
```

**Note**: Leading `/` is stripped and has no functional effect. Both `/leaf` and `leaf` produce the same result. The `/` serves as a visual convention indicating "from scope root".

## Node and Link Rules

Modify nodes/links after initial creation:

```yaml
network:
  node_rules:
    - path: "^pod1/spine/.*$"  # Regex pattern
      disabled: true
      risk_groups: ["Maintenance"]
      attrs:
        maintenance_mode: active

  link_rules:
    - source: "^pod1/leaf/.*$"
      target: "^pod1/spine/.*$"
      bidirectional: true       # Match both directions (default: true)
      capacity: 200
      attrs:
        upgraded: true
```

### Node Rule Fields

- `path`: Regex pattern for matching node names (default: `".*"`)
- `match`: Optional attribute conditions to filter nodes (see below)
- `expand`: Optional variable expansion (see [Variable Expansion](#variable-expansion-in-rules))
- `disabled`, `risk_groups`, `attrs`: Properties to set on matching nodes

**Node rule with match conditions:**

```yaml
node_rules:
  - path: ".*"
    match:
      logic: and              # "and" or "or" (default: "or")
      conditions:
        - {attr: role, op: "==", value: compute}
        - {attr: tier, op: ">=", value: 2}
    disabled: true
```

### Link Rule Fields

- `source`, `target`: Regex patterns for matching link endpoints
- `bidirectional`: If `true` (default), matches both A→B and B→A directions
- `link_match`: Optional conditions to filter by the link's own attributes
- `expand`: Optional variable expansion (see [Variable Expansion](#variable-expansion-in-rules))
- Direct properties: `capacity`, `cost`, `disabled`, `risk_groups`, `attrs`

**Link rule with link_match:**

```yaml
link_rules:
  - source: "^pod1/.*$"
    target: "^pod2/.*$"
    link_match:
      logic: and
      conditions:
        - {attr: capacity, op: ">=", value: 400}
        - {attr: type, op: "==", value: fiber}
    cost: 99                  # Only high-capacity fiber links updated
```

### Variable Expansion in Rules

Use `expand` to apply a rule across multiple patterns:

```yaml
node_rules:
  - path: "${dc}_srv1"
    expand:
      vars:
        dc: [dc1, dc2, dc3]
      mode: cartesian
    attrs:
      tagged: true

link_rules:
  - source: "${src}_srv"
    target: "${tgt}_srv"
    expand:
      vars:
        src: [dc1, dc2]
        tgt: [dc2, dc3]
      mode: zip               # Pairs by index: dc1->dc2, dc2->dc3
    capacity: 200
```

**Processing order**:

1. Groups and direct nodes created
2. **Node rules applied**
3. Blueprint links and network links expanded
4. Direct links created
5. **Link rules applied**

## Components Library

Define hardware components for cost/power modeling:

```yaml
components:
  SpineRouter:
    component_type: chassis
    description: "64-port spine router"
    capex: 50000.0              # Cost per instance
    power_watts: 2500.0         # Typical power usage
    power_watts_max: 3000.0     # Peak power usage
    capacity: 64000.0           # Gbps
    ports: 64
    attrs:
      vendor: "Example Corp"
    children:
      LineCard400G:
        component_type: linecard
        capex: 8000.0
        power_watts: 400.0
        capacity: 12800.0
        ports: 32
        count: 4

  Optic400G:
    component_type: optic
    description: "400G pluggable optic"
    capex: 2500.0
    power_watts: 12.0
    capacity: 400.0
```

**Component fields**:

| Field | Description |
|-------|-------------|
| `component_type` | Category: `chassis`, `linecard`, `optic`, etc. |
| `description` | Human-readable description |
| `capex` | Cost per instance |
| `power_watts` | Typical power consumption (watts) |
| `power_watts_max` | Peak power consumption (watts) |
| `capacity` | Capacity in Gbps |
| `ports` | Number of ports |
| `count` | Instance count (for children) |
| `attrs` | Additional metadata |
| `children` | Nested child components |

**Usage in nodes/links**:

```yaml
network:
  nodes:
    spine1:
      attrs:
        hardware:
          component: "SpineRouter"
          count: 2
```

## Risk Groups

Risk groups define hierarchical failure correlation using three methods: direct definition, membership rules, and dynamic generation. Groups can model any failure domain: physical infrastructure, geographic regions, vendor dependencies, or custom correlation patterns.

### Direct Definition

```yaml
risk_groups:
  # Full object form with hierarchy
  - name: "Region_West"
    disabled: false             # Optional: disable on load
    attrs:
      type: geographic
    children:
      - name: "Site_Seattle"
        children:
          - name: "Cluster_SEA_01"
      - name: "Site_Portland"

  # String shorthand (equivalent to {name: "CustomGroup"})
  - "CustomGroup"
```

**Risk group fields**: `name` (required), `disabled`, `attrs`, `children`, `membership`, `generate`

### Membership Rules

Dynamically assign entities to risk groups based on attribute conditions:

```yaml
risk_groups:
  - name: HighCapacityLinks
    membership:
      scope: link              # Required: node, link, or risk_group
      match:
        logic: and             # "and" or "or" (default: "and")
        conditions:
          - attr: capacity
            op: ">="
            value: 1000

  - name: CoreNodes
    membership:
      scope: node
      match:
        logic: and
        conditions:
          - attr: role
            op: "=="
            value: core
          - attr: tier
            op: ">="
            value: 2
```

**Key points:**

- `scope`: Type of entities to match (`node`, `link`, or `risk_group`)
- `match.logic`: Defaults to `"and"` (stricter than other contexts)
- `match.conditions`: Uses same operators as selectors
- Entities are added to risk group during network build
- Supports dot-notation for nested attributes (e.g., `hardware.vendor`)

### Generate Blocks

Automatically create risk groups from unique attribute values:

```yaml
risk_groups:
  - generate:
      scope: link              # Required: node or link (not risk_group)
      group_by: connection_type # Attribute to group by (supports dot-notation)
      name: "LinkType_${value}"
      attrs:                   # Optional: static attrs for generated groups
        generated: true

  - generate:
      scope: node
      group_by: region
      name: "Region_${value}"
```

**Generate block fields:**

| Field | Required | Description |
|-------|----------|-------------|
| `scope` | Yes | `node` or `link` (cannot generate from risk_groups) |
| `group_by` | Yes | Attribute name (supports dot-notation) |
| `name` | Yes | Template with `${value}` placeholder |
| `path` | No | Regex to filter entities before grouping |
| `attrs` | No | Static attributes for generated groups |

**Using path to filter entities:**

```yaml
risk_groups:
  - generate:
      scope: node
      path: "^prod_.*"         # Only production nodes
      group_by: env
      name: "Env_${value}"
```

This creates risk groups only from nodes matching the path pattern. For example, if you have `prod_srv1`, `prod_srv2` (env: production), and `dev_srv1` (env: development), only `Env_production` is created because `dev_srv1` doesn't match `^prod_.*`.

**Key points:**

- Creates one risk group per unique attribute value
- Entities with null/missing attribute are skipped
- Generated groups are created during network build
- Use `path` to narrow scope before grouping

### Validation

Risk group references are validated at scenario load time:

**Undefined References:** All risk group names referenced by nodes and links must exist in the `risk_groups` section. Validation errors list affected entities and undefined groups:

```yaml
# This will fail validation
network:
  nodes:
    Router1:
      risk_groups: ["PowerZone_A"]  # References undefined risk group

risk_groups:
  - name: "PowerZone_B"  # Only PowerZone_B is defined
```

**Circular Hierarchies:** Parent-child relationships cannot form cycles:

```yaml
# This will fail validation
risk_groups:
  - name: "GroupA"
    children:
      - name: "GroupB"
        children:
          - name: "GroupA"  # Error: circular reference
```

## Traffic Demands

```yaml
demands:
  production:
    - source: "^dc1/.*"
      target: "^dc2/.*"
      volume: 1000
      mode: combine
      group_mode: flatten     # How to handle grouped nodes
      priority: 1
      flow_policy: SHORTEST_PATHS_ECMP
      attrs:
        service: web

    - source:
        path: "^datacenter/.*"
        match:
          conditions:
            - attr: role
              op: "=="
              value: leaf
      target:
        group_by: metro
      volume: 500
      mode: pairwise
      priority: 2
```

### Traffic Modes

| Mode | Description |
|------|-------------|
| `combine` | Single aggregate flow between source/target groups via pseudo nodes |
| `pairwise` | Individual flows between all source-target node pairs |

### Group Modes

When selectors use `group_by`, `group_mode` controls how grouped nodes produce demands:

| Group Mode | Description |
|------------|-------------|
| `flatten` | Flatten all groups into single source/target sets (default) |
| `per_group` | Create separate demands for each group |
| `group_pairwise` | Create pairwise demands between groups |

### Flow Policies

Flow policies can be specified as preset strings or inline configuration objects.

**Preset strings:**

| Preset | Description |
|--------|-------------|
| `SHORTEST_PATHS_ECMP` | IP/IGP routing with equal-split ECMP |
| `SHORTEST_PATHS_WCMP` | IP/IGP routing with weighted ECMP (by capacity) |
| `TE_WCMP_UNLIM` | MPLS-TE / SDN with unlimited tunnels |
| `TE_ECMP_16_LSP` | MPLS-TE with 16 ECMP LSPs per demand |
| `TE_ECMP_UP_TO_256_LSP` | MPLS-TE with up to 256 ECMP LSPs |

**Inline configuration objects:**

For advanced scenarios, you can specify a custom flow policy as an inline object:

```yaml
demands:
  custom:
    - source: A
      target: B
      volume: 100
      flow_policy:
        path_alg: SPF
        flow_placement: PROPORTIONAL
```

Inline objects are preserved and passed to the analysis engine. The supported fields depend on the underlying NetGraph-Core FlowPolicyConfig. Common fields include:

- `path_alg`: Path algorithm (`SPF`, etc.)
- `flow_placement`: Flow distribution strategy (`PROPORTIONAL`, `EQUAL_BALANCED`)

**Note:** Preset strings are recommended for most use cases. Inline objects provide flexibility for specialized routing behaviors but require knowledge of the underlying configuration options.

### Variable Expansion in Demands

```yaml
demands:
  inter_dc:
    - source: "^${src_dc}/.*"
      target: "^${dst_dc}/.*"
      volume: 100
      expand:
        vars:
          src_dc: [dc1, dc2]
          dst_dc: [dc2, dc3]
        mode: cartesian
```

## Failure Policies

Failure policies define how nodes, links, and risk groups fail during Monte Carlo simulations.

### Structure

```yaml
failures:
  policy_name:
    attrs: {}                        # Optional metadata
    expand_groups: false             # Expand to shared-risk entities
    expand_children: false           # Fail child risk groups recursively
    modes:                           # Required: weighted failure modes
      - weight: 1.0                  # Mode selection weight
        attrs: {}                    # Optional mode metadata
        rules: []                    # Rules applied when mode is selected
```

### Mode Selection

Exactly one mode is selected per failure iteration based on normalized weights:

```yaml
modes:
  - weight: 0.3   # 30% probability of selection
    rules: [...]
  - weight: 0.5   # 50% probability
    rules: [...]
  - weight: 0.2   # 20% probability
    rules: [...]
```

- Modes with zero or negative weight are never selected
- If all weights are non-positive, falls back to the first mode

### Rule Structure

```yaml
rules:
  - scope: link                # Required: node, link, or risk_group
    path: "^edge/.*"           # Optional regex on entity name/id
    match:
      conditions: []           # Optional: filter conditions
      logic: or                # Condition logic: and | or (default: or)
    mode: all                  # Selection: all | choice | random (default: all)
    probability: 1.0           # For random: [0.0, 1.0]
    count: 1                   # For choice: number to select
    weight_by: null            # For choice: attribute for weighted sampling
```

### Rule Modes

| Mode | Description | Parameters |
|------|-------------|------------|
| `all` | Select all matching entities | None |
| `choice` | Random sample from matches | `count`, optional `weight_by` |
| `random` | Each match selected with probability | `probability` in [0, 1] |

### Condition Logic

When multiple `match.conditions` are specified:

| Logic | Behavior |
|-------|----------|
| `or` | Entity matches if **any** condition is true |
| `and` | Entity matches if **all** conditions are true |

If `match` is omitted or `match.conditions` is empty, all entities of the given scope match.

**Context-specific defaults**:

| Context | Default `logic` | Rationale |
|---------|-----------------|-----------|
| Link `match` | `"or"` | Inclusive: match any condition |
| Demand `match` | `"or"` | Inclusive: match any condition |
| Membership rules | `"and"` | Precise: must match all conditions |
| Failure rules | `"or"` | Inclusive: match any condition |

### Weighted Sampling (choice mode)

When `weight_by` is set for `mode: choice`:

```yaml
- scope: link
  mode: choice
  count: 2
  weight_by: capacity   # Sample proportional to capacity attribute
```

- Uses Efraimidis-Spirakis algorithm for weighted sampling without replacement
- Entities with non-positive or missing weights are sampled uniformly after positive-weight items
- Falls back to uniform sampling if all weights are non-positive

### Risk Group Expansion

```yaml
expand_groups: true
```

When enabled, after initial failures are selected, expands to fail all entities that share a risk group with any failed entity (BFS traversal).

```yaml
expand_children: true
```

When enabled and a risk_group is marked as failed, recursively fails all child risk groups.

### Complete Example

```yaml
failures:
  weighted_modes:
    attrs:
      description: "Balanced failure simulation"
    expand_groups: true
    expand_children: false
    modes:
      # 30% chance: fail 1 risk group weighted by distance
      - weight: 0.3
        rules:
          - scope: risk_group
            mode: choice
            count: 1
            weight_by: distance_km

      # 50% chance: fail 1 non-core node weighted by capacity
      - weight: 0.5
        rules:
          - scope: node
            mode: choice
            count: 1
            match:
              logic: and
              conditions:
                - attr: role
                  op: "!="
                  value: core
            weight_by: attached_capacity_gbps

      # 20% chance: random link failures with 1% probability each
      - weight: 0.2
        rules:
          - scope: link
            mode: random
            probability: 0.01
```

### Entity Scopes

| Scope | Description |
|-------|-------------|
| `node` | Match against node attributes |
| `link` | Match against link attributes |
| `risk_group` | Match against risk group names/attributes |

## Workflow Steps

```yaml
workflow:
  - type: NetworkStats
    name: baseline_stats

  - type: MaximumSupportedDemand
    name: msd_baseline
    demand_set: production
    acceptance_rule: hard
    alpha_start: 1.0
    growth_factor: 2.0
    resolution: 0.05

  - type: TrafficMatrixPlacement
    name: tm_placement
    demand_set: production
    failure_policy: weighted_modes
    iterations: 1000
    parallelism: 8
    alpha_from_step: msd_baseline
    alpha_from_field: data.alpha_star

  - type: MaxFlow
    name: capacity_matrix
    source: "^(dc[1-3])$"
    target: "^(dc[1-3])$"
    mode: pairwise
    failure_policy: single_link
    iterations: 500

  - type: CostPower
    name: cost_analysis
    include_disabled: true
    aggregation_level: 2
```

### Step Types

| Type | Description |
|------|-------------|
| `BuildGraph` | Export graph to JSON (node-link format) |
| `NetworkStats` | Compute basic statistics (node/link counts, degrees) |
| `MaxFlow` | Monte Carlo capacity analysis between node groups |
| `TrafficMatrixPlacement` | Monte Carlo demand placement for a named matrix |
| `MaximumSupportedDemand` | Search for maximum supportable demand scaling (`alpha_star`) |
| `CostPower` | Cost and power estimation from components |

### BuildGraph Parameters

```yaml
- type: BuildGraph
  name: build_graph
  add_reverse: true   # Add reverse edges for bidirectional connectivity
```

### NetworkStats Parameters

```yaml
- type: NetworkStats
  name: stats
  include_disabled: false           # Include disabled nodes/links in stats
  excluded_nodes: []                # Optional: temporary node exclusions
  excluded_links: []                # Optional: temporary link exclusions
```

### MaxFlow Parameters

Baseline (no failures) is always run first as a reference. The `iterations` parameter specifies how many failure scenarios to run.

```yaml
- type: MaxFlow
  name: capacity_analysis
  source: "^servers/.*"
  target: "^storage/.*"
  mode: combine              # combine | pairwise
  failure_policy: policy_name
  iterations: 1000
  parallelism: auto          # or integer
  seed: 42                   # Optional: for reproducibility
  shortest_path: false       # Restrict to shortest paths only
  require_capacity: true     # Path selection considers capacity
  flow_placement: PROPORTIONAL  # PROPORTIONAL | EQUAL_BALANCED
  store_failure_patterns: false
  include_flow_details: false   # Cost distribution per flow
  include_min_cut: false        # Min-cut edge list per flow
```

### TrafficMatrixPlacement Parameters

Baseline (no failures) is always run first as a reference. The `iterations` parameter specifies how many failure scenarios to run.

```yaml
- type: TrafficMatrixPlacement
  name: tm_placement
  demand_set: default
  failure_policy: policy_name
  iterations: 100
  parallelism: auto
  placement_rounds: auto     # or integer
  seed: 42                   # Optional: for reproducibility
  include_flow_details: true
  include_used_edges: false
  store_failure_patterns: false
  # Alpha scaling options
  alpha: 1.0                 # Explicit scaling factor
  # Or reference another step's output:
  alpha_from_step: msd_step_name
  alpha_from_field: data.alpha_star
```

### MaximumSupportedDemand Parameters

```yaml
- type: MaximumSupportedDemand
  name: msd
  demand_set: default
  acceptance_rule: hard      # Currently only "hard" supported
  alpha_start: 1.0           # Starting alpha for search
  growth_factor: 2.0         # Growth factor for bracketing (> 1.0)
  alpha_min: 0.000001        # Minimum alpha bound
  alpha_max: 1000000000.0    # Maximum alpha bound
  resolution: 0.01           # Convergence resolution
  max_bracket_iters: 32
  max_bisect_iters: 32
  seeds_per_alpha: 1         # Seeds per alpha (majority vote)
  placement_rounds: auto
```

### CostPower Parameters

```yaml
- type: CostPower
  name: cost_power
  include_disabled: false    # Include disabled nodes/links
  aggregation_level: 2       # Hierarchy level for aggregation (split by /)
```

## Selector Reference

Selectors work across links, demands, and workflows.

### Selection Patterns

The DSL uses two distinct selection patterns:

**Path-based Node Selection** (links, demands, workflows):

- Works on node entities
- Uses regex patterns on hierarchical node names (`path`)
- Supports capture group-based grouping
- Supports attribute-based grouping (`group_by`)
- Supports attribute filtering (`match` conditions)
- Supports `active_only` filtering

**Condition-based Entity Selection** (failure rules, membership rules):

- Works on nodes, links, or risk_groups (`scope`)
- Uses only attribute-based filtering (`conditions`)
- No path/regex patterns (operates on all entities of specified type)
- See Failure Policies section for details

### String Pattern (Regex)

```yaml
source: "^dc1/spine/.*$"
```

Patterns use Python `re.match()`, anchored at start.

### Selector Object

```yaml
source:
  path: "^dc1/.*"           # Regex on node.name
  group_by: metro           # Group by attribute value
  match:                    # Filter by conditions
    logic: and
    conditions:
      - attr: role
        op: "=="
        value: spine
  active_only: true         # Exclude disabled nodes
```

At least one of `path`, `group_by`, or `match` must be specified.

### Context-Aware Defaults for active_only

The `active_only` field has context-dependent defaults:

| Context | Default | Rationale |
|---------|---------|-----------|
| `links` | `false` | Links to disabled nodes are created |
| `node_rules` | `false` | Rules can target disabled nodes |
| `demand` | `true` | Traffic only between active nodes |
| `workflow` | `true` | Analysis uses active nodes only |

### Capture Groups for Labeling

```yaml
# Single capture group
source: "^(dc[1-3])/.*"     # Groups: dc1, dc2, dc3

# Multiple capture groups join with |
source: "^(dc\\d+)/(spine|leaf)/.*"  # Groups: dc1|spine, dc1|leaf
```

## YAML Anchors

Use `vars` section for reusable values:

```yaml
vars:
  default_cap: &cap 10000
  base_attrs: &attrs {cost: 100, region: "dc1"}
  spine_config: &spine_cfg
    hardware:
      component: "SpineRouter"
      count: 1

network:
  nodes:
    spine1: {attrs: {<<: *attrs, <<: *spine_cfg, capacity: *cap}}
    spine2: {attrs: {<<: *attrs, <<: *spine_cfg, capacity: *cap, region: "dc2"}}
```

Anchors are resolved during YAML parsing, before schema validation.

```

### references/EXAMPLES.md

```markdown
# NetGraph DSL Examples

Complete working examples for common use cases.

> For quick patterns and pitfalls, see the main [SKILL.md](../SKILL.md).
> For detailed field reference, see [REFERENCE.md](REFERENCE.md).

## Example 1: Simple Data Center

A basic leaf-spine topology with traffic analysis.

```yaml
network:
  nodes:
    leaf:
      count: 4
      template: "leaf{n}"
      attrs:
        role: leaf
    spine:
      count: 2
      template: "spine{n}"
      attrs:
        role: spine
  links:
    - source: /leaf
      target: /spine
      pattern: mesh
      capacity: 100
      cost: 1

demands:
  default:
    - source: "^leaf/.*"
      target: "^leaf/.*"
      volume: 50
      mode: pairwise

failures:
  single_link:
    modes:
      - weight: 1.0
        rules:
          - scope: link
            mode: choice
            count: 1

workflow:
  - type: TrafficMatrixPlacement
    name: placement
    demand_set: default
    failure_policy: single_link
    iterations: 100
```

**Result**: 6 nodes (4 leaf + 2 spine), 8 links (4x2 mesh)

## Example 2: Multi-Pod with Blueprint

Two pods sharing a blueprint, connected via spine layer.

```yaml
blueprints:
  clos_pod:
    nodes:
      leaf:
        count: 4
        template: "leaf{n}"
        attrs:
          role: leaf
      spine:
        count: 2
        template: "spine{n}"
        attrs:
          role: spine
    links:
      - source: /leaf
        target: /spine
        pattern: mesh
        capacity: 100

network:
  nodes:
    pod[1-2]:
      blueprint: clos_pod

  links:
    - source:
        path: "pod1/spine"
        match:
          conditions:
            - attr: role
              op: "=="
              value: spine
      target:
        path: "pod2/spine"
      pattern: mesh
      capacity: 400
```

**Result**: 12 nodes (2 pods x 6 nodes), 20 links (16 internal + 4 inter-pod)

## Example 3: Backbone with Risk Groups

Wide-area network with shared-risk link groups.

```yaml
network:
  nodes:
    NewYork: {attrs: {site_type: core}}
    Chicago: {attrs: {site_type: core}}
    LosAngeles: {attrs: {site_type: core}}

  links:
    # Parallel diverse paths
    - source: NewYork
      target: Chicago
      capacity: 100
      cost: 10
      risk_groups: [RG_NY_CHI]
    - source: NewYork
      target: Chicago
      capacity: 100
      cost: 10
    # Single path
    - source: Chicago
      target: LosAngeles
      capacity: 100
      cost: 15
      risk_groups: [RG_CHI_LA]

risk_groups:
  - name: RG_NY_CHI
    attrs:
      corridor: NYC-Chicago
      distance_km: 1200
  - name: RG_CHI_LA
    attrs:
      corridor: Chicago-LA
      distance_km: 2800

failures:
  srlg_failure:
    modes:
      - weight: 1.0
        rules:
          - scope: risk_group
            mode: choice
            count: 1
```

**Result**: 3 nodes, 3 links, 2 risk groups

## Example 4: Variable Expansion at Scale

Large fabric using variable expansion.

```yaml
network:
  nodes:
    plane[1-4]/rack[1-8]:
      count: 48
      template: "server{n}"
      attrs:
        role: compute

    fabric/spine[1-4]:
      count: 1
      template: "spine"
      attrs:
        role: spine

  links:
    - source: "plane${p}/rack${r}"
      target: "fabric/spine${s}"
      expand:
        vars:
          p: [1, 2, 3, 4]
          r: [1, 2, 3, 4, 5, 6, 7, 8]
          s: [1, 2, 3, 4]
        mode: cartesian
      pattern: mesh
      capacity: 100
```

**Result**: 1540 nodes (4x8x48 compute + 4 spine), 6144 links

## Example 5: Full Mesh Topology

Simple 4-node full mesh for testing.

```yaml
seed: 42

network:
  nodes:
    N1: {}
    N2: {}
    N3: {}
    N4: {}

  links:
    - source: N1
      target: N2
      capacity: 2.0
      cost: 1.0
    - source: N1
      target: N3
      capacity: 1.0
      cost: 1.0
    - source: N1
      target: N4
      capacity: 2.0
      cost: 1.0
    - source: N2
      target: N3
      capacity: 2.0
      cost: 1.0
    - source: N2
      target: N4
      capacity: 1.0
      cost: 1.0
    - source: N3
      target: N4
      capacity: 2.0
      cost: 1.0

failures:
  single_link_failure:
    modes:
      - weight: 1.0
        rules:
          - scope: link
            mode: choice
            count: 1

demands:
  baseline:
    - source: "^N([1-4])$"
      target: "^N([1-4])$"
      volume: 12.0
      mode: pairwise

workflow:
  - type: MaximumSupportedDemand
    name: msd
    demand_set: baseline
    acceptance_rule: hard
    alpha_start: 1.0
    resolution: 0.05

  - type: MaxFlow
    name: capacity_matrix
    source: "^(N[1-4])$"
    target: "^(N[1-4])$"
    mode: pairwise
    failure_policy: single_link_failure
    iterations: 1000
    seed: 42
```

## Example 6: Attribute-Based Selectors

Using match conditions to filter nodes.

```yaml
network:
  nodes:
    servers:
      count: 4
      template: "srv{n}"
      attrs:
        role: compute
        rack: "rack-1"
    servers_b:
      count: 2
      template: "srvb{n}"
      attrs:
        role: compute
        rack: "rack-9"
    switches:
      count: 2
      template: "sw{n}"
      attrs:
        tier: spine

  links:
    - source:
        path: "/servers"
        match:
          logic: and
          conditions:
            - attr: role
              op: "=="
              value: compute
            - attr: rack
              op: "!="
              value: "rack-9"
      target:
        path: "/switches"
        match:
          conditions:
            - attr: tier
              op: "=="
              value: spine
      pattern: mesh
      capacity: 10
      cost: 1
```

**Result**: 8 nodes, 8 links (only rack-1 servers connect to switches)

## Example 7: Blueprint with Parameter Overrides

Customizing blueprint instances.

```yaml
blueprints:
  bp1:
    nodes:
      leaf:
        count: 1
        attrs:
          some_field:
            nested_key: 111

network:
  nodes:
    Main:
      blueprint: bp1
      params:
        leaf.attrs.some_field.nested_key: 999
```

**Result**: Node `Main/leaf/leaf1` has `attrs.some_field.nested_key = 999`

## Example 8: Node and Link Rules

Modifying topology after creation.

```yaml
blueprints:
  test_bp:
    nodes:
      switches:
        count: 3
        template: "switch{n}"

network:
  nodes:
    group1:
      count: 2
      template: "node{n}"
    group2:
      count: 2
      template: "node{n}"
    my_clos1:
      blueprint: test_bp

  links:
    - source: /group1
      target: /group2
      pattern: mesh
      capacity: 100
      cost: 10

  node_rules:
    - path: "^my_clos1/switches/switch(1|3)$"
      disabled: true
      attrs:
        maintenance_mode: active
        hw_type: newer_model

  link_rules:
    - source: "^group1/node1$"
      target: "^group2/node1$"
      capacity: 200
      cost: 5
```

**Result**: Switches 1 and 3 disabled, specific link upgraded to 200 capacity

## Example 9: Complete Traffic Analysis

Full workflow with MSD and placement analysis.

```yaml
seed: 42

blueprints:
  Clos_L16_S4:
    nodes:
      spine:
        count: 4
        template: spine{n}
        attrs:
          role: spine
      leaf:
        count: 16
        template: leaf{n}
        attrs:
          role: leaf
    links:
      - source: /leaf
        target: /spine
        pattern: mesh
        capacity: 3200
        cost: 1

network:
  nodes:
    metro1/pop[1-2]:
      blueprint: Clos_L16_S4
      attrs:
        metro_name: new-york
        node_type: pop

demands:
  baseline:
    - source: "^metro1/pop1/.*"
      target: "^metro1/pop2/.*"
      volume: 15000.0
      mode: pairwise
      flow_policy: TE_WCMP_UNLIM

failures:
  single_link:
    modes:
      - weight: 1.0
        rules:
          - scope: link
            mode: choice
            count: 1

workflow:
  - type: NetworkStats
    name: network_statistics

  - type: MaximumSupportedDemand
    name: msd_baseline
    demand_set: baseline
    acceptance_rule: hard
    alpha_start: 1.0
    growth_factor: 2.0
    resolution: 0.05

  - type: TrafficMatrixPlacement
    name: tm_placement
    seed: 42
    demand_set: baseline
    failure_policy: single_link
    iterations: 1000
    parallelism: 7
    include_flow_details: true
    alpha_from_step: msd_baseline
    alpha_from_field: data.alpha_star
```

## Example 10: Group-By Selectors

Grouping nodes by attribute for demand generation.

```yaml
network:
  nodes:
    dc1_srv1: {attrs: {dc: dc1, role: server}}
    dc1_srv2: {attrs: {dc: dc1, role: server}}
    dc2_srv1: {attrs: {dc: dc2, role: server}}
    dc2_srv2: {attrs: {dc: dc2, role: server}}
  links:
    - source: dc1_srv1
      target: dc2_srv1
      capacity: 100
    - source: dc1_srv2
      target: dc2_srv2
      capacity: 100

demands:
  inter_dc:
    - source:
        group_by: dc
      target:
        group_by: dc
      volume: 100
      mode: pairwise
```

**Result**: Traffic flows grouped by datacenter attribute

## Example 11: Advanced Failure Policies

Multiple weighted failure modes with conditions and weighted sampling.

```yaml
network:
  nodes:
    core1: {attrs: {role: core, capacity_gbps: 1000}}
    core2: {attrs: {role: core, capacity_gbps: 1000}}
    edge1: {attrs: {role: edge, capacity_gbps: 400, region: west}}
    edge2: {attrs: {role: edge, capacity_gbps: 400, region: east}}
    edge3: {attrs: {role: edge, capacity_gbps: 200, region: west}}
  links:
    - source: core1
      target: core2
      capacity: 1000
      risk_groups: [RG_core]
    - source: core1
      target: edge1
      capacity: 400
      risk_groups: [RG_west]
    - source: core1
      target: edge3
      capacity: 200
      risk_groups: [RG_west]
    - source: core2
      target: edge2
      capacity: 400
      risk_groups: [RG_east]

risk_groups:
  - name: RG_core
    attrs: {tier: core, distance_km: 50}
  - name: RG_west
    attrs: {tier: edge, distance_km: 500}
  - name: RG_east
    attrs: {tier: edge, distance_km: 800}

failures:
  mixed_failures:
    expand_groups: true          # Expand to shared-risk entities
    expand_children: false
    modes:
      # 40% chance: fail 1 edge node weighted by capacity
      - weight: 0.4
        attrs: {scenario: edge_failure}
        rules:
          - scope: node
            mode: choice
            count: 1
            match:
              logic: and
              conditions:
                - attr: role
                  op: "=="
                  value: edge
            weight_by: capacity_gbps

      # 35% chance: fail 1 risk group weighted by distance
      - weight: 0.35
        attrs: {scenario: srlg_failure}
        rules:
          - scope: risk_group
            mode: choice
            count: 1
            weight_by: distance_km

      # 15% chance: fail all west-region nodes
      - weight: 0.15
        attrs: {scenario: regional_outage}
        rules:
          - scope: node
            mode: all
            match:
              conditions:
                - attr: region
                  op: "=="
                  value: west

      # 10% chance: random link failures (5% each)
      - weight: 0.1
        attrs: {scenario: random_link}
        rules:
          - scope: link
            mode: random
            probability: 0.05

workflow:
  - type: MaxFlow
    name: failure_analysis
    source: "^(edge[1-3])$"
    target: "^(edge[1-3])$"
    mode: pairwise
    failure_policy: mixed_failures
    iterations: 1000
    seed: 42
```

**Result**: 5 nodes, 4 links, 3 risk groups, failure policy with 4 weighted modes

## Example 12: Hardware Components and Cost Analysis

Using the components library for cost/power modeling.

```yaml
components:
  SpineRouter:
    component_type: chassis
    description: "64-port spine switch"
    capex: 55000.0
    power_watts: 2000.0
    power_watts_max: 3000.0
    capacity: 102400.0
    ports: 64

  LeafRouter:
    component_type: chassis
    description: "48-port leaf switch"
    capex: 25000.0
    power_watts: 800.0
    power_watts_max: 1200.0
    capacity: 38400.0
    ports: 48

  Optic400G:
    component_type: optic
    description: "400G DR4 pluggable"
    capex: 3000.0
    power_watts: 16.0
    capacity: 400.0

network:
  name: "datacenter-fabric"
  version: "2.0"

  nodes:
    spine:
      count: 2
      template: "spine{n}"
      attrs:
        hardware:
          component: SpineRouter
          count: 1
    leaf:
      count: 4
      template: "leaf{n}"
      attrs:
        hardware:
          component: LeafRouter
          count: 1

  links:
    - source: /leaf
      target: /spine
      pattern: mesh
      count: 2                    # 2 parallel links per pair
      capacity: 800
      cost: 1
      attrs:
        hardware:
          source:
            component: Optic400G
            count: 2
          target:
            component: Optic400G
            count: 2
            exclusive: true          # Dedicated optics (rounds up count)

workflow:
  - type: NetworkStats
    name: stats

  - type: CostPower
    name: cost_analysis
    include_disabled: false
    aggregation_level: 1               # Aggregate by top-level group
```

**Result**: 6 nodes, 16 links (4x2x2), component-based cost/power analysis

## Example 13: YAML Anchors for Reuse

Using `vars` section for DRY configuration.

```yaml
vars:
  default_link: &link_cfg
    capacity: 100
    cost: 1
  spine_attrs: &spine_attrs
    role: spine
    tier: 2
  leaf_attrs: &leaf_attrs
    role: leaf
    tier: 1

network:
  nodes:
    spine:
      count: 2
      template: "spine{n}"
      attrs:
        <<: *spine_attrs             # Merge anchor
        region: east

    leaf:
      count: 4
      template: "leaf{n}"
      attrs:
        <<: *leaf_attrs
        region: east

  links:
    - source: /leaf
      target: /spine
      pattern: mesh
      <<: *link_cfg                # Reuse link config
      attrs:
        link_type: fabric
```

**Result**: Anchors resolved during YAML parsing; cleaner, less repetitive config

## Example 14: One-to-One Pattern and Zip Expansion

Demonstrating pairwise connectivity patterns.

```yaml
network:
  nodes:
    # 4 servers, 2 switches - compatible for one_to_one (4 is multiple of 2)
    server[1-4]:
      count: 1
      template: "srv"
    switch[1-2]:
      count: 1
      template: "sw"

  links:
    # one_to_one: server1->switch1, server2->switch2, server3->switch1, server4->switch2
    - source: /server
      target: /switch
      pattern: one_to_one
      capacity: 100

    # zip expansion: pairs variables by index (equal-length lists required)
    - source: "server${idx}"
      target: "switch${sw}"
      expand:
        vars:
          idx: [1, 2]
          sw: [1, 2]
        mode: zip            # server1->switch1, server2->switch2
      pattern: one_to_one
      capacity: 50
      cost: 2
```

**Result**: Demonstrates one_to_one modulo wrap and zip expansion mode

## Example 15: Traffic Demands with Variable Expansion and Group Modes

Advanced demand configuration.

```yaml
network:
  nodes:
    dc1_leaf1: {attrs: {dc: dc1, role: leaf}}
    dc1_leaf2: {attrs: {dc: dc1, role: leaf}}
    dc2_leaf1: {attrs: {dc: dc2, role: leaf}}
    dc2_leaf2: {attrs: {dc: dc2, role: leaf}}
    dc3_leaf1: {attrs: {dc: dc3, role: leaf}}
  links:
    - {source: dc1_leaf1, target: dc2_leaf1, capacity: 100}
    - {source: dc1_leaf2, target: dc2_leaf2, capacity: 100}
    - {source: dc2_leaf1, target: dc3_leaf1, capacity: 100}

demands:
  # Variable expansion in demands
  inter_dc:
    - source: "^${src}/.*"
      target: "^${dst}/.*"
      volume: 50
      expand:
        vars:
          src: [dc1, dc2]
          dst: [dc2, dc3]
        mode: zip            # dc1->dc2, dc2->dc3

  # Group modes with group_by
  grouped:
    - source:
        group_by: dc
      target:
        group_by: dc
      volume: 100
      mode: pairwise
      group_mode: per_group          # Separate demand per group pair
      priority: 1
      flow_policy: SHORTEST_PATHS_WCMP
```

**Result**: Shows variable expansion in demands, group_mode, and priority

## Example 16: Hierarchical Risk Groups

Nested risk group structure with children.

```yaml
network:
  nodes:
    rack1_srv1: {risk_groups: [Rack1_Card1]}
    rack1_srv2: {risk_groups: [Rack1_Card1]}
    rack1_srv3: {risk_groups: [Rack1_Card2]}
    rack2_srv1: {risk_groups: [Rack2]}
  links:
    - {source: rack1_srv1, target: rack2_srv1, capacity: 100}
    - {source: rack1_srv2, target: rack2_srv1, capacity: 100}
    - {source: rack1_srv3, target: rack2_srv1, capacity: 100}

risk_groups:
  - name: Rack1
    attrs: {location: "DC1-Row1"}
    children:
      - name: Rack1_Card1
        attrs: {slot: 1}
      - name: Rack1_Card2
        attrs: {slot: 2}
  - name: Rack2
    disabled: false
    attrs: {location: "DC1-Row2"}

failures:
  hierarchical:
    expand_groups: true
    expand_children: true   # Failing Rack1 also fails Card1, Card2
    modes:
      - weight: 1.0
        rules:
          - scope: risk_group
            mode: choice
            count: 1
            match:
              conditions:
                - attr: location
                  op: contains    # String contains
                  value: "DC1"
```

**Result**: Hierarchical risk groups with recursive child failure expansion

## Example 17: Risk Group Membership Rules

Dynamically assign entities based on attributes.

```yaml
network:
  nodes:
    core1: {attrs: {role: core, tier: 3, datacenter: dc1}}
    core2: {attrs: {role: core, tier: 3, datacenter: dc2}}
    edge1: {attrs: {role: edge, tier: 1, datacenter: dc1}}
    edge2: {attrs: {role: edge, tier: 1, datacenter: dc2}}
  links:
    - source: core1
      target: core2
      capacity: 1000
      attrs:
        route_type: backbone
        path_id: primary
    - source: core1
      target: edge1
      capacity: 400

risk_groups:
  # Assign all core tier-3 nodes
  - name: CoreTier3
    membership:
      scope: node
      match:
        logic: and              # Must match ALL conditions
        conditions:
          - attr: role
            op: "=="
            value: core
          - attr: tier
            op: "=="
            value: 3

  # Assign links by route type
  - name: BackboneLinks
    membership:
      scope: link
      match:
        logic: and
        conditions:
          - attr: route_type      # Dot-notation for nested attrs
            op: "=="
            value: backbone

  # String shorthand for simple groups
  - "ManualGroup1"
```

**Result**: Nodes and links automatically assigned to risk groups based on attributes

## Example 18: Generated Risk Groups

Create risk groups from unique attribute values.

```yaml
network:
  nodes:
    srv1: {attrs: {datacenter: dc1, rack: r1}}
    srv2: {attrs: {datacenter: dc1, rack: r2}}
    srv3: {attrs: {datacenter: dc2, rack: r1}}
  links:
    - source: srv1
      target: srv2
      capacity: 100
      attrs:
        connection_type: intra_dc
    - source: srv2
      target: srv3
      capacity: 100
      attrs:
        connection_type: inter_dc

risk_groups:
  # Generate risk group per datacenter (from nodes)
  - generate:
      scope: node
      group_by: datacenter
      name: "DC_${value}"
      attrs:
        generated: true
        type: location

  # Generate risk group per rack (from nodes)
  - generate:
      scope: node
      group_by: rack
      name: "Rack_${value}"

  # Generate risk group per connection type (from links)
  - generate:
      scope: link
      group_by: connection_type
      name: "Links_${value}"
```

**Result**: Creates 6 risk groups:

- `DC_dc1` (srv1, srv2)
- `DC_dc2` (srv3)
- `Rack_r1` (srv1, srv3)
- `Rack_r2` (srv2)
- `Links_intra_dc` (link srv1→srv2)
- `Links_inter_dc` (link srv2→srv3)

## Example 19: Additional Selector Operators

Demonstrating all condition operators.

```yaml
network:
  nodes:
    srv1: {attrs: {tier: 1, tags: [prod, web], region: null}}
    srv2: {attrs: {tier: 2, tags: [prod, db], region: east}}
    srv3: {attrs: {tier: 3, tags: [dev], region: west}}
    srv4: {attrs: {tier: 2}}
  links:
    - {source: srv1, target: srv2, capacity: 100}
    - {source: srv2, target: srv3, capacity: 100}
    - {source: srv3, target: srv4, capacity: 100}

demands:
  filtered:
    # Tier comparison operators
    - source:
        match:
          conditions:
            - attr: tier
              op: ">="
              value: 2
      target:
        match:
          conditions:
            - attr: tier
              op: "<"
              value: 3
      volume: 50
      mode: pairwise

    # List membership operators
    - source:
        match:
          conditions:
            - attr: region
              op: in
              value: [east, west]
      target:
        match:
          conditions:
            - attr: tags
              op: contains     # List contains value
              value: prod
      volume: 25
      mode: combine

    # Existence operators
    - source:
        match:
          conditions:
            - attr: region
              op: exists       # Attribute exists and not null
      target:
        match:
          conditions:
            - attr: region
              op: not_exists   # Attribute missing or null
      volume: 10
      mode: pairwise
```

**Result**: Demonstrates `>=`, `<`, `in`, `contains`, `exists`, `not_exists` operators

## Example 20: link_match and Rule Expansion

Using `link_match` to filter link rules by the link's own attributes, and `expand` for variable-based rule application.

```yaml
network:
  nodes:
    dc1_srv: {}
    dc2_srv: {}
    dc3_srv: {}
  links:
    - {source: dc1_srv, target: dc2_srv, capacity: 100, cost: 1, attrs: {type: fiber}}
    - {source: dc1_srv, target: dc2_srv, capacity: 500, cost: 1, attrs: {type: fiber}}
    - {source: dc2_srv, target: dc3_srv, capacity: 500, cost: 1, attrs: {type: copper}}

  # Update only high-capacity fiber links
  link_rules:
    - source: ".*"
      target: ".*"
      link_match:
        logic: and
        conditions:
          - {attr: capacity, op: ">=", value: 400}
          - {attr: type, op: "==", value: fiber}
      cost: 99
      attrs:
        priority: high

  # Apply node rules using variable expansion
  node_rules:
    - path: "${dc}_srv"
      expand:
        vars:
          dc: [dc1, dc2]
        mode: cartesian
      attrs:
        tagged: true
```

**Result**: Only the 500-capacity fiber link (dc1_srv -> dc2_srv) gets cost 99. Nodes dc1_srv and dc2_srv are tagged.

## Example 21: Nested Inline Nodes (No Blueprint)

Creating hierarchical topology structure without using blueprints.

```yaml
network:
  nodes:
    datacenter:
      attrs:
        region: west
        tier: 1
      nodes:
        rack1:
          attrs:
            rack_id: 1
          nodes:
            tor:
              count: 1
              template: "sw{n}"
              attrs:
                role: switch
            servers:
              count: 4
              template: "srv{n}"
              attrs:
                role: compute
        rack2:
          attrs:
            rack_id: 2
          nodes:
            tor:
              count: 1
              template: "sw{n}"
              attrs:
                role: switch
            servers:
              count: 4
              template: "srv{n}"
              attrs:
                role: compute

  links:
    # Connect servers to their TOR switch in each rack
    - source:
        path: "datacenter/rack1/servers"
      target:
        path: "datacenter/rack1/tor"
      pattern: mesh
      capacity: 25
    - source:
        path: "datacenter/rack2/servers"
      target:
        path: "datacenter/rack2/tor"
      pattern: mesh
      capacity: 25
    # Connect TOR switches
    - source: datacenter/rack1/tor/sw1
      target: datacenter/rack2/tor/sw1
      capacity: 100
```

**Result**: Creates 10 nodes (2 switches + 8 servers) in a two-rack hierarchy. All nodes inherit `region: west` and `tier: 1` from the datacenter parent. Each rack's nodes get the appropriate `rack_id`.

## Example 22: path Filter in Generate Blocks

Using `path` to narrow entities before generating risk groups.

```yaml
network:
  nodes:
    prod_web1: {attrs: {env: production, service: web}}
    prod_web2: {attrs: {env: production, service: web}}
    prod_db1: {attrs: {env: production, service: database}}
    dev_web1: {attrs: {env: development, service: web}}
    dev_db1: {attrs: {env: development, service: database}}
  links:
    - {source: prod_web1, target: prod_db1, capacity: 100, attrs: {link_type: internal}}
    - {source: prod_web2, target: prod_db1, capacity: 100, attrs: {link_type: internal}}
    - {source: dev_web1, target: dev_db1, capacity: 50, attrs: {link_type: internal}}

risk_groups:
  # Generate env-based risk groups only for production nodes
  - generate:
      scope: node
      path: "^prod_.*"
      group_by: env
      name: "Env_${value}"
      attrs:
        generated: true
        critical: true

  # Generate service-based risk groups for all nodes
  - generate:
      scope: node
      group_by: service
      name: "Service_${value}"

  # Generate link risk groups only for production links
  - generate:
      scope: link
      path: ".*prod.*"
      group_by: link_type
      name: "ProdLinks_${value}"

demands:
  baseline:
    - source: "^prod_web.*"
      target: "^prod_db.*"
      volume: 50
      mode: pairwise
      flow_policy: SHORTEST_PATHS_ECMP

failures:
  production_failure:
    expand_groups: true
    modes:
      - weight: 1.0
        rules:
          - scope: risk_group
            path: "^Env_.*"
            mode: choice
            count: 1
```

**Result**: Creates the following risk groups:

- `Env_production` (only production nodes due to path filter)
- `Service_web` (prod_web1, prod_web2, dev_web1)
- `Service_database` (prod_db1, dev_db1)
- `ProdLinks_internal` (only production links due to path filter)

Note: `Env_development` is NOT created because dev nodes don't match `^prod_.*`.

```