# Asset Inventory and Data Export

With your factory scene organized, let’s explore it **programmatically** using Python scripting. This section teaches you to traverse the USD stage, identify matching prims, and export their attributes to CSV for reporting. You will add visual navigation waypoints in the next lesson.

## Understanding Stage Traversal

**Stage traversal** walks a USD scene hierarchy to query prims. The `stage.Traverse()` calls used here visit active, loaded, defined, non-abstract prims. They do not descend into scenegraph instances or include unloaded payload contents. The resulting counts describe that traversal, not every object in the source files.

#### TIP
For a deep dive into Stage traversal and more OpenUSD topics, check out our [learning path](https://docs.nvidia.com/learn-openusd/latest/beyond-basics/stage-traversal.html).

## Why Stage Traversal Matters for Digital Twins

As your factory digital twin grows in complexity, manually tracking assets becomes impractical. Consider these scenarios:

**Facility Management Needs**

- “How many safety devices do we have in the facility?”
- “What types of machines are installed in Production Line A?”
- “Which assets need maintenance based on their installation dates?”
- “What’s the total count of each asset type for procurement planning?”

In real facility management, you need to:

- **Generate asset inventories** for maintenance planning
- **Create compliance reports** showing all safety equipment locations
- **Track component installations** across production lines
- **Export data for ERP integration** and facility management systems

Manual clicking through hundreds of objects isn’t practical—scripting automates this essential workflow.

### How USD Stage Traversal Works

USD traversal operates by walking through prims in **depth-first order**

```default
/Factory (Root)
├── /Factory/ProductionLine_A (visited first)
│   ├── /Factory/ProductionLine_A/Machine_001 (visited second)
│   └── /Factory/ProductionLine_A/Machine_002 (visited third)
└── /Factory/ProductionLine_B (visited fourth)
    └── /Factory/ProductionLine_B/Machine_003 (visited fifth)
```

This visits each prim included by the traversal once in depth-first order. See the [OpenUSD stage traversal API](https://openusd.org/release/api/class_usd_stage.html) for the default predicate and instance behavior.

## Python Scripting in Omniverse

**Accessing the Script Editor**

1. Navigate to **Developer > Script Editor**  to open the Script Editor.

### Basic Stage Traversal: Listing Traversed Prims

Let’s start with a fundamental script that traverses your factory stage and lists the paths included by the default traversal.

#### Script 1: Default Stage Traversal

#### WARNING
Depending on the size of your scene this may take some time to complete.

**Copy and paste this script into the Script Editor**

```python
# Import necessary USD modules
from pxr import Usd
import omni.usd

# Get the current stage (your opened factory scene)
stage = omni.usd.get_context().get_stage()

# Check if stage is valid
if not stage:
    print("No stage currently loaded!")
else:
    print("=== Factory Scene Traversal ===")
    print("Traversing matching prims in depth-first order:\n")
    
    # Traverse all active, loaded, defined prims
    prim_count = 0
    for prim in stage.Traverse():
        print(f"Path: {prim.GetPath()}")
        print(f"Type: {prim.GetTypeName()}")
        print(f"Kind: {prim.GetMetadata('kind') or ''}")
        print("-" * 40)
        prim_count += 1
    
    print(f"\nTotal prims traversed: {prim_count}")
```

**Execute the script** and observe:

![Script Editor displaying stage-traversal code and output listing prim paths, types and kinds.](optimization-data/images/image80.png)
- The **hierarchical structure** of your factory scene
- **Prim types** (Xform, Mesh, Material, etc.)
- **USD Kind metadata** (Component, Assembly, Group)
- **Total count** of prims included by this traversal

##### Understanding the Output

Your results will show the organized structure of your digital twin:

| Output Element   | What It Tells You                          | Why It Matters                    |
|------------------|--------------------------------------------|-----------------------------------|
| **Path**         | Hierarchical location in scene             | Navigation and organization       |
| **Type**         | USD primitive type (Mesh, Xform, etc.)     | Technical structure understanding |
| **Kind**         | Asset classification (Component, Assembly) | Logical grouping and filtering    |

### Targeted Asset Identification

Rather than listing everything, let’s create more focused scripts that identify specific types of assets relevant to facility management.

#### Script 2: Finding Manufacturing Equipment

**This script identifies machines and equipment specifically**

#### NOTE
This script lists candidates whose names contain equipment keywords or whose kind is `component`. Keywords include **machine**, **robot**, **conveyor**, **press**, **router**, and **feeder**. A component can also be a prop, and a matching name can belong to an internal part. Review the paths and count only the roots of the equipment assets you intend to inventory; this heuristic is not a verified equipment count.

```python
from pxr import Usd, UsdGeom
import omni.usd

# Get current stage
stage = omni.usd.get_context().get_stage()

if stage:
    print("=== Manufacturing Equipment Inventory ===\n")
    
    machines = []
    
    for prim in stage.Traverse():
        # Look for prims with "machine" in the name or specific kinds
        prim_name = prim.GetName().lower()
        prim_kind = prim.GetMetadata("kind") or ""
        
        # Identify potential manufacturing equipment
        if (any(keyword in prim_name for keyword in ['machine', 'robot', 'conveyor', 'press', 'router', 'feeder']) 
            or prim_kind == 'component'):
            
            # Get additional information
            transform = UsdGeom.Xformable(prim)
            
            machine_data = {
                'name': prim.GetName(),
                'path': str(prim.GetPath()),
                'type': prim.GetTypeName(),
                'kind': prim_kind
            }
            
            machines.append(machine_data)
            
            print(f"Equipment: {machine_data['name']}")
            print(f"Location: {machine_data['path']}")
            print(f"Type: {machine_data['type']}")
            print(f"Classification: {machine_data['kind']}")
            print("-" * 50)
    
    print(f"\nTotal manufacturing equipment found: {len(machines)}")
```

#### Script 3: Props and Safety Equipment Inventory

**This script focuses on props, safety equipment, and environmental objects**

#### NOTE
This script is searching prims similar to the previous one, but it will categorize them by type.<br />
\\\\
Such as: **Safety Equipment**, **Storage/Transport**, **Lighting**

```python
from pxr import Usd
import omni.usd

# Get current stage
stage = omni.usd.get_context().get_stage()

if stage:
    print("=== Props and Safety Equipment Inventory ===\n")
    
    props = []
    safety_equipment = []
    
    for prim in stage.Traverse():
        prim_name = prim.GetName().lower()
        prim_path = str(prim.GetPath()).lower()
        
        # Categorize based on name patterns
        if any(keyword in prim_name for keyword in ['cone', 'barrier', 'sign', 'safety', 'warning']):
            safety_equipment.append({
                'name': prim.GetName(),
                'path': str(prim.GetPath()),
                'category': 'Safety Equipment'
            })
            
        elif any(keyword in prim_name for keyword in ['pallet', 'container', 'box', 'rack', 'shelf']):
            props.append({
                'name': prim.GetName(),
                'path': str(prim.GetPath()),
                'category': 'Storage/Transport'
            })
            
        elif any(keyword in prim_name for keyword in ['light', 'lamp', 'fixture']):
            props.append({
                'name': prim.GetName(),
                'path': str(prim.GetPath()),
                'category': 'Lighting'
            })
    
    # Display results
    print("SAFETY EQUIPMENT:")
    for item in safety_equipment:
        print(f"  {item['name']} at {item['path']}")
    
    print(f"\nPROPS BY CATEGORY:")
    for item in props:
        print(f"  {item['category']}: {item['name']} at {item['path']}")
    
    print(f"\nSummary:")
    print(f"Safety Equipment: {len(safety_equipment)}")
    print(f"Props: {len(props)}")
```

### CSV Export: Asset Data Export

#### Export Matching Scene Attributes to CSV

Keep prims with at least one target attribute, build a record for each match, and write those records to CSV. The illustrated tags and rows explain the filter; they are not verified course export results.

**Traverse the stage**

**Filter by attributes**

**Lower branch:** Skip prims with no matching attributes.

**Build records**

**Write CSV.**

![A tagged Delta router and cart pass through an attribute filter into records and CSV rows; an untagged cube is skipped.](optimization-data/images/inventory-export-workflow.png)![Factory viewport with the Script Editor open to an inventory export script and its console output.](optimization-data/images/image81.png)

Now let’s export the attributes taught in this course from matching prims to CSV.

This script targets prims with at least one of the custom attributes added in the metadata and production-line lessons. It does not export untagged assets or all properties of each matching prim.

**Key Features of this Export Script**

**Targeted Filtering** Only exports assets that contain your custom metadata attributes, making the process faster and the results more relevant for facility management.

**Smart Categorization** Automatically categorizes assets based on naming patterns, grouping them into meaningful categories like “Manufacturing Equipment,” “Safety Equipment,” and “Storage/Transport.”

**Structured Output** Creates a standardized CSV format that can be easily imported into facility management systems, maintenance databases, or spreadsheet applications.

**Performance Optimization** Uses efficient USD methods to check for specific attributes without processing every prim’s complete attribute list.

#### NOTE
To change the location that the file will save to, search for the comment in the code:

```default
# Set your CSV file save location here
CSV_SAVE_PATH = "C:/Users/YourName/Desktop/factory_inventory.csv"
```

**Understanding the TARGET_ATTRIBUTES Configuration**

The script searches specifically for these custom attributes:

- `Unique_ID` — The integer identifier added during asset metadata review.
- `ProductionLine_ID` — The string identifying a production line.
- `Capacity_UnitsPerHour` — The integer production capacity of an assembly.

Only prims containing at least one of these attributes are included. A blank field means the attribute is absent or has no default value; it does not mean zero capacity. Read errors are reported as `Error reading value` and must be investigated.

Before interpreting totals, confirm that the intended payloads are loaded and the intended asset or line roots carry these attributes. Default traversal does not descend into instances, so tags authored only on their descendants will not appear. Review the exported paths for internal parts or nested assemblies before treating the row count as an equipment count.

**Interpreting Your Export Results**

![Earlier export example reporting 27 assets: 4 manufacturing equipment, 2 other and 17 safety equipment.](optimization-data/images/image82.png)

This screenshot shows an earlier export example. Use the attribute names and illustrative table below for the current script; do not expect identical counts or columns.

After running the script, you’ll see console output showing:

- **Export confirmation** with file location and asset count
- **Category breakdown** showing how many assets were found in each type
- **File location** where you can open the CSV in Excel or your preferred application

The CSV contains only tagged prims included by the traversal. It is a starting point for facility reporting, not proof of a complete inventory.

```python
from pxr import Usd
import omni.usd
import csv
import os

# ===== CONFIGURATION =====
# Set your CSV file save location here
CSV_SAVE_PATH = "C:/Users/YourName/Desktop/factory_inventory.csv"

# Define the specific custom attributes to search for
TARGET_ATTRIBUTES = [
    'Unique_ID',
    'ProductionLine_ID',
    'Capacity_UnitsPerHour'
]
# =========================

stage = omni.usd.get_context().get_stage()

if stage:
    print("Exporting factory inventory with attributes...")
    
    inventory_data = []
    
    for prim in stage.Traverse():
        # Check for target attributes
        found_attributes = {}
        for attr_name in TARGET_ATTRIBUTES:
            if prim.HasAttribute(attr_name):
                try:
                    value = prim.GetAttribute(attr_name).Get()
                    found_attributes[attr_name] = str(value) if value is not None else ''
                except:
                    found_attributes[attr_name] = 'Error reading value'
        
        # Only include prims with target attributes
        if found_attributes:
            # Categorize based on name patterns
            name_lower = prim.GetName().lower()
            if any(keyword in name_lower for keyword in ['machine', 'robot', 'press', 'router', 'feeder']):
                category = 'Manufacturing Equipment'
            elif any(keyword in name_lower for keyword in ['cone', 'barrier', 'safety', 'warning']):
                category = 'Safety Equipment'
            elif any(keyword in name_lower for keyword in ['pallet', 'container', 'box']):
                category = 'Storage/Transport'
            elif any(keyword in name_lower for keyword in ['light', 'lamp']):
                category = 'Lighting'
            else:
                category = 'Other'
            
            prim_data = {
                'Name': prim.GetName(),
                'Path': str(prim.GetPath()),
                'Type': prim.GetTypeName(),
                'Kind': prim.GetMetadata("kind") or "",
                'Category': category
            }
            prim_data.update(found_attributes)
            inventory_data.append(prim_data)
    
    if inventory_data:
        # Write CSV
        base_columns = ['Name', 'Path', 'Type', 'Kind', 'Category']
        target_columns = [attr for attr in TARGET_ATTRIBUTES if any(attr in item for item in inventory_data)]
        columns = base_columns + target_columns
        
        with open(CSV_SAVE_PATH, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.DictWriter(csvfile, fieldnames=columns)
            writer.writeheader()
            for item in inventory_data:
                row = {col: item.get(col, '') for col in columns}
                writer.writerow(row)
        
        print(f"✅ Exported {len(inventory_data)} assets to: {CSV_SAVE_PATH}")
        
        # Category summary
        categories = {}
        for item in inventory_data:
            cat = item['Category']
            categories[cat] = categories.get(cat, 0) + 1
        
        for category, count in sorted(categories.items()):
            print(f"   {category}: {count}")
            
    else:
        print("No assets found with target attributes.")
        
else:
    print("No USD stage loaded.")
```

## Factory Inventory Export Results

The table below is an illustrative example of the updated columns, not a captured course run. With the current name-based rules, `CL6_Line_Full` is classified as `Other`. Your rows and included attribute columns depend on the loaded stage and its tags; verify actual output in the course application.

| Name          | Path                               | Type   | Kind     | Category   |   Unique_ID | ProductionLine_ID          |   Capacity_UnitsPerHour |
|---------------|------------------------------------|--------|----------|------------|-------------|----------------------------|-------------------------|
| CL6_Line_Full | /World/CL6_Line_Full               | Xform  | assembly | Other      |             | PL_001_ElectronicsAssembly |                     450 |
| N_05_Assembly | /World/CL6_Line_Full/N_05_Assembly | Xform  | assembly | Other      |      105001 |                            |                         |

### Practical Applications and Next Steps

#### Real-World Use Cases

The CSV export you’ve created enables:

| Use Case                 | Data Application                   | Business Value                      |
|--------------------------|------------------------------------|-------------------------------------|
| **Maintenance Planning** | Asset locations and schedules      | Preventive maintenance optimization |
| **Compliance Reporting** | Safety equipment inventory         | Regulatory compliance documentation |
| **Facility Management**  | Reviewed inventory of tagged prims | Integration with CMMS systems       |
| **Change Management**    | Before/after asset comparisons     | Impact assessment and planning      |

### Extending the Scripts

You can enhance these scripts by:

- **Adding filters** for specific production lines or areas
- **Including geometric data** like bounding box dimensions
- **Collecting material information** for procurement planning
- **Integrating with databases** for real-time facility management
- **Generating reports** in different formats (JSON, Excel, XML)

### Integration With Facility Systems

This scripting approach transforms your visual digital twin into a data-rich facility management tool, bridging the gap between 3D visualization and operational business systems. The automated inventory generation you’ve learned represents a fundamental capability for industrial digital twin applications.
