Analysis Examples¶
This guide provides comprehensive examples of using OptiScope's analysis tools to extract insights from optimization results.
Pareto Front Identification¶
Identify non-dominated solutions from your optimization results.
Basic Usage¶
from optiscope.analysis import identify_pareto_front
from optiscope.core.result_set import ResultSet
import numpy as np
# Identify Pareto-optimal solutions
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions
)
print(f"Found {pareto_mask.sum()} Pareto-optimal solutions out of {len(result.objectives)}")
# Create a result set from the Pareto front
pareto_set = ResultSet(
name="pareto_front",
indices=np.where(pareto_mask)[0].tolist(),
created_by="pareto_analysis",
description="Non-dominated solutions"
)
# Add to result
result.add_set(pareto_set)
# Access Pareto solutions
pareto_objectives = result.objectives[pareto_mask]
print(f"Pareto front objectives:\n{pareto_objectives}")
With Feasibility Filtering¶
Only consider feasible solutions when identifying the Pareto front:
import numpy as np
# Create feasibility mask (all constraints <= 0)
if result.constraints is not None:
feasible_mask = np.all(result.constraints <= 0, axis=1)
else:
feasible_mask = np.ones(len(result.objectives), dtype=bool)
# Identify Pareto front among feasible solutions
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions,
mask=feasible_mask
)
print(f"Found {pareto_mask.sum()} feasible Pareto-optimal solutions")
Smart Pareto Filter¶
Reduce large Pareto fronts to a representative subset while preserving diversity.
Manual Epsilon Specification¶
from optiscope.analysis import smart_pareto_filter
import numpy as np
# First, get the Pareto front
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]
print(f"Pareto front has {len(pareto_objectives)} solutions")
# Apply Smart Pareto Filter with manual epsilon
# Epsilon controls the minimum separation between selected points
filtered_indices = smart_pareto_filter(
objectives=pareto_objectives,
epsilon=0.05, # 5% minimum separation in normalized space
normalize=True
)
print(f"Smart Pareto Filter reduced to {len(filtered_indices)} solutions")
# Map back to original indices
final_indices = pareto_indices[filtered_indices]
# Create result set
result.add_set(ResultSet(
name="filtered_pareto_manual",
indices=final_indices.tolist(),
created_by="smart_pareto_filter",
description=f"Filtered Pareto front with epsilon=0.05 ({len(filtered_indices)} points)"
))
Adaptive Smart Pareto Filter¶
Automatically determine the best epsilon based on target reduction:
from optiscope.analysis import adaptive_smart_pareto_filter
import numpy as np
# Get Pareto front
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]
# Apply adaptive filter
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]
# Apply adaptive filter
filtered_indices = adaptive_smart_pareto_filter(
objectives=pareto_objectives,
target_reduction=0.5, # Reduce to 50% of original
min_points=10, # Keep at least 10 points
max_points=50, # Keep at most 50 points
normalize=True
)
print(f"Adaptive filter reduced from {len(pareto_objectives)} to {len(filtered_indices)} solutions")
# Create result set
final_indices = pareto_indices[filtered_indices]
result.add_set(ResultSet(
name="filtered_pareto",
indices=final_indices.tolist(),
created_by="adaptive_smart_pareto_filter",
description=f"Representative subset of Pareto front ({len(filtered_indices)} points)"
))
Visualizing Filter Effect¶
from optiscope.analysis import visualize_filter_effect
# Visualize the effect of filtering (for 2D or 3D objectives)
fig = visualize_filter_effect(
objectives=pareto_objectives,
filtered_indices=filtered_indices,
directions=result.metadata.optimization_directions
)
fig.show()
TOPSIS (Multi-Criteria Decision Analysis)¶
Rank solutions based on your preferences using TOPSIS.
Basic TOPSIS¶
from optiscope.analysis import topsis
import numpy as np
# Define weights for each objective
# Weights should sum to 1.0 and represent relative importance
weights = [0.4, 0.3, 0.3] # 40% weight on first objective, 30% on others
# Calculate TOPSIS scores
scores = topsis(
objectives=result.objectives,
weights=weights,
directions=result.metadata.optimization_directions,
normalize_method="vector" # or "minmax"
)
# Get the best solution
best_idx = np.argmax(scores)
print(f"Best solution index: {best_idx}")
print(f"TOPSIS score: {scores[best_idx]:.3f}")
print(f"Objectives: {result.objectives.iloc[best_idx]}")
# Get top 10 solutions
top_10_indices = np.argsort(scores)[-10:][::-1]
print(f"\nTop 10 solutions:")
for i, idx in enumerate(top_10_indices):
print(f"{i+1}. Index {idx}: Score = {scores[idx]:.3f}")
Detailed TOPSIS Results¶
# Get detailed results including distances and ranking
details = topsis(
objectives=result.objectives,
weights=[0.4, 0.3, 0.3],
directions=result.metadata.optimization_directions,
return_details=True
)
print(f"Ideal solution: {details['ideal']}")
print(f"Anti-ideal solution: {details['anti_ideal']}")
print(f"\nTop 5 solutions:")
for i, idx in enumerate(details['ranking'][:5]):
print(f"{i+1}. Index {idx}:")
print(f" Score: {details['scores'][idx]:.3f}")
print(f" Distance to ideal: {details['distance_to_ideal'][idx]:.3f}")
print(f" Distance to anti-ideal: {details['distance_to_anti_ideal'][idx]:.3f}")
# Create result set for top solutions
result.add_set(ResultSet(
name="top_10_topsis",
indices=details['ranking'][:10].tolist(),
created_by="topsis",
description=f"Top 10 solutions by TOPSIS (weights: {weights})"
))
Comparing Weight Scenarios¶
Compare how different stakeholder preferences affect rankings:
from optiscope.analysis import compare_weight_scenarios
import numpy as np
# Define different weight scenarios
scenarios = {
"Cost-focused": np.array([0.6, 0.2, 0.2]),
"Performance-focused": np.array([0.2, 0.6, 0.2]),
"Balanced": np.array([0.33, 0.33, 0.34]),
"Quality-focused": np.array([0.2, 0.2, 0.6])
}
# Compare scenarios
comparison = compare_weight_scenarios(
objectives=result.objectives,
weight_scenarios=scenarios,
directions=result.metadata.optimization_directions,
top_n=5 # Show top 5 for each scenario
)
print("Top solutions for each scenario:")
print(comparison)
# Find consensus solutions (appear in top 5 for all scenarios)
top_indices_per_scenario = []
for scenario_name in scenarios.keys():
scenario_top = comparison[comparison['Scenario'] == scenario_name]['Solution_Index'].values
top_indices_per_scenario.append(set(scenario_top))
consensus = set.intersection(*top_indices_per_scenario)
print(f"\nConsensus solutions (in top 5 for all scenarios): {consensus}")
Sensitivity Analysis¶
Analyze how sensitive rankings are to weight changes:
from optiscope.analysis import sensitivity_analysis_topsis
import numpy as np
# Define base weights
base_weights = np.array([0.4, 0.3, 0.3])
# Perform sensitivity analysis
sensitivity = sensitivity_analysis_topsis(
objectives=result.objectives,
base_weights=base_weights,
directions=result.metadata.optimization_directions,
perturbation=0.1, # ±10% variation
n_samples=100 # Number of random weight variations
)
# Check stability of top solutions
print("Stability of top solutions:")
for i in range(5):
stability = sensitivity['stability_top_5'][i]
print(f"Solution {i}: appears in top 5 in {stability:.1%} of scenarios")
# Identify robust solutions (stable across weight variations)
robust_threshold = 0.8 # 80% stability
robust_solutions = np.where(sensitivity['stability_top_5'] >= robust_threshold)[0]
print(f"\nRobust solutions (>80% stability): {robust_solutions}")
Knee Point Detection¶
Identify the "best compromise" solutions on the Pareto front.
Basic Knee Detection¶
from optiscope.analysis import detect_knee_points
import numpy as np
# Get Pareto front
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
pareto_objectives = result.objectives.iloc[pareto_indices]
# Detect knee point using angle-based method
knee_indices = detect_knee_points(
objectives=pareto_objectives,
method="angle", # or "distance", "tradeoff", "curvature"
n_knees=1,
normalize=True
)
# Map back to original indices
knee_idx = pareto_indices[knee_indices[0]]
print(f"Knee point at index: {knee_idx}")
print(f"Knee point objectives: {result.objectives.iloc[knee_idx]}")
# Create result set
result.add_set(ResultSet(
name="knee_point",
indices=[knee_idx],
created_by="knee_detection",
description="Best compromise solution (knee point)"
))
Comparing Detection Methods¶
# Compare different knee detection methods
methods = ["angle", "distance", "tradeoff", "curvature"]
knee_solutions = {}
for method in methods:
knee_indices = detect_knee_points(
objectives=pareto_objectives,
method=method,
n_knees=1,
normalize=True
)
knee_idx = pareto_indices[knee_indices[0]]
knee_solutions[method] = knee_idx
print(f"{method.capitalize()} method: index {knee_idx}")
print(f" Objectives: {result.objectives.iloc[knee_idx]}")
# Check if methods agree
if len(set(knee_solutions.values())) == 1:
print("\nAll methods agree on the knee point!")
else:
print(f"\nMethods found {len(set(knee_solutions.values()))} different knee points")
Finding Knee Region¶
Instead of a single point, find a region of good compromise solutions:
from optiscope.analysis import find_knee_region
# Find knee region
region_indices, start, end = find_knee_region(
objectives=pareto_objectives,
width=0.1, # 10% of normalized space
method="angle",
normalize=True
)
# Map back to original indices
region_original_indices = pareto_indices[region_indices]
print(f"Knee region contains {len(region_indices)} solutions")
print(f"Region spans from index {start} to {end}")
# Create result set
result.add_set(ResultSet(
name="knee_region",
indices=region_original_indices.tolist(),
created_by="knee_region_detection",
description=f"Knee region with {len(region_indices)} compromise solutions"
))
Combining Analysis Tools¶
The real power comes from combining multiple analysis tools in a workflow.
Complete Analysis Workflow¶
from optiscope.analysis import (
identify_pareto_front,
adaptive_smart_pareto_filter,
topsis,
detect_knee_points
)
import numpy as np
# Step 1: Identify Pareto front
print("Step 1: Identifying Pareto front...")
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
print(f" Found {len(pareto_indices)} Pareto-optimal solutions")
# Step 2: Apply Smart Pareto Filter
print("\nStep 2: Applying Smart Pareto Filter...")
filtered_indices = adaptive_smart_pareto_filter(
objectives=result.objectives.iloc[pareto_indices],
target_reduction=0.3, # Reduce to 30% of Pareto front
min_points=10,
normalize=True
)
final_indices = pareto_indices[filtered_indices]
print(f" Reduced to {len(final_indices)} representative solutions")
# Step 3: Rank with TOPSIS
print("\nStep 3: Ranking with TOPSIS...")
scores = topsis(
objectives=result.objectives.iloc[final_indices],
weights=[0.4, 0.3, 0.3],
directions=result.metadata.optimization_directions
)
# Get top 3 solutions
top_3_in_filtered = np.argsort(scores)[-3:][::-1]
top_3_indices = final_indices[top_3_in_filtered]
print(f"\nTop 3 recommended solutions:")
for i, idx in enumerate(top_3_indices):
print(f"{i+1}. Index {idx}: TOPSIS score = {scores[top_3_in_filtered[i]]:.3f}")
print(f" Objectives: {result.objectives.iloc[idx]}")
# Step 4: Detect knee point
print("\nStep 4: Detecting knee point...")
knee_indices = detect_knee_points(
objectives=result.objectives.iloc[final_indices],
method="angle",
n_knees=1,
normalize=True
)
knee_idx = final_indices[knee_indices[0]]
print(f" Knee point at index: {knee_idx}")
print(f" Objectives: {result.objectives.iloc[knee_idx]}")
# Create result sets for each stage
result.add_set(ResultSet(
name="pareto_front",
indices=pareto_indices.tolist(),
created_by="analysis_workflow",
description="All Pareto-optimal solutions"
))
result.add_set(ResultSet(
name="filtered_pareto",
indices=final_indices.tolist(),
created_by="analysis_workflow",
description="Representative Pareto subset"
))
result.add_set(ResultSet(
name="top_3_recommended",
indices=top_3_indices.tolist(),
created_by="analysis_workflow",
description="Top 3 solutions by TOPSIS"
))
result.add_set(ResultSet(
name="knee_point",
indices=[knee_idx],
created_by="analysis_workflow",
description="Best compromise solution"
))
print(f"\nAnalysis complete! Created {len(result.sets)} result sets.")
Pareto Front + TOPSIS on Filtered Set¶
# A common workflow: filter Pareto front, then rank with TOPSIS
# 1. Get Pareto front
pareto_mask = identify_pareto_front(
objectives=result.objectives,
directions=result.metadata.optimization_directions
)
pareto_indices = np.where(pareto_mask)[0]
# 2. Filter to manageable size
filtered_indices = adaptive_smart_pareto_filter(
objectives=result.objectives.iloc[pareto_indices],
target_reduction=0.4,
min_points=15,
max_points=30,
normalize=True
)
final_indices = pareto_indices[filtered_indices]
# 3. Rank with TOPSIS
scores = topsis(
objectives=result.objectives.iloc[final_indices],
weights=[0.5, 0.3, 0.2], # Adjust to your preferences
directions=result.metadata.optimization_directions
)
# 4. Select best solution
best_in_filtered = np.argmax(scores)
best_idx = final_indices[best_in_filtered]
print(f"Recommended solution: index {best_idx}")
print(f"TOPSIS score: {scores[best_in_filtered]:.3f}")
print(f"Objectives: {result.objectives.iloc[best_idx]}")
print(f"Design variables: {result.design_variables.iloc[best_idx]}")
Tips and Best Practices¶
When to Use Each Tool¶
- Pareto Front Identification: Always start here for multi-objective problems to identify the best trade-off solutions.
- Smart Pareto Filter: Use when your Pareto front has >50 solutions and you need cleaner visualization or a manageable set for decision-making.
- TOPSIS: Use when you have clear preferences (weights) for objectives and need to select a single best solution or rank solutions.
- Knee Detection: Use when you want to find the "best compromise" without specifying weights, especially for 2-3 objective problems.
Choosing TOPSIS Weights¶
# Equal weights (no preference)
weights = [1/3, 1/3, 1/3]
# Prioritize first objective
weights = [0.6, 0.2, 0.2]
# Normalize weights (they should sum to 1)
weights = np.array([2, 1, 1])
weights = weights / weights.sum() # [0.5, 0.25, 0.25]
Handling Large Datasets¶
For very large optimization results (>100,000 solutions):
# 1. Pre-filter to reduce size
# Keep only top 20% for each objective
masks = []
for i in range(result.objectives.shape[1]):
threshold = np.percentile(result.objectives[:, i], 20)
if result.metadata.optimization_directions[i] == "minimize":
masks.append(result.objectives[:, i] <= threshold)
else:
masks.append(result.objectives[:, i] >= threshold)
combined_mask = np.any(masks, axis=0)
filtered_result = result.objectives[combined_mask]
# 2. Then apply Pareto identification
pareto_mask = identify_pareto_front(
objectives=filtered_result,
directions=result.metadata.optimization_directions
)