Skip to content

Stopping Conditions

Stopping conditions for Xopt optimization.

This module contains classes that implement various stopping criteria for optimization processes. Each stopping condition class takes an Xopt dataframe and VOCS object to determine whether optimization should stop.

CompositeCondition

Bases: StoppingCondition

Combine multiple stopping conditions with AND/OR logic.

Parameters:

Name Type Description Default
conditions List[StoppingCondition]

List of stopping conditions to combine.

required
logic str

Logic to combine conditions: "and" or "or" (default: "or").

required
Source code in xopt/stopping_conditions.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
class CompositeCondition(StoppingCondition):
    """
    Combine multiple stopping conditions with AND/OR logic.

    Parameters
    ----------
    conditions : List[StoppingCondition]
        List of stopping conditions to combine.
    logic : str, optional
        Logic to combine conditions: "and" or "or" (default: "or").
    """

    name: Literal["CompositeCondition"] = "CompositeCondition"
    conditions: List["StoppingConditionUnion"] = Field(
        description="List of stopping conditions to combine", min_length=1
    )
    logic: str = Field(
        default="or", description="Logic to combine conditions: 'and' or 'or'"
    )

    @field_serializer("conditions")
    @classmethod
    def serialize_conditions(cls, v):
        serialized_conditions = []
        for condition in v:
            serialized_conditions.append(
                condition.model_dump() | {"name": condition.__class__.__name__}
            )
        return serialized_conditions

    @field_validator("logic")
    @classmethod
    def validate_logic(cls, v):
        if v.lower() not in ["and", "or"]:
            raise ValueError("logic must be 'and' or 'or'")
        return v.lower()

    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """Combine conditions using specified logic."""
        results = []

        for condition in self.conditions:
            _should_stop = condition.should_stop(data, vocs)
            if _should_stop and self.logic == "or":
                return True
            results.append(_should_stop)

        if self.logic == "and":
            return all(results)
        return False

should_stop(data, vocs)

Combine conditions using specified logic.

Source code in xopt/stopping_conditions.py
336
337
338
339
340
341
342
343
344
345
346
347
348
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """Combine conditions using specified logic."""
    results = []

    for condition in self.conditions:
        _should_stop = condition.should_stop(data, vocs)
        if _should_stop and self.logic == "or":
            return True
        results.append(_should_stop)

    if self.logic == "and":
        return all(results)
    return False

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)

ConvergenceCondition

Bases: StoppingCondition

Stop when optimization converges (improvement is below threshold).

Parameters:

Name Type Description Default
objective_name str

Name of the objective to monitor for convergence.

required
improvement_threshold float

Minimum improvement required to continue optimization.

required
patience int

Number of evaluations to wait without improvement before stopping.

required
relative bool

Whether to use relative improvement (default: False).

required
Source code in xopt/stopping_conditions.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
class ConvergenceCondition(StoppingCondition):
    """
    Stop when optimization converges (improvement is below threshold).

    Parameters
    ----------
    objective_name : str
        Name of the objective to monitor for convergence.
    improvement_threshold : float
        Minimum improvement required to continue optimization.
    patience : int
        Number of evaluations to wait without improvement before stopping.
    relative : bool, optional
        Whether to use relative improvement (default: False).
    """

    name: Literal["ConvergenceCondition"] = "ConvergenceCondition"
    objective_name: str = Field(description="Name of the objective to monitor")
    improvement_threshold: PositiveFloat = Field(
        description="Minimum improvement required to continue optimization"
    )
    patience: PositiveInt = Field(
        description="Number of evaluations to wait without improvement"
    )
    relative: bool = Field(
        default=False, description="Whether to use relative improvement"
    )

    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """Stop if no improvement for specified patience."""
        if data.empty or self.objective_name not in vocs.objectives:
            return False

        if self.objective_name not in data.columns:
            return False

        if len(data) < self.patience + 1:
            return False

        objective_type = vocs.objectives[self.objective_name]
        objective_values = data[self.objective_name].dropna()

        # Check improvement over the last 'patience' evaluations
        recent_values = objective_values.iloc[-(self.patience + 1) :]

        if isinstance(objective_type, MinimizeObjective):
            best_recent = recent_values.min()
            baseline = recent_values.iloc[0]
            improvement = baseline - best_recent
        else:  # MAXIMIZE
            best_recent = recent_values.max()
            baseline = recent_values.iloc[0]
            improvement = best_recent - baseline

        if self.relative and abs(baseline) > 1e-12:
            improvement = improvement / abs(baseline)

        return improvement < self.improvement_threshold

should_stop(data, vocs)

Stop if no improvement for specified patience.

Source code in xopt/stopping_conditions.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """Stop if no improvement for specified patience."""
    if data.empty or self.objective_name not in vocs.objectives:
        return False

    if self.objective_name not in data.columns:
        return False

    if len(data) < self.patience + 1:
        return False

    objective_type = vocs.objectives[self.objective_name]
    objective_values = data[self.objective_name].dropna()

    # Check improvement over the last 'patience' evaluations
    recent_values = objective_values.iloc[-(self.patience + 1) :]

    if isinstance(objective_type, MinimizeObjective):
        best_recent = recent_values.min()
        baseline = recent_values.iloc[0]
        improvement = baseline - best_recent
    else:  # MAXIMIZE
        best_recent = recent_values.max()
        baseline = recent_values.iloc[0]
        improvement = best_recent - baseline

    if self.relative and abs(baseline) > 1e-12:
        improvement = improvement / abs(baseline)

    return improvement < self.improvement_threshold

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)

FeasibilityCondition

Bases: StoppingCondition

Stop when a feasible solution is found.

Parameters:

Name Type Description Default
require_all_constraints bool

Whether all constraints must be satisfied (default: True).

required
Source code in xopt/stopping_conditions.py
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
class FeasibilityCondition(StoppingCondition):
    """
    Stop when a feasible solution is found.

    Parameters
    ----------
    require_all_constraints : bool, optional
        Whether all constraints must be satisfied (default: True).
    """

    name: Literal["FeasibilityCondition"] = "FeasibilityCondition"
    require_all_constraints: bool = Field(
        default=True, description="Whether all constraints must be satisfied"
    )

    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """Stop when a feasible solution is found."""
        if data.empty or not vocs.constraints:
            return False

        # Use VOCS to determine feasibility
        feasibility_data = get_feasibility_data(vocs, data)

        if self.require_all_constraints:
            # Stop if any point is fully feasible
            return feasibility_data["feasible"].any()
        else:
            # Stop if any individual constraint is satisfied
            constraint_columns = [
                col
                for col in feasibility_data.columns
                if col.startswith("feasible_") and col != "feasible"
            ]
            if constraint_columns:
                return feasibility_data[constraint_columns].any().any()

should_stop(data, vocs)

Stop when a feasible solution is found.

Source code in xopt/stopping_conditions.py
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """Stop when a feasible solution is found."""
    if data.empty or not vocs.constraints:
        return False

    # Use VOCS to determine feasibility
    feasibility_data = get_feasibility_data(vocs, data)

    if self.require_all_constraints:
        # Stop if any point is fully feasible
        return feasibility_data["feasible"].any()
    else:
        # Stop if any individual constraint is satisfied
        constraint_columns = [
            col
            for col in feasibility_data.columns
            if col.startswith("feasible_") and col != "feasible"
        ]
        if constraint_columns:
            return feasibility_data[constraint_columns].any().any()

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)

MaxEvaluationsCondition

Bases: StoppingCondition

Stop after a maximum number of evaluations. Evaluations can be counted in different ways based on parameters. If count_valid_only is True, only evaluations without errors are counted. If use_dataframe_index is True, the dataframe index is used to count evaluations instead of the number of rows.

Parameters:

Name Type Description Default
max_evaluations int

Maximum number of function evaluations before stopping.

required
count_valid_only bool

Whether to count only valid evaluations (default: False).

required
use_dataframe_index bool

Whether to use the dataframe index to count evaluations (default: False).

required
Source code in xopt/stopping_conditions.py
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
class MaxEvaluationsCondition(StoppingCondition):
    """
    Stop after a maximum number of evaluations. Evaluations can be counted
    in different ways based on parameters. If count_valid_only is True, only
    evaluations without errors are counted. If use_dataframe_index is True,
    the dataframe index is used to count evaluations instead of the number of rows.

    Parameters
    ----------
    max_evaluations : int
        Maximum number of function evaluations before stopping.
    count_valid_only : bool, optional
        Whether to count only valid evaluations (default: False).
    use_dataframe_index : bool, optional
        Whether to use the dataframe index to count evaluations (default: False).
    """

    name: Literal["MaxEvaluationsCondition"] = "MaxEvaluationsCondition"
    max_evaluations: PositiveInt = Field(
        description="Maximum number of function evaluations"
    )
    count_valid_only: bool = Field(
        default=False,
        description="Whether to count only valid evaluations that do not raise errors",
    )
    use_dataframe_index: bool = Field(
        default=False,
        description="Whether to use the dataframe index to count evaluations",
    )

    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """Stop if we've reached the maximum number of evaluations."""
        if data.empty:
            return False

        if self.use_dataframe_index:
            # assume index starts at 0
            return max(data.index) + 1 >= self.max_evaluations

        if self.count_valid_only:
            valid_data = data[data["error"].isna()] if "error" in data.columns else data
            return len(valid_data) >= self.max_evaluations

        return len(data) >= self.max_evaluations

should_stop(data, vocs)

Stop if we've reached the maximum number of evaluations.

Source code in xopt/stopping_conditions.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """Stop if we've reached the maximum number of evaluations."""
    if data.empty:
        return False

    if self.use_dataframe_index:
        # assume index starts at 0
        return max(data.index) + 1 >= self.max_evaluations

    if self.count_valid_only:
        valid_data = data[data["error"].isna()] if "error" in data.columns else data
        return len(valid_data) >= self.max_evaluations

    return len(data) >= self.max_evaluations

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)

StagnationCondition

Bases: StoppingCondition

Stop when the best objective value hasn't improved for a number of evaluations.

Parameters:

Name Type Description Default
objective_name str

Name of the objective to monitor.

required
patience int

Number of evaluations without improvement before stopping.

required
tolerance float

Minimum improvement considered significant (default: 1e-8).

required
Source code in xopt/stopping_conditions.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
class StagnationCondition(StoppingCondition):
    """
    Stop when the best objective value hasn't improved for a number of evaluations.

    Parameters
    ----------
    objective_name : str
        Name of the objective to monitor.
    patience : int
        Number of evaluations without improvement before stopping.
    tolerance : float, optional
        Minimum improvement considered significant (default: 1e-8).
    """

    name: Literal["StagnationCondition"] = "StagnationCondition"
    objective_name: str = Field(description="Name of the objective to monitor")
    patience: PositiveInt = Field(
        description="Number of evaluations without improvement before stopping"
    )
    tolerance: PositiveFloat = Field(
        default=1e-8, description="Minimum improvement considered significant"
    )

    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """Stop if no improvement in best value for specified patience."""
        if data.empty or self.objective_name not in vocs.objectives:
            return False

        if self.objective_name not in data.columns:
            return False

        if len(data) < self.patience + 1:
            return False

        objective_type = vocs.objectives[self.objective_name]
        objective_values = data[self.objective_name].dropna()

        # Track the best value seen so far
        if isinstance(objective_type, MinimizeObjective):
            cumulative_best = objective_values.cummin()
        else:  # MAXIMIZE
            cumulative_best = objective_values.cummax()

        recent_best = cumulative_best.iloc[-1]
        past_best = cumulative_best.iloc[-(self.patience + 1)]

        if isinstance(objective_type, MinimizeObjective):
            improvement = past_best - recent_best
        else:  # MAXIMIZE
            improvement = recent_best - past_best

        return improvement < self.tolerance

should_stop(data, vocs)

Stop if no improvement in best value for specified patience.

Source code in xopt/stopping_conditions.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """Stop if no improvement in best value for specified patience."""
    if data.empty or self.objective_name not in vocs.objectives:
        return False

    if self.objective_name not in data.columns:
        return False

    if len(data) < self.patience + 1:
        return False

    objective_type = vocs.objectives[self.objective_name]
    objective_values = data[self.objective_name].dropna()

    # Track the best value seen so far
    if isinstance(objective_type, MinimizeObjective):
        cumulative_best = objective_values.cummin()
    else:  # MAXIMIZE
        cumulative_best = objective_values.cummax()

    recent_best = cumulative_best.iloc[-1]
    past_best = cumulative_best.iloc[-(self.patience + 1)]

    if isinstance(objective_type, MinimizeObjective):
        improvement = past_best - recent_best
    else:  # MAXIMIZE
        improvement = recent_best - past_best

    return improvement < self.tolerance

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)

StoppingCondition

Bases: XoptBaseModel, ABC

Abstract base class for stopping conditions.

All stopping conditions must implement the should_stop method which takes an Xopt dataframe and VOCS object and returns True if optimization should stop.

Source code in xopt/stopping_conditions.py
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class StoppingCondition(XoptBaseModel, ABC):
    """
    Abstract base class for stopping conditions.

    All stopping conditions must implement the should_stop method which takes
    an Xopt dataframe and VOCS object and returns True if optimization should stop.
    """

    model_config = ConfigDict(validate_assignment=True, extra="forbid")

    @abstractmethod
    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """
        Determine if optimization should stop based on data and VOCS.

        Parameters
        ----------
        data : pd.DataFrame
            The Xopt optimization data containing variables, objectives, constraints, etc.
        vocs : VOCS
            The VOCS object defining variables, objectives, and constraints.

        Returns
        -------
        bool
            True if optimization should stop, False otherwise.
        """
        ...

should_stop(data, vocs) abstractmethod

Determine if optimization should stop based on data and VOCS.

Parameters:

Name Type Description Default
data DataFrame

The Xopt optimization data containing variables, objectives, constraints, etc.

required
vocs VOCS

The VOCS object defining variables, objectives, and constraints.

required

Returns:

Type Description
bool

True if optimization should stop, False otherwise.

Source code in xopt/stopping_conditions.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@abstractmethod
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """
    Determine if optimization should stop based on data and VOCS.

    Parameters
    ----------
    data : pd.DataFrame
        The Xopt optimization data containing variables, objectives, constraints, etc.
    vocs : VOCS
        The VOCS object defining variables, objectives, and constraints.

    Returns
    -------
    bool
        True if optimization should stop, False otherwise.
    """
    ...

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)

TargetValueCondition

Bases: StoppingCondition

Stop when an objective reaches a target value.

Parameters:

Name Type Description Default
objective_name str

Name of the objective to monitor.

required
target_value float

Target value for the objective.

required
tolerance float

Tolerance for reaching the target (default: 1e-6).

required
Source code in xopt/stopping_conditions.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
class TargetValueCondition(StoppingCondition):
    """
    Stop when an objective reaches a target value.

    Parameters
    ----------
    objective_name : str
        Name of the objective to monitor.
    target_value : float
        Target value for the objective.
    tolerance : float, optional
        Tolerance for reaching the target (default: 1e-6).
    """

    name: Literal["TargetValueCondition"] = "TargetValueCondition"
    objective_name: str = Field(description="Name of the objective to monitor")
    target_value: float = Field(description="Target value for the objective")
    tolerance: PositiveFloat = Field(
        default=1e-6, description="Tolerance for reaching the target"
    )

    def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
        """Stop if objective reaches target value within tolerance."""
        if data.empty or self.objective_name not in vocs.objectives:
            return False

        if self.objective_name not in data.columns:
            return False

        objective_type = vocs.objectives[self.objective_name]
        objective_values = data[self.objective_name].dropna()

        if len(objective_values) == 0:
            return False

        if isinstance(objective_type, MinimizeObjective):
            best_value = objective_values.min()
            return best_value <= self.target_value + self.tolerance
        else:  # MAXIMIZE
            best_value = objective_values.max()
            return best_value >= self.target_value - self.tolerance

should_stop(data, vocs)

Stop if objective reaches target value within tolerance.

Source code in xopt/stopping_conditions.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def should_stop(self, data: pd.DataFrame, vocs: VOCS) -> bool:
    """Stop if objective reaches target value within tolerance."""
    if data.empty or self.objective_name not in vocs.objectives:
        return False

    if self.objective_name not in data.columns:
        return False

    objective_type = vocs.objectives[self.objective_name]
    objective_values = data[self.objective_name].dropna()

    if len(objective_values) == 0:
        return False

    if isinstance(objective_type, MinimizeObjective):
        best_value = objective_values.min()
        return best_value <= self.target_value + self.tolerance
    else:  # MAXIMIZE
        best_value = objective_values.max()
        return best_value >= self.target_value - self.tolerance

yaml(**kwargs)

serialize first then dump to yaml string

Source code in xopt/pydantic.py
231
232
233
234
235
236
237
238
def yaml(self, **kwargs):
    """serialize first then dump to yaml string"""
    output = json.loads(
        self.to_json(
            **kwargs,
        )
    )
    return yaml.dump(output)