Working with Solutions
Solutions represent the results of solving an optimization problem. LunaModel provides a comprehensive Solution class to store, analyze, and manipulate solver results.
Overview
A Solution in LunaModel contains:
- Samples: Variable assignments (one or more)
- Objective values: The objective function value for each sample
- Feasibility: Whether each sample satisfies all constraints
- Metadata: Additional information like solving time, solver name, etc.
Creating Solutions
From Solver Results
Typically, solutions come from solving a model with an external solver:
from luna_model import Model, Solution
# Create and solve a model (pseudo-code)
model = Model()
# ... define model ...
# Solution would come from a solver
# This is a manual example for illustration
solution = Solution(
samples=[{"x": 1, "y": 0, "z": 1}],
obj_values=[42.0],
feasible=[True]
)
Manual Construction
Create solutions manually for testing or analysis:
from luna_model import Solution
# Single solution
solution = Solution(
samples=[{"x": 1.0, "y": 2.0}],
obj_values=[5.0],
feasible=[True]
)
# Multiple solutions
solution = Solution(
samples=[
{"x": 1, "y": 0},
{"x": 0, "y": 1},
{"x": 1, "y": 1}
],
obj_values=[3.0, 2.0, 5.0],
feasible=[True, True, False]
)
From Different Formats
Use translators to convert from solver-specific formats:
from luna_model.translator import NumpyTranslator, DwaveTranslator
# From NumPy array
numpy_translator = NumpyTranslator()
solution = numpy_translator.to_lm(result_array, variable_names)
# From D-Wave SampleSet
dwave_translator = DwaveTranslator()
solution = dwave_translator.to_lm(sampleset)
Accessing Solution Data
Samples
Access variable assignments:
# Get all samples
samples = solution.samples
print(samples) # [{"x": 1, "y": 0}, {"x": 0, "y": 1}]
# Get first sample
first_sample = solution.samples[0]
print(first_sample) # {"x": 1, "y": 0}
# Access specific variable in a sample
x_value = solution.samples[0]["x"]
print(x_value) # 1
Objective Values
Access objective function values:
# Get all objective values
obj_values = solution.obj_values
print(obj_values) # [3.0, 2.0, 5.0]
# Get objective for first sample
first_obj = solution.obj_values[0]
print(first_obj) # 3.0
Feasibility
Check which solutions are feasible:
# Check all feasibility flags
feasible = solution.feasible
print(feasible) # [True, True, False]
# Check if first solution is feasible
is_feasible = solution.feasible[0]
print(is_feasible) # True
# Count feasible solutions
num_feasible = sum(solution.feasible)
print(f"{num_feasible} feasible solutions")
Best Solutions
Get the best (optimal or near-optimal) solution:
# Get best sample (lowest objective for MIN, highest for MAX)
if solution.samples:
best_idx = solution.obj_values.index(min(solution.obj_values))
best_sample = solution.samples[best_idx]
print(best_sample)
Solution Properties
Number of Samples
# Get number of solutions
num_solutions = len(solution.samples)
print(f"Solution set contains {num_solutions} samples")
# Check if solution is empty
if len(solution.samples) == 0:
print("No solutions found")
Variable Names
# Get all variable names from first sample
if solution.samples:
var_names = solution.samples[0].keys()
print(f"Variables: {list(var_names)}")
Solution Quality
# Best objective value
best_obj = min(solution.obj_values) # For minimization
worst_obj = max(solution.obj_values)
# Objective value range
obj_range = worst_obj - best_obj
print(f"Objective range: {obj_range}")
# Average objective (for feasible solutions only)
feasible_objs = [obj for obj, feas in zip(solution.obj_values, solution.feasible) if feas]
if feasible_objs:
avg_obj = sum(feasible_objs) / len(feasible_objs)
print(f"Average objective: {avg_obj}")
Evaluating Solutions
Against a Model
Evaluate objective and check constraints:
from luna_model import Model
model = Model()
# ... define model ...
# Evaluate solution
result = model.evaluate(solution)
# Check specific sample
sample = solution.samples[0]
obj_value = model.objective.evaluate(sample)
print(f"Objective value: {obj_value}")
# Check all constraints
all_satisfied = all(
constraint.check(sample)
for constraint in model.constraints
)
print(f"All constraints satisfied: {all_satisfied}")
Individual Expressions
Evaluate any expression with a solution:
from luna_model import Expression
# Create an expression
expr = 3 * x + 2 * y + 5
# Evaluate with a sample (dict mapping variable names to values)
sample = {"x": 1, "y": 2}
value = expr.evaluate(sample)
print(f"Expression value: {value}") # 3(1) + 2(2) + 5 = 12
Constraint Violations
Identify violated constraints:
sample = solution.samples[0]
violations = []
for i, constraint in enumerate(model.constraints):
if not constraint.check(sample):
violations.append((i, constraint))
if violations:
print(f"Found {len(violations)} constraint violations:")
for idx, constraint in violations:
print(f" Constraint {idx}: {constraint}")
else:
print("All constraints satisfied")
Working with Multiple Solutions
Filtering Solutions
Filter solutions based on criteria:
# Get only feasible solutions
feasible_solutions = [
sample for sample, feas in zip(solution.samples, solution.feasible)
if feas
]
# Get solutions within threshold of best
best_obj = min(solution.obj_values)
threshold = 1.1 * best_obj # Within 10% of best
near_optimal = [
sample for sample, obj in zip(solution.samples, solution.obj_values)
if obj <= threshold
]
Sorting Solutions
Sort by objective value:
# Create list of (sample, objective, feasible) tuples
solution_data = list(zip(solution.samples, solution.obj_values, solution.feasible))
# Sort by objective (best first)
solution_data.sort(key=lambda x: x[1])
# Get top 5 solutions
top_5 = solution_data[:5]
for sample, obj, feas in top_5:
print(f"Objective: {obj}, Feasible: {feas}, Sample: {sample}")
Comparing Solutions
Compare two solutions:
sample1 = solution.samples[0]
sample2 = solution.samples[1]
# Check if identical
identical = sample1 == sample2
# Count differences
differences = sum(1 for var in sample1 if sample1[var] != sample2.get(var))
print(f"Number of different variables: {differences}")
# Hamming distance (for binary variables)
hamming = sum(1 for var in sample1 if sample1[var] != sample2.get(var, 0))
Solution Analysis
Statistics
Compute statistics over solution sets:
import statistics
# Objective statistics (feasible only)
feasible_objs = [
obj for obj, feas in zip(solution.obj_values, solution.feasible)
if feas
]
if feasible_objs:
print(f"Best: {min(feasible_objs)}")
print(f"Worst: {max(feasible_objs)}")
print(f"Mean: {statistics.mean(feasible_objs)}")
print(f"Median: {statistics.median(feasible_objs)}")
print(f"StdDev: {statistics.stdev(feasible_objs)}")
Variable Statistics
Analyze variable values across solutions:
# For a specific variable
x_values = [sample["x"] for sample in solution.samples]
print(f"x values: {x_values}")
print(f"x min: {min(x_values)}")
print(f"x max: {max(x_values)}")
print(f"x average: {sum(x_values) / len(x_values)}")
# Frequency for binary variables
x_frequency = sum(x_values) / len(x_values)
print(f"x is 1 in {x_frequency*100:.1f}% of solutions")
Diversity
Measure solution diversity:
# Count unique solutions
unique_solutions = len(set(
tuple(sorted(sample.items())) for sample in solution.samples
))
print(f"Unique solutions: {unique_solutions} / {len(solution.samples)}")
# Average pairwise distance (for binary variables)
def hamming_distance(s1, s2):
return sum(1 for k in s1 if s1[k] != s2.get(k, 0))
total_distance = 0
count = 0
for i in range(len(solution.samples)):
for j in range(i + 1, len(solution.samples)):
total_distance += hamming_distance(solution.samples[i], solution.samples[j])
count += 1
avg_distance = total_distance / count if count > 0 else 0
print(f"Average pairwise Hamming distance: {avg_distance:.2f}")
Solution Manipulation
Extracting Subsets
Create new solution objects from subsets:
# Extract top N solutions
N = 10
top_n_indices = sorted(range(len(solution.obj_values)),
key=lambda i: solution.obj_values[i])[:N]
top_n_solution = Solution(
samples=[solution.samples[i] for i in top_n_indices],
obj_values=[solution.obj_values[i] for i in top_n_indices],
feasible=[solution.feasible[i] for i in top_n_indices]
)
Merging Solutions
Combine multiple solution sets:
# Merge two solutions
combined_samples = solution1.samples + solution2.samples
combined_obj_values = solution1.obj_values + solution2.obj_values
combined_feasible = solution1.feasible + solution2.feasible
merged_solution = Solution(
samples=combined_samples,
obj_values=combined_obj_values,
feasible=combined_feasible
)
Filtering Variables
Extract solutions with only specific variables:
# Keep only certain variables
keep_vars = ["x", "y"]
filtered_samples = [
{var: value for var, value in sample.items() if var in keep_vars}
for sample in solution.samples
]
filtered_solution = Solution(
samples=filtered_samples,
obj_values=solution.obj_values,
feasible=solution.feasible
)
Result and Sample Classes
Solutions in LunaModel use simple Python dictionaries for samples. Each sample is a dictionary mapping variable names (strings) to their values.
# A sample is just a dictionary
sample = {"x": 1, "y": 0, "z": 1}
# Access values
x_val = sample["x"]
# Iterate over variables
for var_name, value in sample.items():
print(f"{var_name} = {value}")
Exporting Solutions
To Dictionary
# Export solution data
solution_dict = {
"samples": solution.samples,
"obj_values": solution.obj_values,
"feasible": solution.feasible,
"num_solutions": len(solution.samples)
}
To DataFrame
import pandas as pd
# Create DataFrame from samples
df = pd.DataFrame(solution.samples)
df["objective"] = solution.obj_values
df["feasible"] = solution.feasible
print(df)
To File
import json
# Save to JSON
with open("solution.json", "w") as f:
json.dump({
"samples": solution.samples,
"obj_values": solution.obj_values,
"feasible": solution.feasible
}, f, indent=2)
# Load from JSON
with open("solution.json", "r") as f:
data = json.load(f)
loaded_solution = Solution(
samples=data["samples"],
obj_values=data["obj_values"],
feasible=data["feasible"]
)
Best Practices
Checking Solutions
Always verify solution quality:
# Check feasibility
if solution.samples and not solution.feasible[0]:
print("Warning: Best solution is infeasible!")
# Verify objective value
if solution.samples:
computed_obj = model.objective.evaluate(solution.samples[0])
reported_obj = solution.obj_values[0]
if abs(computed_obj - reported_obj) > 1e-6:
print("Warning: Objective value mismatch!")
Handling Empty Solutions
# Check before accessing
if not solution.samples:
print("No solutions found")
else:
# Get best solution
best_idx = solution.obj_values.index(min(solution.obj_values))
best = solution.samples[best_idx]
print(f"Best solution: {best}")
Working with Large Solution Sets
# Process solutions in batches for memory efficiency
batch_size = 1000
for i in range(0, len(solution.samples), batch_size):
batch_samples = solution.samples[i:i+batch_size]
batch_objs = solution.obj_values[i:i+batch_size]
# Process batch...
Next Steps
- Translators: Solutions - Convert between solution formats
- API Reference: Solution - Detailed API documentation