Basic Usage of Optiscope¶
This notebook demonstrates the basic usage of the optiscope library, including:
- Creating synthetic optimization data
- Creating an
OptimizationResultobject - Adding metadata
- Creating result sets
- Saving and loading results
In [1]:
Copied!
from pathlib import Path
import numpy as np
import pandas as pd
from optiscope import (
DesignVariable,
Objective,
OptimizationResult,
ProblemMetadata,
load_results,
save_results,
)
from optiscope.core import OptimizationDirection
from pathlib import Path
import numpy as np
import pandas as pd
from optiscope import (
DesignVariable,
Objective,
OptimizationResult,
ProblemMetadata,
load_results,
save_results,
)
from optiscope.core import OptimizationDirection
1. Generate Synthetic Data¶
First, we define a function to generate synthetic multi-objective optimization data. This simulates a ZDT1-like problem with 3 design variables and 2 objectives.
In [2]:
Copied!
def generate_synthetic_data(n_points: int = 100) -> dict:
"""Generate synthetic multi-objective optimization data."""
np.random.seed(42)
# Design variables
x1 = np.random.uniform(0, 10, n_points)
x2 = np.random.uniform(0, 10, n_points)
x3 = np.random.uniform(0, 10, n_points)
# Objectives (ZDT1-like problem)
f1 = x1
g = 1 + 9 * (x2 + x3) / 2
h = 1 - np.sqrt(f1 / g)
f2 = g * h
# Constraints
g1 = x1 + x2 - 10 # inequality: g1 <= 0
g2 = x1 + x3 - 15 # inequality: g2 <= 0
h1 = (x1 - 5) * (x2 - 5) * (x3 - 5) # equality: h1 = 0 (relaxed for synthetic data)
# Observables (derived quantities)
total_mass = x1 + x2 + x3
efficiency = f1 / (f2 + 1e-6)
return {
"design_variables": pd.DataFrame({"x1": x1, "x2": x2, "x3": x3}),
"objectives": pd.DataFrame({"f1_minimize": f1, "f2_minimize": f2}),
"inequality_constraints": pd.DataFrame({"g1_sum_x1_x2": g1, "g2_sum_x1_x3": g2}),
"equality_constraints": pd.DataFrame({"h1_product": h1}),
"observables": pd.DataFrame({"total_mass": total_mass, "efficiency": efficiency}),
}
print("Generating synthetic optimization data...")
data = generate_synthetic_data(n_points=100)
print(f"Generated {len(data['design_variables'])} solutions")
print(f"- Design variables: {list(data['design_variables'].columns)}")
print(f"- Objectives: {list(data['objectives'].columns)}")
def generate_synthetic_data(n_points: int = 100) -> dict:
"""Generate synthetic multi-objective optimization data."""
np.random.seed(42)
# Design variables
x1 = np.random.uniform(0, 10, n_points)
x2 = np.random.uniform(0, 10, n_points)
x3 = np.random.uniform(0, 10, n_points)
# Objectives (ZDT1-like problem)
f1 = x1
g = 1 + 9 * (x2 + x3) / 2
h = 1 - np.sqrt(f1 / g)
f2 = g * h
# Constraints
g1 = x1 + x2 - 10 # inequality: g1 <= 0
g2 = x1 + x3 - 15 # inequality: g2 <= 0
h1 = (x1 - 5) * (x2 - 5) * (x3 - 5) # equality: h1 = 0 (relaxed for synthetic data)
# Observables (derived quantities)
total_mass = x1 + x2 + x3
efficiency = f1 / (f2 + 1e-6)
return {
"design_variables": pd.DataFrame({"x1": x1, "x2": x2, "x3": x3}),
"objectives": pd.DataFrame({"f1_minimize": f1, "f2_minimize": f2}),
"inequality_constraints": pd.DataFrame({"g1_sum_x1_x2": g1, "g2_sum_x1_x3": g2}),
"equality_constraints": pd.DataFrame({"h1_product": h1}),
"observables": pd.DataFrame({"total_mass": total_mass, "efficiency": efficiency}),
}
print("Generating synthetic optimization data...")
data = generate_synthetic_data(n_points=100)
print(f"Generated {len(data['design_variables'])} solutions")
print(f"- Design variables: {list(data['design_variables'].columns)}")
print(f"- Objectives: {list(data['objectives'].columns)}")
Generating synthetic optimization data... Generated 100 solutions - Design variables: ['x1', 'x2', 'x3'] - Objectives: ['f1_minimize', 'f2_minimize']
2. Create OptimizationResult Object¶
We create a ProblemMetadata object to describe the problem and then instantiate the OptimizationResult with the generated data.
In [3]:
Copied!
problem_metadata = ProblemMetadata(
name="ZDT1-like Problem",
description="Synthetic multi-objective test problem",
solver="Random Sampling",
n_design_variables=3,
n_objectives=2,
n_inequality_constraints=2,
n_equality_constraints=1,
n_evaluations=100,
solver_version=None,
computation_time=None,
)
result = OptimizationResult(
problem_metadata=problem_metadata,
design_variables=data["design_variables"],
objectives=data["objectives"],
inequality_constraints=data["inequality_constraints"],
equality_constraints=data["equality_constraints"],
observables=data["observables"],
)
print(f"Created result: {result}")
problem_metadata = ProblemMetadata(
name="ZDT1-like Problem",
description="Synthetic multi-objective test problem",
solver="Random Sampling",
n_design_variables=3,
n_objectives=2,
n_inequality_constraints=2,
n_equality_constraints=1,
n_evaluations=100,
solver_version=None,
computation_time=None,
)
result = OptimizationResult(
problem_metadata=problem_metadata,
design_variables=data["design_variables"],
objectives=data["objectives"],
inequality_constraints=data["inequality_constraints"],
equality_constraints=data["equality_constraints"],
observables=data["observables"],
)
print(f"Created result: {result}")
Created result: OptimizationResult(problem='ZDT1-like Problem', n_points=100, n_objectives=2, n_design_vars=3)
3. Add Variable Metadata¶
Adding metadata helps in understanding the variables and is useful for visualization (e.g., units, bounds).
In [4]:
Copied!
# Design variables
result.add_variable_metadata(
DesignVariable(
name="x1",
description="First design variable",
units="m",
lower_bound=0.0,
upper_bound=10.0,
)
)
result.add_variable_metadata(
DesignVariable(
name="x2",
description="Second design variable",
units="m",
lower_bound=0.0,
upper_bound=10.0,
)
)
result.add_variable_metadata(
DesignVariable(
name="x3",
description="Third design variable",
units="m",
lower_bound=0.0,
upper_bound=10.0,
)
)
# Objectives
result.add_variable_metadata(
Objective(
name="f1_minimize",
description="First objective function",
direction=OptimizationDirection.MINIMIZE,
ideal_value=float(data["objectives"]["f1_minimize"].min()),
nadir_value=float(data["objectives"]["f1_minimize"].max()),
)
)
result.add_variable_metadata(
Objective(
name="f2_minimize",
description="Second objective function",
direction=OptimizationDirection.MINIMIZE,
ideal_value=float(data["objectives"]["f2_minimize"].min()),
nadir_value=float(data["objectives"]["f2_minimize"].max()),
)
)
print(f"Added metadata for {len(result._variable_metadata)} variables")
# Design variables
result.add_variable_metadata(
DesignVariable(
name="x1",
description="First design variable",
units="m",
lower_bound=0.0,
upper_bound=10.0,
)
)
result.add_variable_metadata(
DesignVariable(
name="x2",
description="Second design variable",
units="m",
lower_bound=0.0,
upper_bound=10.0,
)
)
result.add_variable_metadata(
DesignVariable(
name="x3",
description="Third design variable",
units="m",
lower_bound=0.0,
upper_bound=10.0,
)
)
# Objectives
result.add_variable_metadata(
Objective(
name="f1_minimize",
description="First objective function",
direction=OptimizationDirection.MINIMIZE,
ideal_value=float(data["objectives"]["f1_minimize"].min()),
nadir_value=float(data["objectives"]["f1_minimize"].max()),
)
)
result.add_variable_metadata(
Objective(
name="f2_minimize",
description="Second objective function",
direction=OptimizationDirection.MINIMIZE,
ideal_value=float(data["objectives"]["f2_minimize"].min()),
nadir_value=float(data["objectives"]["f2_minimize"].max()),
)
)
print(f"Added metadata for {len(result._variable_metadata)} variables")
Added metadata for 5 variables
4. Create Result Sets¶
We can define sets of solutions based on criteria, such as feasibility or Pareto optimality.
In [5]:
Copied!
# Find feasible solutions (all constraints satisfied)
g1_values = data["inequality_constraints"]["g1_sum_x1_x2"]
g2_values = data["inequality_constraints"]["g2_sum_x1_x3"]
h1_values = data["equality_constraints"]["h1_product"]
# Check feasibility (inequality <= 0, equality ~ 0 with tolerance)
feasible_mask = (
(g1_values <= 0) & (g2_values <= 0) & (np.abs(h1_values) <= 10.0)
) # relaxed tolerance
feasible_indices = feasible_mask[feasible_mask].index.tolist()
feasible_set = result.create_set(
name="feasible",
indices=feasible_indices,
created_by="constraint_filter",
description="Solutions satisfying all constraints",
color="#2ecc71",
)
print(f"Created 'feasible' set: {len(feasible_set)} solutions")
# Simple Pareto front identification
f1 = data["objectives"]["f1_minimize"].values
f2 = data["objectives"]["f2_minimize"].values
pareto_indices = []
for i in range(len(f1)):
dominated = False
for j in range(len(f1)):
if i != j and f1[j] <= f1[i] and f2[j] <= f2[i] and (f1[j] < f1[i] or f2[j] < f2[i]):
dominated = True
break
if not dominated:
pareto_indices.append(i)
pareto_set = result.create_set(
name="pareto_front",
indices=pareto_indices,
created_by="pareto_detection",
description="Non-dominated solutions",
color="#e74c3c",
)
print(f"Created 'pareto_front' set: {len(pareto_set)} solutions")
# Create intersection: feasible Pareto solutions
feasible_pareto = feasible_set.intersection(pareto_set, name="feasible_pareto")
result._set_manager.add_set(feasible_pareto)
print(f"Created 'feasible_pareto' set: {len(feasible_pareto)} solutions")
# Find feasible solutions (all constraints satisfied)
g1_values = data["inequality_constraints"]["g1_sum_x1_x2"]
g2_values = data["inequality_constraints"]["g2_sum_x1_x3"]
h1_values = data["equality_constraints"]["h1_product"]
# Check feasibility (inequality <= 0, equality ~ 0 with tolerance)
feasible_mask = (
(g1_values <= 0) & (g2_values <= 0) & (np.abs(h1_values) <= 10.0)
) # relaxed tolerance
feasible_indices = feasible_mask[feasible_mask].index.tolist()
feasible_set = result.create_set(
name="feasible",
indices=feasible_indices,
created_by="constraint_filter",
description="Solutions satisfying all constraints",
color="#2ecc71",
)
print(f"Created 'feasible' set: {len(feasible_set)} solutions")
# Simple Pareto front identification
f1 = data["objectives"]["f1_minimize"].values
f2 = data["objectives"]["f2_minimize"].values
pareto_indices = []
for i in range(len(f1)):
dominated = False
for j in range(len(f1)):
if i != j and f1[j] <= f1[i] and f2[j] <= f2[i] and (f1[j] < f1[i] or f2[j] < f2[i]):
dominated = True
break
if not dominated:
pareto_indices.append(i)
pareto_set = result.create_set(
name="pareto_front",
indices=pareto_indices,
created_by="pareto_detection",
description="Non-dominated solutions",
color="#e74c3c",
)
print(f"Created 'pareto_front' set: {len(pareto_set)} solutions")
# Create intersection: feasible Pareto solutions
feasible_pareto = feasible_set.intersection(pareto_set, name="feasible_pareto")
result._set_manager.add_set(feasible_pareto)
print(f"Created 'feasible_pareto' set: {len(feasible_pareto)} solutions")
Created 'feasible' set: 23 solutions Created 'pareto_front' set: 8 solutions Created 'feasible_pareto' set: 3 solutions
5. Save and Load Results¶
We can save the results to JSON or CSV and load them back.
In [6]:
Copied!
output_dir = Path("example_output")
output_dir.mkdir(exist_ok=True)
# Save as JSON (preserves all metadata)
json_path = output_dir / "results.json"
save_results(result, json_path)
print(f"Saved JSON: {json_path}")
# Save as CSV (data only, metadata in sidecar)
csv_path = output_dir / "results.csv"
save_results(result, csv_path)
print(f"Saved CSV: {csv_path}")
# Load results back
loaded_result = load_results(json_path)
print(f"Loaded: {loaded_result}")
print(f"Sets preserved: {loaded_result.list_sets()}")
output_dir = Path("example_output")
output_dir.mkdir(exist_ok=True)
# Save as JSON (preserves all metadata)
json_path = output_dir / "results.json"
save_results(result, json_path)
print(f"Saved JSON: {json_path}")
# Save as CSV (data only, metadata in sidecar)
csv_path = output_dir / "results.csv"
save_results(result, csv_path)
print(f"Saved CSV: {csv_path}")
# Load results back
loaded_result = load_results(json_path)
print(f"Loaded: {loaded_result}")
print(f"Sets preserved: {loaded_result.list_sets()}")
Saved JSON: example_output/results.json Saved CSV: example_output/results.csv Loaded: OptimizationResult(problem='ZDT1-like Problem', n_points=100, n_objectives=2, n_design_vars=3) Sets preserved: ['feasible', 'pareto_front', 'feasible_pareto']
6. Access Data from Sets¶
We can easily access the data corresponding to a specific set.
In [7]:
Copied!
pareto_data = loaded_result.get_set_data("pareto_front")
print(f"Pareto front data shape: {pareto_data.shape}")
print(f"Columns: {list(pareto_data.columns)}")
# Summary statistics for Pareto front
pareto_objectives = loaded_result.objectives.iloc[loaded_result.get_set("pareto_front").indices]
print("\nPareto front objective statistics:")
print(pareto_objectives.describe())
pareto_data = loaded_result.get_set_data("pareto_front")
print(f"Pareto front data shape: {pareto_data.shape}")
print(f"Columns: {list(pareto_data.columns)}")
# Summary statistics for Pareto front
pareto_objectives = loaded_result.objectives.iloc[loaded_result.get_set("pareto_front").indices]
print("\nPareto front objective statistics:")
print(pareto_objectives.describe())
Pareto front data shape: (8, 10)
Columns: ['x1', 'x2', 'x3', 'f1_minimize', 'f2_minimize', 'g1_sum_x1_x2', 'g2_sum_x1_x3', 'h1_product', 'total_mass', 'efficiency']
Pareto front objective statistics:
f1_minimize f2_minimize
count 8.000000 8.000000
mean 2.040937 21.930684
std 2.816294 20.587853
min 0.055221 2.598385
25% 0.390666 3.620737
50% 0.663171 17.772207
75% 2.399744 33.904509
max 7.080726 61.196328