Problems
This chapter introduces the MTOP (Multitask Optimization Problem) class, the core component of D²MTOLab for defining and managing multitask optimization problems.
MTOP Class Overview
The MTOP class provides the following key features:
Flexible Task Definition: Supports single/multiobjective, constrained/unconstrained tasks with varying dimensions
Automatic Vectorization: Wraps non-vectorized functions for batch evaluation
Unified Evaluation Mode: Pads evaluation results to uniform dimensions for algorithm processing
Selective Evaluation: Evaluates only specified objectives or constraints for complex scenarios
Default Boundaries: Uses [0, 1] as default search range when bounds are not specified
Declared Variable Types: Records whether each variable is a float, integer, binary, ordinal or categorical, defaulting to all-float
Initialization
Create an MTOP instance with optional unified evaluation mode:
from ddmtolab.Methods.mtop import MTOP
# Basic initialization
problem = MTOP()
# Enable unified evaluation mode
problem = MTOP(unified_eval_mode=True, fill_value=0.0)
Parameters:
unified_eval_mode(bool): Whether to pad all task results to maximum dimensions. Default: Falsefill_value(float): Value used for padding in unified mode. Default: 0.0
Adding Tasks
Using add_task Method
The add_task method is the primary way to add optimization tasks:
def add_task(
objective_func, # Objective function (required)
dim, # Decision variable dimension (required)
constraint_func=None, # Constraint function (optional)
lower_bound=None, # Lower bounds (optional, default 0)
upper_bound=None, # Upper bounds (optional, default 1)
budget=None, # Evaluation budget of the task (optional)
metadata=None, # Free-form descriptor dict (optional)
var_type=None, # Type of each variable (optional, default all float)
var_values=None # Values a categorical or ordinal index stands for (optional)
) -> int: # Returns task index
Example: 2 Tasks with [1,3] Objectives and [2,0] Constraints:
import numpy as np
from ddmtolab.Methods.mtop import MTOP
# Task 1: Single-objective with 2 constraints
def sphere(x):
x = np.atleast_2d(x)
return np.sum(x**2, axis=1, keepdims=True)
def constraints_t1(x):
x = np.atleast_2d(x)
g1 = np.sum(x, axis=1) - 1
g2 = x[:, 0]**2 + x[:, 1]**2 - 0.5
return np.column_stack([g1, g2])
# Task 2: 3-objective without constraints
def multi_obj(x):
x = np.atleast_2d(x)
f1 = np.sum(x**2, axis=1)
f2 = np.sum((x - 1)**2, axis=1)
f3 = np.sum(np.abs(x), axis=1)
return np.column_stack([f1, f2, f3])
problem = MTOP()
problem.add_task(sphere, dim=3, constraint_func=constraints_t1,
lower_bound=[-5]*3, upper_bound=[5]*3)
problem.add_task(multi_obj, dim=2, lower_bound=[-1]*2, upper_bound=[1]*2)
print(problem)
Output:
MTOP with 2 tasks:
Unified eval mode: False (fill_value=0.0)
Max number of objectives (n_objs_max): 3
Max number of constraints (n_cons_max): 2
Task 0: dim=3, n_objs=1, n_cons=2, bounds=[-5.0..5.0]
Task 1: dim=2, n_objs=3, n_cons=0, bounds=[-1.0..1.0]
Declaring Variable Types
Every variable is a float unless the task says otherwise. It can instead give
one code per variable, F float, I integer, B binary, O ordinal
or C categorical, and record what a categorical or ordinal index stands
for:
problem.add_task(objective, dim=8,
lower_bound=[0]*8, upper_bound=[1, 1, 1, 10, 10, 10, 4, 4],
var_type=[('float', 3), ('integer', 3), ('categorical', 2)],
var_values={6: ['tanh', 'relu', 'sigmoid', 'sin', 'swish'],
7: ['tanh', 'relu', 'sigmoid', 'sin', 'swish']})
info = problem.get_task_info(0)
info['var_type'] # array(['F','F','F','I','I','I','C','C'], dtype='<U1')
info['var_values'][6] # ('tanh', 'relu', 'sigmoid', 'sin', 'swish')
The declaration is descriptive: nothing rounds or repairs a value because of it, so it never changes what a problem evaluates to. See the Methods chapter for every accepted spelling.
Batch Adding with Tuples
Add multiple tasks at once using tuple arguments:
indices = problem.add_task(
objective_func=(obj1, obj2, obj3),
dim=(3, 4, 5),
constraint_func=(con1, con2, con3),
lower_bound=([-1]*3, [-2]*4, [-3]*5),
upper_bound=([1]*3, [2]*4, [3]*5)
)
Using add_tasks Method
Add tasks from configuration dictionaries:
tasks_config = [
{
'objective_func': obj1,
'dim': 3,
'constraint_func': con1,
'lower_bound': [-1]*3,
'upper_bound': [1]*3
},
{
'objective_func': obj2,
'dim': 4,
'constraint_func': con2,
'lower_bound': [-2]*4,
'upper_bound': [2]*4
}
]
problem.add_tasks(tasks_config)
Task Evaluation
Single Task Evaluation
Evaluate individual tasks with evaluate_task:
# Evaluate task 0
X0 = np.array([[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6]]) # 2 solutions
objs, cons = problem.evaluate_task(0, X0)
print(f"Objectives shape: {objs.shape}") # (2, n_objs)
print(f"Constraints shape: {cons.shape}") # (2, n_cons)
Selective Evaluation
Evaluate only specific objectives or constraints. The eval_objectives and eval_constraints parameters support:
True: Evaluate all (default)False: Skip evaluation, return empty arrayint: Evaluate single objective/constraint at specified indexList[int]: Evaluate multiple objectives/constraints at specified indices
Example:
# Evaluate only objectives
objs, cons = problem.evaluate_task(0, X, eval_objectives=True,
eval_constraints=False)
# Evaluate specific objective indices
objs, cons = problem.evaluate_task(0, X, eval_objectives=[0, 2],
eval_constraints=False)
# Evaluate single constraint
objs, cons = problem.evaluate_task(0, X, eval_objectives=False,
eval_constraints=0)
Multitask Evaluation
Evaluate multiple tasks simultaneously with evaluate_tasks:
# Prepare input data for each task
X0 = np.random.uniform(-1, 1, (4, 3)) # Task 0: 4 solutions
X1 = np.random.uniform(-2, 2, (3, 4)) # Task 1: 3 solutions
X2 = np.random.uniform(-3, 3, (5, 5)) # Task 2: 5 solutions
# Evaluate all tasks
objs_list, cons_list = problem.evaluate_tasks(
task_indices=[0, 1, 2],
X_list=[X0, X1, X2]
)
for i, (objs, cons) in enumerate(zip(objs_list, cons_list)):
print(f"Task {i}: objs shape={objs.shape}, cons shape={cons.shape}")
Multitask Selective Evaluation
Apply different evaluation settings to each task:
# Unified mode: same settings for all tasks
objs_list, cons_list = problem.evaluate_tasks(
task_indices=[0, 1, 2],
X_list=[X0, X1, X2],
eval_objectives=True, # All tasks evaluate objectives
eval_constraints=False # All tasks skip constraints
)
# Per-task mode: different settings per task
objs_list, cons_list = problem.evaluate_tasks(
task_indices=[0, 1, 2],
X_list=[X0, X1, X2],
eval_objectives=[True, [0], [0, 2]], # Task 0: all, Task 1: first, Task 2: first and third
eval_constraints=[False, True, 0] # Task 0: skip, Task 1: all, Task 2: first
)
Unified Evaluation Mode
When tasks have different numbers of objectives or constraints, unified evaluation mode pads all results to maximum dimensions:
# Check current dimensions
print(f"Max objectives: {problem.n_objs}")
print(f"Max constraints: {problem.n_cons}")
# Enable unified evaluation mode
problem.set_unified_eval_mode(enabled=True, fill_value=0.0)
X0 = np.array([[0.1, 0.2, 0.3]])
X1 = np.array([[0.1, 0.2, 0.3, 0.4]])
X2 = np.array([[0.1, 0.2, 0.3, 0.4, 0.5]])
print("\nWith unified evaluation mode:")
for i, X in enumerate([X0, X1, X2]):
objs, cons = problem.evaluate_task(i, X)
print(f" Task {i}: objs shape={objs.shape}, cons shape={cons.shape}")
# Disable unified evaluation mode
problem.set_unified_eval_mode(enabled=False)
Automatic Vectorization
MTOP automatically handles batch evaluation of functions. If a function raises an exception with batch input, it switches to row-by-row evaluation:
# Non-vectorized function: only handles single row
def sphere_single_row(x):
if x.ndim > 1:
raise ValueError("Only handles 1D input")
return np.sum(x**2)
problem = MTOP()
idx = problem.add_task(sphere_single_row, dim=3)
X_batch = np.array([[0.1, 0.2, 0.3],
[0.4, 0.5, 0.6],
[0.7, 0.8, 0.9]])
obj, _ = problem.evaluate_task(idx, X_batch)
print(f"Batch evaluation result:\n{obj}")
Important: Automatic vectorization relies on functions raising exceptions for incompatible inputs. It is strongly recommended to write vectorized functions directly:
# Correct vectorized function
def sphere_vectorized(x):
x = np.atleast_2d(x)
return np.sum(x**2, axis=1, keepdims=True)
Declaring a Budget and Metadata
A task can carry its own evaluation budget \(N^k\) and a free-form descriptor, so the problem definition states how expensive it is instead of every call site repeating it:
problem = MTOP()
problem.add_task(
expensive_simulation, dim=10,
budget=200, # this task affords 200 evaluations
metadata={'source': 'CFD', 'fidelity': 'high'}
)
Both broadcast across tasks exactly the way dim does – a single value
applies to every task, a tuple gives one value per task:
problem.add_task((cheap_model, expensive_model), dim=(10, 10),
budget=(1000, 100)) # per-task budgets
An algorithm reads the declared budget through resolve_budget, which is
already wired into the algorithms that support it. An explicitly passed
max_nfes always wins, so existing calls behave exactly as before; the
declared budget is consulted only when the caller leaves max_nfes unset:
from ddmtolab.Algorithms.MTMO.ParEGO_KT import ParEGO_KT
ParEGO_KT(problem).optimize() # uses the declared budget
ParEGO_KT(problem, max_nfes=150).optimize() # explicit argument wins
metadata is never populated or interpreted by the platform; it is stored
per task and returned by get_task_info for the caller’s own bookkeeping.
Budgets must be positive integers and metadata must be a dict, both validated
when the task is added.
Querying Task Information
Basic Properties
Query basic problem properties:
# Number of tasks
print(f"Number of tasks: {problem.n_tasks}")
# Dimensions per task
print(f"Task dimensions: {problem.dims}")
# Objectives per task
print(f"Objectives per task: {problem.n_objs}")
# Constraints per task
print(f"Constraints per task: {problem.n_cons}")
# Unified evaluation mode status
print(f"Unified eval mode: {problem.unified_eval_mode}")
print(f"Fill value: {problem.fill_value}")
# Declared evaluation budgets and descriptors (None / {} when undeclared)
print(f"Task budgets: {problem.budgets}")
print(f"Task metadata: {problem.metadata}")
Detailed Task Information
Get complete information for a specific task:
info = problem.get_task_info(1)
print(info)
Output:
{'dimension': 4, 'n_objectives': 2, 'n_constraints': 2,
'lower_bounds': array([[-2., -2., -2., -2.]]),
'upper_bounds': array([[2., 2., 2., 2.]]),
'objective_func': <function obj2>,
'constraint_funcs': [<function wrapper>],
'budget': None,
'metadata': {}}
budget is the evaluation budget the task declared, or None when it
declared none, and metadata is the descriptor supplied when the task was
added, or {}. See Declaring a Budget and Metadata.
Problem Summary
Print a summary of the entire problem:
print(problem)
Output:
MTOP with 3 tasks:
Unified eval mode: False (fill_value=0.0)
Max number of objectives (n_objs_max): 3
Max number of constraints (n_cons_max): 3
Task 0: dim=3, n_objs=1, n_cons=3, bounds=[-1.0..1.0]
Task 1: dim=4, n_objs=2, n_cons=2, bounds=[-2.0..2.0]
Task 2: dim=5, n_objs=3, n_cons=1, bounds=[-3.0..3.0]
MTOP Method Summary
Category |
Method |
Description |
|---|---|---|
Initialization |
|
Initialize MTOP instance with optional unified evaluation mode |
|
Dynamically set unified evaluation mode |
|
Task Addition |
|
Add single or multiple tasks (supports tuple form) |
|
Add tasks from configuration dictionary list |
|
Task Evaluation |
|
Evaluate single task with selective evaluation support |
|
Batch evaluate multiple tasks with selective evaluation |
|
Information Query |
|
Get detailed information dictionary for specified task |
Properties |
|
Total number of tasks |
|
List of dimensions per task |
|
|
List of bounds per task |
|
|
List of objective counts per task |
|
|
List of constraint counts per task |
|
|
Unified evaluation mode status |
|
|
Fill value setting |
Important Notes
When using the MTOP class, note the following:
Function Design: Both objective and constraint functions should:
Handle 2D array input with shape (n, D)
Return 2D arrays with shape (n, M) for objectives or (n, C) for constraints
Use
np.atleast_2d(x)to ensure 2D inputCompute along
axis=1to keep rows independentConstraints follow the form g(x) ≤ 0 (feasible when ≤ 0)
Boundary Settings: If
lower_boundorupper_boundare not specified, default values of 0 and 1 are used. Explicitly specify search boundaries based on your problem.Unified Evaluation Mode: Enabling this mode pads all task results to the same dimensions, useful for multitask algorithms but increases memory overhead.
Selective Evaluation: In complex data-driven optimization scenarios where evaluation costs vary across tasks/objectives/constraints, use selective evaluation based on actual data availability.
Complete Problem Definition Example
For maintainability and platform compatibility, optimization problems should be encapsulated as Python classes. Problem files should be stored in ./Problems/ subdirectories by type:
STSO: Single-task single-objectiveSTMO: Single-task multiobjectiveMTSO: Multitask single-objectiveMTMO: Multitask multiobjectiveRWO: Real-world optimization problems
Problem Class Definition
Example: CEC17MTMO Problem 1:
import numpy as np
from ddmtolab.Methods.mtop import MTOP
class CEC17MTMO:
"""CEC2017 Multitask Multiobjective Test Suite"""
def __init__(self, mat_dir='../Problems/MTMO/data_cec17mtmo'):
"""Initialize problem class with data file path"""
self.mat_dir = mat_dir
def P1(self):
"""Problem 1: 2 tasks with different objectives"""
dim = 50
def T1(x):
# Task 1: 2 objectives
x = np.atleast_2d(x)
q = 1.0 + np.sum(x[:, 1:] ** 2, axis=1)
x1 = x[:, 0]
f1 = q * np.cos(np.pi * x1 / 2)
f2 = q * np.sin(np.pi * x1 / 2)
return np.vstack([f1, f2]).T
def T2(x):
# Task 2: 2 objectives
x = np.atleast_2d(x)
q = 1.0 + 9.0 / (dim - 1) * np.sum(np.abs(x[:, 1:]), axis=1)
x1 = x[:, 0]
f1 = x1
f2 = q * (1.0 - (x1 / q) ** 2)
return np.vstack([f1, f2]).T
# Define search space: x1∈[0,1], other variables∈[-100,100]
lb = np.array([0.0] + [-100.0] * (dim - 1))
ub = np.array([1.0] + [100.0] * (dim - 1))
# Create and configure multitask optimization problem
problem = MTOP()
problem.add_task(T1, dim=dim, lower_bound=lb, upper_bound=ub)
problem.add_task(T2, dim=dim, lower_bound=lb, upper_bound=ub)
return problem
Metric Configuration
For problems requiring complex metrics (e.g., multiobjective optimization), provide configuration for metric calculation:
def P1_T1_PF(N, M):
"""Generate true Pareto front for Task 1"""
theta = np.linspace(0, np.pi / 2, N)
f1 = np.cos(theta)
f2 = np.sin(theta)
return np.column_stack([f1, f2])
def P1_T2_PF(N, M):
"""Generate true Pareto front for Task 2"""
f1 = np.linspace(0, 1, N)
f2 = 1 - f1 ** 2
return np.column_stack([f1, f2])
# Metric configuration dictionary
SETTINGS = {
'metric': 'IGD', # Performance metric type
'n_ref': 10000, # Number of reference points to sample
'ref_path': './MOReference', # Directory of pre-stored reference files (optional)
'P1': {'T1': P1_T1_PF, 'T2': P1_T2_PF} # PF generators per task
}
Implementation Guidelines
Problem Class Structure
Class name should clearly reflect problem source (e.g.,
CEC17MTMO)Each problem instance method (e.g.,
P1,P2) is annotated-> MTOPand returns a configured MTOP object. The annotation is the contract problem discovery keys off: a public method without it is treated as a helper, not a benchmark problemConfigurable sizes always use the standard parameter names:
D(decision variables),M(objectives),K(tasks),Kp(WFG position parameters)Task functions support batch computation using
np.atleast_2d
Configuration Dictionary
metric: Performance metric type (required), e.g.,'IGD','HV'n_ref: Number of reference points to sample from a PF generator (optional, default 10000)ref_path: Directory holding pre-stored reference files, used when a reference is given as a file name (optional, default'./MOReference')Problem-task mapping: Nested dictionary structure
{'P#': {'T#': reference_info}}
File Organization
Recommended: Write problem class, reference generators, and SETTINGS in the same file
Alternative: Create separate
problem_settings.pyfor complex configurationsFile naming: lowercase with underscores (e.g.,
cec17_mtmo.py)Class naming: UpperCamelCase (e.g.,
CEC17MTMO)
Benchmark Problems
D²MTOLab ships 28 problem suites holding 211 benchmark problems. Every
problem is a method annotated -> MTOP that returns a fully configured MTOP
instance; D, M, K and Kp in the tables below are the parameter
names those methods accept.
Single-Task Single-Objective (STSO)
Suite |
Problems |
Tasks |
Dim |
Obj / Con |
Description |
|---|---|---|---|---|---|
|
9 |
1 |
D |
1 / 0 |
Classical landscapes (Ackley, Elliptic, Griewank, Rastrigin, Rosenbrock, Schwefel, Schwefel 2.22, Sphere, Weierstrass), unrotated and unshifted |
|
9 |
1 |
D |
1 / 0 |
The same classical landscapes, rotated and shifted so the optimum is not at the origin |
|
18 |
1 |
10-30 |
1 / 2-5 |
CEC 2010 constrained real-parameter optimization (P1-P18); D is capped at 30 by the shipped offset vectors |
Usage Example:
from ddmtolab.Problems.STSO import CLASSICALSO
problem = CLASSICALSO().P1(D=30) # 30-dimensional Ackley
Single-Task Multi-Objective (STMO)
Suite |
Problems |
Tasks |
Dim |
Obj / Con |
Description |
|---|---|---|---|---|---|
|
6 |
1 |
D |
2 / 0 |
ZDT suite (ZDT1-ZDT6): convex, non-convex, disconnected and multi-modal Pareto fronts |
|
9 |
1 |
D |
M / 0-M |
DTLZ suite (DTLZ1-DTLZ9), scalable in M and D; DTLZ8-DTLZ9 are constrained. Default D = M + k - 1 |
|
9 |
1 |
D |
M / 0 |
WFG suite (WFG1-WFG9), scalable in M, D and the position parameter Kp. Default D = Kp + 10 |
|
10 |
1 |
D |
2-3 / 0 |
CEC 2009 unconstrained suite (UF1-UF10): complicated Pareto sets |
|
10 |
1 |
D |
2-3 / 1-2 |
CEC 2009 constrained suite (CF1-CF10); CF6 and CF7 carry two constraints |
|
14 |
1 |
D |
2-3 / 1-4 |
Ma-Wang constrained suite (MW1-MW14): challenging constraint boundaries |
Usage Example:
from ddmtolab.Problems.STMO import ZDT, DTLZ
problem = ZDT().ZDT1(D=30) # 30 decision variables
problem = DTLZ().DTLZ2(M=5) # 5 objectives, D defaults to M + k - 1
# the reference Pareto fronts live next to the suite
from ddmtolab.Problems.STMO.DTLZ import SETTINGS
Multi-Task Single-Objective (MTSO)
Suite |
Problems |
Tasks |
Dim |
Obj / Con |
Description |
|---|---|---|---|---|---|
|
9 |
2 |
25-50 |
1 / 0 |
CEC 2017 EMTO competition (P1-P9): complete/partial/no intersection at high/medium/low similarity. Task 2 of P6 is 25-dimensional |
|
9 |
2 |
10 |
1 / 0 |
10-dimensional variant of CEC17MTSO, for expensive / surrogate-assisted studies |
|
6 |
K |
50 |
1 / 0 |
CEC 2019 many-task optimization: K tasks generated from one landscape by rotation and shift |
|
9 |
2 |
D |
1 / 1-2 |
Constrained multi-task benchmark (CMT1-CMT9); task 2 of CMT6 and CMT8 carries two constraints |
|
4 |
3-5 |
10 |
1 / 0 |
Compact many-task problems (P1-P4) with 3 or 5 tasks and varying similarity |
|
12 |
K |
25-50 |
1 / 0 |
Scalable test problem generator for sequential transfer optimization (STOP1-STOP12) |
Usage Example:
from ddmtolab.Problems.MTSO import CEC17MTSO
problem = CEC17MTSO().P1() # CI-HS: complete intersection, high similarity
print(problem.n_tasks, problem.dims)
Multi-Task Multi-Objective (MTMO)
Suite |
Problems |
Tasks |
Dim |
Obj / Con |
Description |
|---|---|---|---|---|---|
|
9 |
2 |
10-50 |
2-3 / 0 |
CEC 2017 MTMO competition (P1-P9): bi-objective and tri-objective task pairs |
|
10 |
2 |
10-30 |
2-3 / 0 |
CEC 2019 CPLX benchmark (P1-P10), LZ09-based, with complicated Pareto sets |
|
6 |
K |
10-50 |
2 / 0 |
CEC 2019 many-task multi-objective benchmark (P1-P6); P3 is 10-dimensional |
|
10 |
2 |
50 |
2 / 0 |
CEC 2021 MTMO benchmark (P1-P10) |
|
2 |
2 |
10 |
2 / 0-1 |
Compact ZDT4-based instances (P1-P2); P2 is constrained |
Usage Example:
from ddmtolab.Problems.MTMO import CEC17MTMO
from ddmtolab.Problems.MTMO.cec17_mtmo import SETTINGS
problem = CEC17MTMO().P8() # T1: 3-objective, T2: 2-objective
print(problem.n_objs)
Real-World Optimization (RWO)
Suite |
Problems |
Tasks |
Dim |
Obj / Con |
Description |
|---|---|---|---|---|---|
|
1 |
3 |
5-7 |
1 / 0 |
Parameter extraction of photovoltaic models: single-diode, double-diode and PV-module tasks |
|
2 |
3 |
25-30 |
2 / 24-29 |
Synchronous optimal pulse-width modulation: harmonic distortion vs switching loss, monotone-angle constraints |
|
6 |
1 |
101-1867 |
1 / 0 |
Neural network weight optimization by neuroevolution on scikit-learn datasets |
|
6 |
1 |
20-200 |
1 / 0 |
Traveling salesman problem via random-keys encoding (20-200 cities) |
|
1 |
11 |
75-105 |
1 / 0 |
Sensor coverage problem: one task per sensor count, so tasks differ in dimension |
|
2 |
4-5 |
75-102 |
2 / 0 |
Multi-objective sensor coverage problem: coverage vs deployment cost |
|
1 |
K |
D |
1 / 0 |
Planar kinematic arm control: reach a target with a D-joint arm, K tasks with different arm geometries |
|
12 |
2-4 |
6 |
1 / 0 |
Hyperparameter optimization of physics-informed neural networks (convection, reaction, wave, Helmholtz) |
Usage Example:
from ddmtolab.Problems.RWO import PEPVM
problem = PEPVM().P1() # 3 photovoltaic model-fitting tasks
print(problem.n_tasks, problem.dims)
See Also
Algorithms - Algorithm implementation guide
API Reference - Complete API documentation