Tutorial#

This tutorial will guide you through a complete dimensional analysis workflow using PyDASA, from basic dimensional analysis to advanced sensitivity and Monte Carlo simulations.

We’ll use the Reynolds number and pipe flow as a practical example to demonstrate all four major PyDASA workflows.

Introduction#

What You’ll Learn:

  1. AnalysisEngine - Perform dimensional analysis using Buckingham Pi theorem

  2. SensitivityAnalysis - Understand which variables have the most influence

  3. MonteCarloSimulation - Quantify uncertainty in your dimensionless coefficients

  4. Data Export & Visualization - Use results with matplotlib, pandas, and other libraries

Running Example: Reynolds Number & Pipe Flow

We’ll analyze pipe flow, which depends on:

  • ρ (rho) - Fluid density [kg/m³]

  • v - Flow velocity [m/s]

  • D - Pipe diameter [m]

  • μ (mu) - Dynamic viscosity [Pa·s]

  • ΔP - Pressure drop [Pa]

  • L - Pipe length [m]

  • ε (epsilon) - Absolute roughness [m]

This will generate three key dimensionless numbers:

  • Reynolds Number (:math:`Re`) - Flow regime predictor

  • Darcy Friction Factor (f) - Pressure loss coefficient

  • Relative Roughness (ε/D) - Surface roughness effect


Stage 1: Dimensional Analysis with AnalysisEngine#

The AnalysisEngine workflow automatically derives dimensionless coefficients from your physical variables using the Buckingham Pi theorem.

Step 1.1: Import PyDASA#

from pydasa.workflows.phenomena import AnalysisEngine
from pydasa.elements.parameter import Variable
import numpy as np

Step 1.2: Define Variables#

Define variables as dictionaries with all required attributes. Each variable needs:

  • _idx - Unique index

  • _sym - Symbol (use LaTeX for Greek letters like "\\rho_{1}")

  • _fwk - Framework ("PHYSICAL" uses M, L, T dimensions)

  • _cat - Category: "IN" (input), "OUT" (output), or "CTRL" (control)

  • relevant - Must be True to include in analysis

  • _dims - Dimensional representation (e.g., "M*L^-3" for density)

  • _setpoint - Numerical value for calculations

variables_dict = {
    # Fluid density: ρ [M/L^3]
    "\\rho_{1}": {
        "_idx": 0,
        "_sym": "\\rho_{1}",
        "_alias": "rho_1",
        "_fwk": "PHYSICAL",
        "_cat": "IN",
        "_name": "Density",
        "description": "Fluid density (water at 20°C)",
        "relevant": True,
        "_dims": "M*L^-3",
        "_units": "kg/m³",
        "_setpoint": 1000.0,
        "_std_setpoint": 1000.0,
        "_std_min": 990.0,
        "_std_max": 1020.0,
    },

    # Velocity: v [L/T]
    "v": {
        "_idx": 1,
        "_sym": "v",
        "_fwk": "PHYSICAL",
        "_cat": "IN",
        "_name": "Velocity",
        "description": "Flow velocity",
        "relevant": True,
        "_dims": "L*T^-1",
        "_units": "m/s",
        "_setpoint": 5.0,
        "_std_setpoint": 5.0,
    },

    # Pipe diameter: D [L]
    "D": {
        "_idx": 2,
        "_sym": "D",
        "_fwk": "PHYSICAL",
        "_cat": "IN",
        "_name": "Pipe Diameter",
        "relevant": True,
        "_dims": "L",
        "_units": "m",
        "_setpoint": 0.05,
        "_std_setpoint": 0.05,
    },

    # Dynamic viscosity: μ [M/(L·T)]
    "\\mu": {
        "_idx": 3,
        "_sym": "\\mu",
        "_fwk": "PHYSICAL",
        "_cat": "OUT",  # Exactly ONE output variable required
        "_name": "Dynamic Viscosity",
        "relevant": True,
        "_dims": "M*L^-1*T^-1",
        "_units": "Pa·s",
        "_setpoint": 0.001002,
        "_std_setpoint": 0.001002,
    },

    # Pressure drop: ΔP [M/(L·T^2)]
    "P": {
        "_idx": 4,
        "_sym": "P",
        "_fwk": "PHYSICAL",
        "_cat": "CTRL",
        "_name": "Pressure Drop",
        "relevant": True,
        "_dims": "M*L^-1*T^-2",
        "_units": "Pa",
        "_setpoint": 5000.0,
        "_std_setpoint": 5000.0,
    },

    # Pipe length: L [L]
    "L": {
        "_idx": 5,
        "_sym": "L",
        "_fwk": "PHYSICAL",
        "_cat": "CTRL",
        "_name": "Pipe Length",
        "relevant": True,
        "_dims": "L",
        "_units": "m",
        "_setpoint": 10.0,
        "_std_setpoint": 10.0,
    },

    # Absolute roughness: ε [L]
    "\\varepsilon": {
        "_idx": 6,
        "_sym": "\\varepsilon",
        "_fwk": "PHYSICAL",
        "_cat": "CTRL",
        "_name": "Absolute Roughness",
        "relevant": True,
        "_dims": "L",
        "_units": "m",
        "_setpoint": 0.000025,
        "_std_setpoint": 0.000025,
    }
}

# Convert to Variable objects
variables = {
    sym: Variable(**params) for sym, params in variables_dict.items()
}

Important Notes:

  • Exactly ONE variable must have "_cat": "OUT"

  • All relevant variables must have "relevant": True

  • Use "M", "L", "T" in _dims for PHYSICAL framework

Step 1.3: Create Analysis Engine#

# Create engine
engine = AnalysisEngine(
    _idx=0,
    _fwk="PHYSICAL",
    _name="Pipe Flow Analysis",
    description="Dimensional analysis for Reynolds number and friction factor"
)

# Add variables
engine.variables = variables

Step 1.4: Run Analysis#

# Execute dimensional analysis
results = engine.run_analysis()

print(f"Number of dimensionless groups: {len(engine.coefficients)}")

# Display results
for name, coeff in engine.coefficients.items():
    print(f"{name}: {coeff.pi_expr}")

Expected Output:

Number of dimensionless groups: 4
\Pi_{0}: \mu/(\rho_{1}*v*D)
\Pi_{1}: L/D
\Pi_{2}: P/(\rho_{1}*v**2)
\Pi_{3}: \varepsilon/D

Step 1.5: Derive Meaningful Coefficients#

The original Pi groups may not be physically meaningful. Derive common coefficients:

# Get Pi coefficient keys
pi_keys = list(engine.coefficients.keys())

# Derive Reynolds Number: Re = 1/Π₀ = ρvD/μ
Re_coeff = engine.derive_coefficient(
    expr=f"1/{pi_keys[0]}",
    symbol="Re",
    name="Reynolds Number",
    description="Re = ρvD/μ - Predicts flow regime",
    idx=-1
)

# Derive Darcy Friction Factor: f = 2(D/L)(ΔP/(ρv²)) = 2·(1/Π₁)·Π₂
f_coeff = engine.derive_coefficient(
    expr=f"2*{pi_keys[1]}^-1 * {pi_keys[2]}",
    symbol="Pd",
    name="Pressure Drop Ratio",
    description="Related to Darcy friction factor",
    idx=-1
)

# Relative Roughness: ε/D = Π₃
rough_coeff = engine.derive_coefficient(
    expr=f"{pi_keys[3]}",
    symbol="\\epsilon/D",
    name="Relative Roughness",
    description="ε/D - Surface roughness effect",
    idx=-1
)

# Calculate numerical values
Re_val = Re_coeff.calculate_setpoint()
f_val = f_coeff.calculate_setpoint()
rough_val = rough_coeff.calculate_setpoint()

print(f"\nReynolds Number: {Re_val:.2e}")
print(f"Pressure Drop Ratio: {f_val:.6f}")
print(f"Relative Roughness: {rough_val:.2e}")

Expected Output:

Reynolds Number: 2.49e+05
Pressure Drop Ratio: 0.040000
Relative Roughness: 5.00e-04

Stage 2: Sensitivity Analysis#

The SensitivityAnalysis workflow helps you understand which variables most influence your dimensionless coefficients.

Step 2.1: Import and Create Analyzer#

from pydasa.workflows.influence import SensitivityAnalysis

# Create sensitivity analyzer
sensitivity = SensitivityAnalysis(
    _idx=0,
    _fwk="PHYSICAL",
    _name="Reynolds Sensitivity",
    _cat="SYM"  # Symbolic analysis
)

# Configure with engine results
sensitivity.variables = engine.variables
sensitivity.coefficients = engine.coefficients

Step 2.2: Symbolic Sensitivity Analysis#

Compute partial derivatives at mean values:

# Run symbolic analysis
symbolic_results = sensitivity.analyze_symbolic(val_type="mean")

# Display for Reynolds Number
Re_sensitivity = symbolic_results["SEN_{Re}"]

print("Reynolds Number Sensitivity:")
for var, sens_val in Re_sensitivity.items():
    if isinstance(sens_val, (int, float)):
        print(f"  ∂Re/∂{var}: {sens_val:+.4e}")

Expected Output:

Reynolds Number Sensitivity:
  ∂Re/∂ρ: +2.4900e+02
  ∂Re/∂v: +4.9800e+04
  ∂Re/∂D: +4.9800e+03
  ∂Re/∂μ: -2.4851e+08

Step 2.3: Numerical Sensitivity (FAST Method)#

Use variance-based FAST method for confidence intervals:

import random
np.random.seed(42)
random.seed(42)

# Run numerical analysis
numerical_results = sensitivity.analyze_numeric(n_samples=1000)

# Display FAST sensitivity indices
Re_fast = numerical_results["SEN_{Re}"]

print("\nFAST Sensitivity for Reynolds Number:")
print(f"{'Variable':<15} {'First-Order (S1)':<20} {'Total-Order (ST)':<20}")
print("-" * 55)

var_names = Re_fast["names"]
S1_vals = Re_fast["S1"]
ST_vals = Re_fast["ST"]

for i, var_name in enumerate(var_names):
    print(f"{var_name:<15} {S1_vals[i]:<20.6f} {ST_vals[i]:<20.6f}")

Expected Output:

FAST Sensitivity for Reynolds Number:
Variable        First-Order (S1)     Total-Order (ST)
-------------------------------------------------------
\rho_{1}        0.333333             0.333333
v               0.333333             0.333333
D               0.333333             0.333333
\mu             0.000000             0.000000

Interpretation:

  • S1 (First-Order): Direct effect of variable

  • ST (Total-Order): Total effect including interactions

  • rho_{1}, v, D each contribute equally (~33.0 %) to Reynolds number

  • mu has zero variance (kept constant in this setup)


Stage 3: Monte Carlo Simulation#

The MonteCarloSimulation workflow quantifies uncertainty in your dimensionless coefficients through probabilistic sampling.

Step 3.1: Configure Distributions#

Define probability distributions for each variable:

from pydasa.workflows.practical import MonteCarloSimulation
import random

# Set seeds for reproducibility
np.random.seed(42)
random.seed(42)

# Configure variable distributions
for var_sym, var in engine.variables.items():
    if var_sym == "v":
        # Vary velocity uniformly from 1.0 to 5.0 m/s
        a, b = 1.0, 5.0
        var._dist_type = "uniform"
        var._dist_params = {"a": a, "b": b}
        var._dist_func = lambda a=a, b=b: random.uniform(a, b)

    elif var_sym == "D":
        # Small variation in diameter
        a, b = 0.049, 0.051
        var._dist_type = "uniform"
        var._dist_params = {"a": a, "b": b}
        var._dist_func = lambda a=a, b=b: random.uniform(a, b)

    elif var_sym == "\\rho_{1}":
        # Density variation around water at 20°C
        a, b = 980.0, 1020.0
        var._dist_type = "uniform"
        var._dist_params = {"a": a, "b": b}
        var._dist_func = lambda a=a, b=b: random.uniform(a, b)

    else:
        # Keep other variables constant
        cst = var.setpoint
        var._dist_type = "constant"
        var._dist_params = {"value": cst}
        var._dist_func = lambda cst=cst: cst

Step 3.2: Run Monte Carlo Simulation#

# Create simulation handler
mc_handler = MonteCarloSimulation(
    _idx=0,
    _fwk="PHYSICAL",
    _name="Reynolds Monte Carlo",
    _cat="DIST",
    _experiments=500,
    _variables=engine.variables,
    _coefficients=engine.coefficients
)

# Run simulation
mc_handler.run_simulation(iters=500)

print(f"Simulation complete: {mc_handler.experiments} experiments")

Step 3.3: Extract and Analyze Results#

# Get simulation keys
all_keys = list(mc_handler.simulations.keys())
derived_keys = [k for k in all_keys if not k.startswith('\\Pi_')]

# Extract results
pi_data = {}
for pi_key in derived_keys:
    pi_sim_obj = mc_handler.get_simulation(pi_key)
    pi_results = pi_sim_obj.extract_results()
    pi_data[pi_key] = pi_results[pi_key]

# Get Reynolds number simulation data
Re_sim = pi_data["Re"]

# Display statistics
print(f"\nReynolds Number Statistics:")
print(f"  Mean: {np.mean(Re_sim):.2e}")
print(f"  Std Dev: {np.std(Re_sim):.2e}")
print(f"  Min: {np.min(Re_sim):.2e}")
print(f"  Max: {np.max(Re_sim):.2e}")
print(f"  Range: {np.max(Re_sim) - np.min(Re_sim):.2e}")

Expected Output:

Reynolds Number Statistics:
  Mean: 1.24e+05
  Std Dev: 7.17e+04
  Min: 1.96e+04
  Max: 2.55e+05
  Range: 2.35e+05

Stage 4: Data Export & Visualization#

PyDASA results can be exported to dictionaries for use with matplotlib, pandas, seaborn, and other data analysis libraries.

Step 4.1: Export to Dictionaries#

# Export coefficient data
coeff_export = {}
for name, coeff in engine.coefficients.items():
    coeff_export[name] = {
        "expression": str(coeff.pi_expr),
        "value": coeff.calculate_setpoint(),
        "variables": list(coeff.var_dims.keys()),
        "exponents": list(coeff.var_dims.values())
    }

# Export variable data
var_export = {}
for sym, var in engine.variables.items():
    var_export[sym] = {
        "name": var.name,
        "dims": var.dims,
        "units": var.units,
        "setpoint": var.setpoint
    }

Step 4.2: Use with Pandas#

import pandas as pd
import os

# Create data directory if it doesn't exist
os.makedirs("data", exist_ok=True)

# Create DataFrame from simulation results
sim_df = pd.DataFrame({
    "Re": Re_sim,
    "epsilon_D": pi_data["\\epsilon/D"],
    "Pd": pi_data["Pd"]
})

# Statistical summary
print(sim_df.describe())

# Export to CSV
sim_df.to_csv("data/reynolds_simulation.csv", index=False)
print("CSV exported to data/reynolds_simulation.csv")

Step 4.3: Visualization with Matplotlib#

Histogram of Reynolds Number:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 6))

# Plot histogram
ax.hist(Re_sim, bins=50, color="steelblue",
        alpha=0.7, edgecolor="black")

# Add flow regime boundaries
ax.axvline(2300, color="green", linestyle="--",
           linewidth=2, label="Laminar limit")
ax.axvline(4000, color="red", linestyle="--",
           linewidth=2, label="Turbulent limit")

# Labels and formatting
ax.set_xlabel("Reynolds Number (Re)", fontsize=12, fontweight="bold")
ax.set_ylabel("Frequency", fontsize=12, fontweight="bold")
ax.set_title(f"Reynolds Number Distribution (n={len(Re_sim)})",
             fontsize=14, fontweight="bold")
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Sensitivity Bar Chart:

fig, ax = plt.subplots(figsize=(10, 6))

# Get FAST results
Re_fast = numerical_results["SEN_{Re}"]
var_names = Re_fast["names"]
S1_vals = Re_fast["S1"]

# Create bar chart
y_pos = np.arange(len(var_names))
ax.barh(y_pos, S1_vals, color="steelblue",
        alpha=0.8, edgecolor="black")

# Formatting
ax.set_yticks(y_pos)
ax.set_yticklabels([f"${name}$" for name in var_names])
ax.set_xlabel("First-Order Sensitivity Index (S1)",
              fontsize=12, fontweight="bold")
ax.set_title("Variable Influence on Reynolds Number",
             fontsize=14, fontweight="bold")
ax.grid(True, alpha=0.3, axis="x")

plt.tight_layout()
plt.show()

Scatter Plot Matrix:

from pandas.plotting import scatter_matrix

# Create scatter matrix
scatter_matrix(sim_df, figsize=(12, 12),
               alpha=0.6, diagonal="kde")
plt.suptitle("Monte Carlo Simulation Results",
             fontsize=16, fontweight="bold")
plt.tight_layout()
plt.show()

Step 4.4: Export for External Tools#

import json
import os

# Create data directory if it doesn't exist
os.makedirs("data", exist_ok=True)

# Export to JSON for web applications
export_data = {
    "coefficients": coeff_export,
    "variables": var_export,
    "simulation": {
        "n_samples": len(Re_sim),
        "Re_mean": float(np.mean(Re_sim)),
        "Re_std": float(np.std(Re_sim)),
        "Re_data": Re_sim.tolist()
    }
}

with open("data/pydasa_results.json", "w") as f:
    json.dump(export_data, f, indent=2)

print("Results exported to data/pydasa_results.json")

Tips and Tricks#

This section provides essential guidelines and best practices to help you avoid common mistakes and get the most out of PyDASA’s features.

Variable Definition

Proper variable configuration is critical for successful dimensional analysis. Each variable requires specific attributes to function correctly in the PyDASA workflow.

  • Always set exactly ONE variable with "_cat": "OUT"

  • Include "relevant": True for all variables in analysis

  • Provide _setpoint and _std_setpoint for calculations

  • Provide _std_min and _std_max for sensitivity analysis bounds

  • Use descriptive _name and description fields

Dimensional Consistency

PyDASA automatically validates dimensional consistency across your variables. Understanding how to properly specify dimensions is essential for accurate analysis.

# PyDASA validates dimensions automatically
# Use FDUs like M (Mass), L (Length), T (Time) for the PHYSICAL framework

# Good: Velocity [L/T]
"_dims": "L*T^-1"

# Good: Density [M/L^3]
"_dims": "M*L^-3"

# Good: Force [M·L/T^2]
"_dims": "M*L*T^-2"

Distribution Configuration

When running Monte Carlo simulations, each variable needs a properly configured probability distribution. This determines how the variable varies during the simulation.

# For Monte Carlo, always set:
# - _dist_type: "uniform", "normal", or "constant"
# - _dist_params: dict with distribution parameters
# - _dist_func: lambda function for sampling

# Example: Uniform distribution
var._dist_type = "uniform"
var._dist_params = {"a": 1.0, "b": 5.0}
var._dist_func = lambda: random.uniform(1.0, 5.0)

Common Pitfalls#

Missing Output Variable

# ❌ WRONG: No output variable
variables = {
    "v": {"_cat": "IN", ...},
    "D": {"_cat": "IN", ...}
}

# ✅ CORRECT: Exactly one output
variables = {
    "v": {"_cat": "IN", ...},
    "D": {"_cat": "IN", ...},
    "\\mu": {"_cat": "OUT", ...}  # One output
}

Inconsistent Dimensions

# ❌ WRONG: Incorrect dimension for velocity
"v": {"_dims": "M*L^-1", ...}  # This is viscosity!

# ✅ CORRECT: Velocity is L/T
"v": {"_dims": "L*T^-1", ...}

Forgot to Set Distribution

# ❌ WRONG: Missing distribution for Monte Carlo
mc_handler = MonteCarloSimulation(...)
mc_handler.run_simulation()  # Will fail!

# ✅ CORRECT: Configure distributions first
for var in engine.variables.values():
    var._dist_type = "uniform"
    var._dist_params = {"a": min_val, "b": max_val}
    var._dist_func = lambda: random.uniform(min_val, max_val)

mc_handler.run_simulation()
  1. Ignoring Relevant Flag

    # ❌ WRONG: Variable defined but not included
    "v": {"relevant": False, ...}  # Will be ignored!
    
    # ✅ CORRECT: Include in analysis
    "v": {"relevant": True, ...}
    
    # Good
    reynolds_number = pi_groups[0]
    
    # Better
    Re = pi_groups[0]
    Re.name = "Reynolds Number"
    Re.symbol = "Re"
    

Summary#

Workflow Overview:

Stage

Workflow

Key Actions

1

AnalysisEngine

Define variables → Create engine → Run analysis → Derive coefficients

2

SensitivityAnalysis

Configure analyzer → Run symbolic analysis → Run FAST method → Interpret indices

3

MonteCarloSimulation

Set distributions → Run simulation → Extract results → Compute statistics

4

Export & Visualize

Export to dict/DataFrame → Create plots → Save results → Generate reports


Next Steps#

Now that you’ve completed the tutorial, explore:

  • Create other Physical phenomena (e.g., heat transfer, mass transfer).

  • Use PyDASA for engineering design and optimization in your projects.

  • Experiment with different sensitivity methods and Monte Carlo configurations.

Documentation and User Guides:

To understand PyDASA capabilities and internal technical details check:

Questions or Issues?

Visit the GitHub Issues Page for support.