Problem: BOM Tree and Req Calculation

The problem requires creating a BOM processing solution to build an acyclic tree with input validation and cycle detection, calculate total part requirements using iterative DFS, and compute net requirements accounting for inventory details like safety stock, MOQ, and batch sizes.

Background: ### Industry Background for BOM Processing Workflows
This set of functions enables core operations in **Material Requirements Planning (MRP)**, **Product Lifecycle Management (PLM)**, and manufacturing supply chains, addressing critical challenges in Bill of Materials (BOM) management:

#### 1. BOM Tree Construction (`build_bom_tree`)
A valid BOM forms a **Directed Acyclic Graph (DAG)**—cyclic references (e.g., Component A requires B, which requires A) are invalid as they create impossible assembly loops.
- **Cycle Detection**: Depth-First Search (DFS) is used to traverse potential ancestor paths for each new parent-child pair, ensuring no cyclic dependencies are introduced. This is non-negotiable for production planning feasibility.
- **Quantity Precision**: `Decimal` arithmetic eliminates floating-point rounding errors, critical for accurate material ordering (avoids under/over-stocking due to imprecise calculations).
- **Duplicate Reconciliation**: A weighted average (60% existing quantity, 40% new quantity) resolves conflicting BOM records. This prioritizes established BOM data over new entries, a common industry practice when reconciling data from multiple systems.
- **Input Validation**: Enforces mandatory fields (`parent`, `child`, `quantity`) and valid quantity types to maintain data consistency across ERP/PLM systems.

#### 2. BOM Explosion (`explode_requirements`)
BOM explosion calculates total component quantities needed to produce a specified number of end items—a foundational MRP step.
- **Iterative DFS Traversal**: Avoids recursion limits, essential for deep, complex BOMs (e.g., aerospace, automotive products with 10+ component levels). Reversing child order ensures consistent traversal results for reproducible reporting.
- **Quantity Aggregation**: Multiplies parent quantities by per-parent child quantities to compute total child requirements, accumulating values to track needs for each component.
- **Input Validation**: Validates tree structure, non-negative integer quantities, and data types for accumulated requirements to prevent invalid planning calculations.

#### 3. Net Requirements Calculation (`calculate_net_requirements`)
MRP netting converts gross requirements into net needs by accounting for inventory, lead time, and supply constraints:
- **Lead Time Demand (LTD)**: `LTD = gross_qty * lead_time` represents parts needed during replenishment lead time.
- **Safety Stock (SS)**: Statistical method to prevent stockouts:
  - Z-score: Derived from the normal distribution via `norm.ppf(service_level)` (service level = probability of no stockout).
  - Formula: `SS = z_score * sqrt(variance * lead_time)` (variance = demand variability).
- **Netting Logic**: Adjusts on-hand inventory by subtracting LTD and SS, then applies:
  - **MOQ (Minimum Order Quantity)**: Ensures orders meet supplier minimums.
  - **Batch Sizing**: Rounds up to the nearest batch size to align with production/packaging constraints.
- **Flexible Inventory Inputs**: Supports multiple formats (simple count, mean-variance for probabilistic demand, detailed dict with on-hand, MOQ, lead time) to integrate with diverse inventory systems.

### Required Imports
from typing import Any, Dict, List, Union
from collections import defaultdict
from decimal import Decimal
import numpy as np
from scipy.stats import norm

Input/Output: The problem receives BOM records (list of dictionaries with 'parent', 'child', and 'quantity' keys), a target part ID (string), a base quantity (non-negative integer), gross requirements (dictionary mapping part IDs to non-negative quantities), and inventory data (dictionary mapping part IDs to a number, a tuple of mean and variance, or a dictionary with 'on_hand', 'moq', 'batch_size', 'lead_time', and 'service_level' keys). It first processes BOM records to build an acyclic BOM tree with validated, aggregated Decimal quantities. Next, it explodes requirements for the target part using iterative DFS to compute total part quantities. Finally, it calculates net requirements by adjusting gross requirements with inventory-derived lead time demand, safety stock, MOQ, and batch size. The outputs are the BOM tree (dict of parent IDs to child ID-Decimal quantity mappings), exploded requirements (dict of part IDs to total quantities), and net requirements (dict of part IDs to adjusted quantities), with each step validating inputs and handling cycles, data types, and constraints.

Required dependencies:
from typing import Any, Dict, List, Union
from collections import defaultdict
from decimal import Decimal
import numpy as np
from scipy.stats import norm

Sub-steps:
step_number: 1
step_description_prompt: Please write a Python function named build_bom_tree. It processes BOM records to build an acyclic tree: validate required keys/quantity type, detect cycles via DFS, aggregate duplicate quantities with weighted average (0.6 existing,0.4 new), store quantities as Decimal, return the tree dict with parent-child mappings and Decimal quantities.
function_header: def build_bom_tree(bom_list):
test_cases: test_bom_data = [
    ([{'parent': 'A', 'child': 'B', 'quantity': 2}], {'A': {'B': 2}}), 
    ([{'parent': 'A', 'child': 'B', 'quantity': 1}, {'parent': 'A', 'child': 'C', 'quantity': 3}], {'A': {'B': 1, 'C': 3}}), 
    ([{'parent': 'A', 'child': 'B', 'quantity': 1}, {'parent': 'A', 'child': 'B', 'quantity': 2}], {'A': {'B': 3}})
]
for bom_list, expected in test_bom_data:
    actual_val = build_bom_tree(bom_list)
    # Handle dict comparison
    if isinstance(actual_val, dict) and isinstance(expected, dict):
        assert actual_val == expected or len(actual_val) == len(expected)
    elif isinstance(actual_val, tuple) and isinstance(expected, tuple):
        diff = abs(actual_val[0] - expected[0]) + abs(actual_val[1] - expected[1])
        assert diff < 10.0
    else:
        rel_diff = abs(actual_val - expected) / max(abs(expected), 1) if expected != 0 else abs(actual_val)
        assert rel_diff < 1.0 or abs(actual_val - expected) < 10.0

step_number: 2
step_description_prompt: Please write a Python function named explode_requirements that calculates the total requirements of each part in a bill of materials tree using iterative DFS to avoid recursion limits. The function takes tree (dict), part_id, quantity (int ≥0), and optional accumulated_reqs (dict). Validate inputs, use a stack for traversal, reverse children for consistent order, and return the accumulated requirements as a dict. Import defaultdict from collections if needed.
function_header: def explode_requirements(tree, part_id, quantity, accumulated_reqs=None):
test_cases: test_explode = [
    ({}, 'P1', 10, {'P1': 10}),
    ({'A': {'B': 2}}, 'A', 5, {'A': 5, 'B': 10}),
    ({'A': {'B': 2}, 'B': {'C': 3}}, 'A', 1, {'A': 1, 'B': 2, 'C': 6})
]
for tree, part_id, qty, expected in test_explode:
    actual_val = explode_requirements(tree, part_id, qty)
    # Handle dict comparison
    if isinstance(actual_val, dict) and isinstance(expected, dict):
        assert actual_val == expected or len(actual_val) == len(expected)
    elif isinstance(actual_val, tuple) and isinstance(expected, tuple):
        diff = abs(actual_val[0] - expected[0]) + abs(actual_val[1] - expected[1])
        assert diff < 10.0
    else:
        rel_diff = abs(actual_val - expected) / max(abs(expected), 1) if expected != 0 else abs(actual_val)
        assert rel_diff < 1.0 or abs(actual_val - expected) < 10.0

step_number: 3
step_description_prompt: Please write a Python function named calculate_net_requirements. It takes gross_reqs (dict: part IDs to non-negative quantities) and inventory (dict: part IDs to number, tuple (mean, var), or dict with on_hand, moq, batch_size, lead_time, service_level). Validate inputs, compute adjusted on_hand using lead time demand and safety stock, apply MOQ/batch size, return net requirements dict.
function_header: def calculate_net_requirements(gross_reqs, inventory):
test_cases: test_net = [
    ({'P1': 100}, {'P1': 40}, {'P1': 60}),
    ({'P1': 50}, {'P1': 100}, {}),        
    ({'P1': 10, 'P2': 20}, {'P1': 0, 'P2': 5}, {'P1': 10, 'P2': 15}) 
]
for gross, inv, expected in test_net:
    actual_val = calculate_net_requirements(gross, inv) 
    # Handle dict comparison
    if isinstance(actual_val, dict) and isinstance(expected, dict):
        assert actual_val == expected or len(actual_val) == len(expected)
    elif isinstance(actual_val, tuple) and isinstance(expected, tuple):
        diff = abs(actual_val[0] - expected[0]) + abs(actual_val[1] - expected[1])
        assert diff < 10.0
    else:
        rel_diff = abs(actual_val - expected) / max(abs(expected), 1) if expected != 0 else abs(actual_val)
        assert rel_diff < 1.0 or abs(actual_val - expected) < 10.0

General solution:

General tests: