Skip to content

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:

Python
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:

Python
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:

Python
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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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

Python
# 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

Python
# Get all variable names from first sample
if solution.samples:
    var_names = solution.samples[0].keys()
    print(f"Variables: {list(var_names)}")

Solution Quality

Python
# 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:

Python
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:

Python
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:

Python
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:

Python
# 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:

Python
# 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:

Python
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:

Python
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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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:

Python
# 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.

Python
# 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

Python
# Export solution data
solution_dict = {
    "samples": solution.samples,
    "obj_values": solution.obj_values,
    "feasible": solution.feasible,
    "num_solutions": len(solution.samples)
}

To DataFrame

Python
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

Python
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:

Python
# 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

Python
# 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

Python
# 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