Skip to content

API for Developers

Here you will find the API provide for developers.

Solver Data

Class and functions related to storing the solver’s results.

Classes

perprof.solver_data.SolverData

Store performance data from a single solver/algorithm.

Contains performance metrics and metadata for one solver across multiple test problems.

Attributes:

  • algname (str) –

    Name of the algorithm/solver being analyzed.

  • data (DataFrame) –

    DataFrame containing performance results with required columns:

    • “name”: The problem name (string identifier).
    • “exit”: Exit flag to determine successful termination.
    • “time”: Elapsed time for the algorithm (float, seconds).
    • “fval”: Function value at the solution (optional).
    • “primal”: Primal residual at the solution (optional).
    • “dual”: Dual residual at the solution (optional).
  • success (list[str]) –

    List of exit flag values considered successful termination. Default: [“c”, “converged”, “solved”, “success”]

Example

import pandas as pd from perprof.solver_data import SolverData

Create from DataFrame

data = pd.DataFrame({ … “name”: [“prob1”, “prob2”, “prob3”], … “exit”: [“converged”, “converged”, “failed”], … “time”: [1.2, 3.4, 10.0] … }) solver = SolverData(“Newton”, data)

Access data

solver.algname ‘Newton’ len(solver.data) 3 successful = solver.data[solver.data.exit.isin(solver.success)] len(successful) / len(solver.data) > 0.5 True

Source code in perprof/solver_data.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
 57
 58
 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
103
104
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
class SolverData:
    """Store performance data from a single solver/algorithm.

    Contains performance metrics and metadata for one solver across multiple test problems.

    Attributes:
        algname (str):
            Name of the algorithm/solver being analyzed.
        data (pandas.DataFrame):
            DataFrame containing performance results with required columns:

            - "name": The problem name (string identifier).
            - "exit": Exit flag to determine successful termination.
            - "time": Elapsed time for the algorithm (float, seconds).
            - "fval": Function value at the solution (optional).
            - "primal": Primal residual at the solution (optional).
            - "dual": Dual residual at the solution (optional).
        success (list[str]):
            List of exit flag values considered successful termination.
            Default: ["c", "converged", "solved", "success"]

    Example:
        >>> import pandas as pd
        >>> from perprof.solver_data import SolverData
        >>>
        >>> # Create from DataFrame
        >>> data = pd.DataFrame({
        ...     "name": ["prob1", "prob2", "prob3"],
        ...     "exit": ["converged", "converged", "failed"],
        ...     "time": [1.2, 3.4, 10.0]
        ... })
        >>> solver = SolverData("Newton", data)
        >>>
        >>> # Access data
        >>> solver.algname
        'Newton'
        >>> len(solver.data)
        3
        >>> successful = solver.data[solver.data.exit.isin(solver.success)]
        >>> len(successful) / len(solver.data) > 0.5
        True
    """

    def __init__(
        self,
        algname: str,
        data: Union[str, Path, pd.DataFrame],
        success: list[str] | None = None,
        read_csv_args: dict | None = None,
    ) -> None:
        """Initialize SolverData from file or DataFrame.

        Args:
            algname (str):
                Name of the algorithm/solver for identification.
            data (Union[str, Path, pd.DataFrame]):
                Source of performance data. Can be:
                - File path (str/Path) to CSV file with solver results
                - Pre-loaded pandas DataFrame with required columns
            success (list[str], optional):
                Exit flag values considered successful termination.
                If None, defaults to ["c", "converged", "solved", "success"].
            read_csv_args (dict, optional):
                Additional arguments passed to pandas.read_csv when loading files.
                Useful for custom separators, encoding, etc.

        Raises:
            TypeError: If data is not a supported type (str, Path, or DataFrame).
            ValueError: If required columns ("name", "exit", "time") are missing.

        Example:
            >>> # From DataFrame
            >>> import pandas as pd
            >>> from perprof.solver_data import SolverData
            >>> df = pd.DataFrame({
            ...     "name": ["prob1", "prob2"],
            ...     "exit": ["converged", "failed"],
            ...     "time": [1.0, 5.0]
            ... })
            >>> solver = SolverData("MyAlgorithm", df)
            >>> solver.algname
            'MyAlgorithm'
            >>>
            >>> # Custom success flags
            >>> solver2 = SolverData("Algorithm2", df, success=["optimal", "feasible"])
            >>> solver2.success
            ['optimal', 'feasible']
        """
        self.algname = algname
        if not success:
            success = ["c", "converged", "solved", "success"]
        self.success = success
        if isinstance(data, (str, Path)):
            if not read_csv_args:
                read_csv_args = {}
            self.data = pd.read_csv(data, **read_csv_args)
        elif isinstance(data, pd.DataFrame):
            self.data = data
        else:
            raise TypeError("Unexpected type for data input")

        # Make sure that a columns name, time and exit exist
        for col in ["name", "exit", "time"]:
            if col not in self.data.columns:
                raise ValueError(f"Missing column {col}")
        for col in ["fval", "primal", "dual"]:
            if col not in self.data:
                self.data[col] = np.nan
Functions
__init__(algname, data, success=None, read_csv_args=None)

Parameters:

  • algname (str) –

    Name of the algorithm/solver for identification.

  • data (Union[str, Path, DataFrame]) –

    Source of performance data. Can be: - File path (str/Path) to CSV file with solver results - Pre-loaded pandas DataFrame with required columns

  • success (list[str], default: None ) –

    Exit flag values considered successful termination. If None, defaults to [“c”, “converged”, “solved”, “success”].

  • read_csv_args (dict, default: None ) –

    Additional arguments passed to pandas.read_csv when loading files. Useful for custom separators, encoding, etc.

Raises:

  • TypeError –

    If data is not a supported type (str, Path, or DataFrame).

  • ValueError –

    If required columns (“name”, “exit”, “time”) are missing.

Example
From DataFrame

import pandas as pd from perprof.solver_data import SolverData df = pd.DataFrame({ … “name”: [“prob1”, “prob2”], … “exit”: [“converged”, “failed”], … “time”: [1.0, 5.0] … }) solver = SolverData(“MyAlgorithm”, df) solver.algname ‘MyAlgorithm’

Custom success flags

solver2 = SolverData(“Algorithm2”, df, success=[“optimal”, “feasible”]) solver2.success [‘optimal’, ‘feasible’]

Source code in perprof/solver_data.py
 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
103
104
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
def __init__(
    self,
    algname: str,
    data: Union[str, Path, pd.DataFrame],
    success: list[str] | None = None,
    read_csv_args: dict | None = None,
) -> None:
    """Initialize SolverData from file or DataFrame.

    Args:
        algname (str):
            Name of the algorithm/solver for identification.
        data (Union[str, Path, pd.DataFrame]):
            Source of performance data. Can be:
            - File path (str/Path) to CSV file with solver results
            - Pre-loaded pandas DataFrame with required columns
        success (list[str], optional):
            Exit flag values considered successful termination.
            If None, defaults to ["c", "converged", "solved", "success"].
        read_csv_args (dict, optional):
            Additional arguments passed to pandas.read_csv when loading files.
            Useful for custom separators, encoding, etc.

    Raises:
        TypeError: If data is not a supported type (str, Path, or DataFrame).
        ValueError: If required columns ("name", "exit", "time") are missing.

    Example:
        >>> # From DataFrame
        >>> import pandas as pd
        >>> from perprof.solver_data import SolverData
        >>> df = pd.DataFrame({
        ...     "name": ["prob1", "prob2"],
        ...     "exit": ["converged", "failed"],
        ...     "time": [1.0, 5.0]
        ... })
        >>> solver = SolverData("MyAlgorithm", df)
        >>> solver.algname
        'MyAlgorithm'
        >>>
        >>> # Custom success flags
        >>> solver2 = SolverData("Algorithm2", df, success=["optimal", "feasible"])
        >>> solver2.success
        ['optimal', 'feasible']
    """
    self.algname = algname
    if not success:
        success = ["c", "converged", "solved", "success"]
    self.success = success
    if isinstance(data, (str, Path)):
        if not read_csv_args:
            read_csv_args = {}
        self.data = pd.read_csv(data, **read_csv_args)
    elif isinstance(data, pd.DataFrame):
        self.data = data
    else:
        raise TypeError("Unexpected type for data input")

    # Make sure that a columns name, time and exit exist
    for col in ["name", "exit", "time"]:
        if col not in self.data.columns:
            raise ValueError(f"Missing column {col}")
    for col in ["fval", "primal", "dual"]:
        if col not in self.data:
            self.data[col] = np.nan

Functions

perprof.solver_data.read_table(filename)

Read solver data from YAML-formatted table file.

Parses files with YAML front matter and tabular data. Standard format for perprof solver data files.

File format
---
Solver Name: MyAlgorithm
Success: converged,optimal
---
problem1 converged 1.23 0.001
problem2 failed 5.67 0.1
problem3 converged 2.45 0.01

Parameters:

  • filename (Union[str, Path]) –

    Path to the table file with YAML header and data rows.

Returns:

  • SolverData ( SolverData ) –

    Parsed solver data ready for profile analysis.

Example

The file format combines YAML metadata with tabular data:

File content example (not actual doctest):
—
Solver Name: MyAlgorithm
Success: converged,optimal
—
problem1 converged 1.23 0.001
problem2 failed 5.67 0.1

pass # Placeholder since file operations can’t be tested in doctest

Source code in perprof/solver_data.py
139
140
141
142
143
144
145
146
147
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def read_table(filename: Union[str, Path]) -> SolverData:
    """Read solver data from YAML-formatted table file.

    Parses files with YAML front matter and tabular data. Standard format for
    perprof solver data files.

    File format:
        ```
        ---
        Solver Name: MyAlgorithm
        Success: converged,optimal
        ---
        problem1 converged 1.23 0.001
        problem2 failed 5.67 0.1
        problem3 converged 2.45 0.01
        ```

    Args:
        filename (Union[str, Path]):
            Path to the table file with YAML header and data rows.

    Returns:
        SolverData: Parsed solver data ready for profile analysis.

    Example:
        The file format combines YAML metadata with tabular data:

        >>> # File content example (not actual doctest):
        >>> # ---
        >>> # Solver Name: MyAlgorithm
        >>> # Success: converged,optimal
        >>> # ---
        >>> # problem1 converged 1.23 0.001
        >>> # problem2 failed 5.67 0.1
        >>> pass  # Placeholder since file operations can't be tested in doctest
    """
    options: _ParseOptions = {
        "algname": None,
        "success": "c,converged,solved,success",
        "free_format": True,
        "col_name": 1,
        "col_exit": 2,
        "col_time": 3,
        "col_fval": 4,
        "col_primal": 5,
        "col_dual": 6,
    }

    with open(filename, encoding="utf-8") as file_:
        lines = file_.readlines()

        in_yaml = False
        for i, line in enumerate(lines):
            if line.strip() == "---":
                if in_yaml:
                    yaml_header = lines[0:i]
                    data_lines = lines[i + 1 :]
                    break
                in_yaml = True

    _parse_yaml(options, "".join(yaml_header))
    success_list = options["success"].split(",")
    data_header = ["name", "exit", "time", "fval", "primal", "dual"]
    header_order = [
        options["col_name"],
        options["col_exit"],
        options["col_time"],
        options["col_fval"],
        options["col_primal"],
        options["col_dual"],
    ]
    data_header = [data_header[i - 1] for i in header_order]
    data = pd.read_csv(
        StringIO("".join([" ".join(data_header) + "\n"] + data_lines)),
        delim_whitespace=True,
    )

    return SolverData(
        options["algname"] or "Unknown",
        data,
        success=success_list,
    )

Profile Data

Class to store the profile configuration and data.

Classes

perprof.profile_data.ProfileData

Computes and stores performance profiles for algorithm comparison.

Implements the Dolan and Moré performance profile methodology. Processes solver timing data and computes the fraction of problems solved by each solver within performance ratios relative to the best solver.

Attributes:

  • solvers (list[SolverData]) –

    List of solver_data.SolverData objects associated with this performance profile.

  • subset (list[str]) –

    If not None, used to restrict the problems in which the profile is created.

  • ratio (array) –

    Ratio matrix computed using the best time for each problem. Shape: (n_problems, n_solvers). Entry [i,j] = time[i,j] / min_time[i]

  • breakpoints (array) –

    Array of unique ratio values sorted in ascending order. Used as x-axis values for profile visualization.

  • cumulative (array) –

    Matrix of cumulative distribution of problems solved. Shape: (len(breakpoints), n_solvers). Entry [i,j] = fraction of problems where solver j has ratio <= breakpoints[i].

Example

import pandas as pd from perprof.profile_data import ProfileData from perprof.solver_data import SolverData

Create solver data from DataFrames

data1 = pd.DataFrame({ … “name”: [“prob1”, “prob2”], … “exit”: [“converged”, “converged”], … “time”: [1.0, 2.0] … }) data2 = pd.DataFrame({ … “name”: [“prob1”, “prob2”], … “exit”: [“converged”, “converged”], … “time”: [1.5, 1.5] … }) solver1 = SolverData(“Newton”, data1, success=[“converged”]) solver2 = SolverData(“BFGS”, data2, success=[“converged”])

Create performance profile

profile = ProfileData(solver1, solver2)

Access computed data

print(f”Ratio matrix shape: {profile.ratio.shape}”) Ratio matrix shape: (2, 2) print(f”Number of breakpoints: {len(profile.breakpoints)}”) Number of breakpoints: 3

Source code in perprof/profile_data.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 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
 57
 58
 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
103
104
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
146
147
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
class ProfileData:
    """Computes and stores performance profiles for algorithm comparison.

    Implements the Dolan and Moré performance profile methodology. Processes solver
    timing data and computes the fraction of problems solved by each solver within
    performance ratios relative to the best solver.

    Attributes:
        solvers (list[SolverData]):
            List of solver_data.SolverData objects associated with this performance profile.
        subset (list[str]):
            If not None, used to restrict the problems in which the profile is created.
        ratio (numpy.array):
            Ratio matrix computed using the best time for each problem.
            Shape: (n_problems, n_solvers). Entry [i,j] = time[i,j] / min_time[i]
        breakpoints (numpy.array):
            Array of unique ratio values sorted in ascending order.
            Used as x-axis values for profile visualization.
        cumulative (numpy.array):
            Matrix of cumulative distribution of problems solved.
            Shape: (len(breakpoints), n_solvers). Entry [i,j] = fraction of problems
            where solver j has ratio <= breakpoints[i].

    Example:
        >>> import pandas as pd
        >>> from perprof.profile_data import ProfileData
        >>> from perprof.solver_data import SolverData
        >>>
        >>> # Create solver data from DataFrames
        >>> data1 = pd.DataFrame({
        ...     "name": ["prob1", "prob2"],
        ...     "exit": ["converged", "converged"],
        ...     "time": [1.0, 2.0]
        ... })
        >>> data2 = pd.DataFrame({
        ...     "name": ["prob1", "prob2"],
        ...     "exit": ["converged", "converged"],
        ...     "time": [1.5, 1.5]
        ... })
        >>> solver1 = SolverData("Newton", data1, success=["converged"])
        >>> solver2 = SolverData("BFGS", data2, success=["converged"])
        >>>
        >>> # Create performance profile
        >>> profile = ProfileData(solver1, solver2)
        >>>
        >>> # Access computed data
        >>> print(f"Ratio matrix shape: {profile.ratio.shape}")
        Ratio matrix shape: (2, 2)
        >>> print(f"Number of breakpoints: {len(profile.breakpoints)}")
        Number of breakpoints: 3
    """

    def __init__(
        self, *solvers: Union[str, Path, SolverData], subset: list[str] | None = None
    ) -> None:
        """Initialize performance profile with solver data or file paths.

        Args:
            *solvers (Union[str, Path, SolverData]):
                Solver data sources. Can be:
                - File paths (str/Path) to YAML files with solver results
                - SolverData objects with pre-loaded data
                At least 2 solvers are required for comparison.
            subset (list[str], optional):
                If provided, restricts the analysis to only these problem names.
                Useful for focusing on specific problem subsets.

        Raises:
            ValueError: If solver input type is not supported or fewer than 2 solvers provided.

        Example:
            >>> import pandas as pd
            >>> from perprof.profile_data import ProfileData
            >>> from perprof.solver_data import SolverData
            >>>
            >>> # From SolverData objects with DataFrames
            >>> data1 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.2, 3.4]})
            >>> data2 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.5, 2.8]})
            >>> solver1 = SolverData("Method1", data1)
            >>> solver2 = SolverData("Method2", data2)
            >>> profile = ProfileData(solver1, solver2)
            >>>
            >>> # With problem subset
            >>> profile_subset = ProfileData(solver1, solver2, subset=["prob1"])
            >>> len(profile_subset._solvers_data)
            1
        """
        self.solvers = []
        for solver in solvers:
            if isinstance(solver, (str, Path)):
                self.solvers.append(read_table(solver))
            elif isinstance(solver, SolverData):
                self.solvers.append(solver)
            else:
                raise ValueError(f"Unexpected type for solver input: {type(solver)}")
        self.subset = subset

        # Variables that will be filled by self.process()
        self._solvers_data: pd.DataFrame | None = None
        self.ratio: np.ndarray | None = None
        self._best_times: np.ndarray | None = None
        self.breakpoints: np.ndarray | None = None
        self.cumulative: np.ndarray | None = None
        self.process()

    def process(self) -> None:
        """Process solver data to compute performance profile.

        Computes ratio matrix, breakpoints, and cumulative distribution:
        1. Merge solver data into unified DataFrame
        2. Set failed convergence times to infinity
        3. Compute ratio matrix: time[solver,problem] / min_time[problem]
        4. Generate breakpoints from unique ratio values
        5. Compute cumulative distribution

        Raises:
            ValueError: If fewer than 2 solvers are provided.

        Example:
            >>> import pandas as pd
            >>> from perprof.profile_data import ProfileData
            >>> from perprof.solver_data import SolverData
            >>>
            >>> # Create test data
            >>> data1 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.0, 2.0]})
            >>> data2 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.5, 1.5]})
            >>> solver1 = SolverData("Solver1", data1, success=["converged"])
            >>> solver2 = SolverData("Solver2", data2, success=["converged"])
            >>> profile = ProfileData(solver1, solver2)
            >>>
            >>> # Check computed ratios and cumulative distribution exist
            >>> profile.ratio is not None
            True
            >>> profile.cumulative is not None
            True
        """
        if len(self.solvers) <= 1:
            raise ValueError("A Profile needs two solvers, at least")

        # create the reduced dataset: |subset| x |solvers|
        cols = ["name", "time"]
        self._solvers_data = self.solvers[0].data[cols].copy()
        for solver in self.solvers[1:]:
            self._solvers_data = self._solvers_data.join(
                solver.data[cols].set_index("name"),
                on="name",
                rsuffix="_" + solver.algname,
            )
        self._solvers_data = self._solvers_data.rename(
            columns={"time": "time_" + self.solvers[0].algname}
        )

        # set to inf the ones that fail convergence
        for solver in self.solvers:
            mask = ~solver.data.exit.isin(solver.success)
            if np.any(mask):
                self._solvers_data.loc[mask, "time_" + solver.algname] = float("inf")

        self._solvers_data.fillna(float("inf"))
        if self.subset:
            self._solvers_data = self._solvers_data[
                self._solvers_data.name.isin(self.subset)
            ]

        # Compute the minimum time
        self._best_times = self._solvers_data.iloc[:, 1:].min(axis=1).values

        # Compute the cumulative distribution
        self.ratio = (
            self._solvers_data.iloc[:, 1:] / self._best_times[:, np.newaxis]
        ).values
        self.ratio[np.isnan(self.ratio)] = float("inf")
        self.breakpoints = np.sort(np.unique(self.ratio.reshape(-1)))
        # This removes inf and nan
        self.breakpoints = self.breakpoints[self.breakpoints < float("inf")]
        # self
        self.cumulative = (
            self.ratio[np.newaxis, :, :] <= self.breakpoints[:, np.newaxis, np.newaxis]
        )
        self.cumulative = self.cumulative.sum(axis=1) / self.ratio.shape[0]
Functions
__init__(*solvers, subset=None)

Parameters:

  • *solvers (Union[str, Path, SolverData], default: () ) –

    Solver data sources. Can be: - File paths (str/Path) to YAML files with solver results - SolverData objects with pre-loaded data At least 2 solvers are required for comparison.

  • subset (list[str], default: None ) –

    If provided, restricts the analysis to only these problem names. Useful for focusing on specific problem subsets.

Raises:

  • ValueError –

    If solver input type is not supported or fewer than 2 solvers provided.

Example

import pandas as pd from perprof.profile_data import ProfileData from perprof.solver_data import SolverData

From SolverData objects with DataFrames

data1 = pd.DataFrame({“name”: [“prob1”, “prob2”], “exit”: [“converged”, “converged”], “time”: [1.2, 3.4]}) data2 = pd.DataFrame({“name”: [“prob1”, “prob2”], “exit”: [“converged”, “converged”], “time”: [1.5, 2.8]}) solver1 = SolverData(“Method1”, data1) solver2 = SolverData(“Method2”, data2) profile = ProfileData(solver1, solver2)

With problem subset

profile_subset = ProfileData(solver1, solver2, subset=[“prob1”]) len(profile_subset._solvers_data) 1

Source code in perprof/profile_data.py
 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def __init__(
    self, *solvers: Union[str, Path, SolverData], subset: list[str] | None = None
) -> None:
    """Initialize performance profile with solver data or file paths.

    Args:
        *solvers (Union[str, Path, SolverData]):
            Solver data sources. Can be:
            - File paths (str/Path) to YAML files with solver results
            - SolverData objects with pre-loaded data
            At least 2 solvers are required for comparison.
        subset (list[str], optional):
            If provided, restricts the analysis to only these problem names.
            Useful for focusing on specific problem subsets.

    Raises:
        ValueError: If solver input type is not supported or fewer than 2 solvers provided.

    Example:
        >>> import pandas as pd
        >>> from perprof.profile_data import ProfileData
        >>> from perprof.solver_data import SolverData
        >>>
        >>> # From SolverData objects with DataFrames
        >>> data1 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.2, 3.4]})
        >>> data2 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.5, 2.8]})
        >>> solver1 = SolverData("Method1", data1)
        >>> solver2 = SolverData("Method2", data2)
        >>> profile = ProfileData(solver1, solver2)
        >>>
        >>> # With problem subset
        >>> profile_subset = ProfileData(solver1, solver2, subset=["prob1"])
        >>> len(profile_subset._solvers_data)
        1
    """
    self.solvers = []
    for solver in solvers:
        if isinstance(solver, (str, Path)):
            self.solvers.append(read_table(solver))
        elif isinstance(solver, SolverData):
            self.solvers.append(solver)
        else:
            raise ValueError(f"Unexpected type for solver input: {type(solver)}")
    self.subset = subset

    # Variables that will be filled by self.process()
    self._solvers_data: pd.DataFrame | None = None
    self.ratio: np.ndarray | None = None
    self._best_times: np.ndarray | None = None
    self.breakpoints: np.ndarray | None = None
    self.cumulative: np.ndarray | None = None
    self.process()
process()

Process solver data to compute performance profile.

Computes ratio matrix, breakpoints, and cumulative distribution: 1. Merge solver data into unified DataFrame 2. Set failed convergence times to infinity 3. Compute ratio matrix: time[solver,problem] / min_time[problem] 4. Generate breakpoints from unique ratio values 5. Compute cumulative distribution

Raises:

  • ValueError –

    If fewer than 2 solvers are provided.

Example

import pandas as pd from perprof.profile_data import ProfileData from perprof.solver_data import SolverData

Create test data

data1 = pd.DataFrame({“name”: [“prob1”, “prob2”], “exit”: [“converged”, “converged”], “time”: [1.0, 2.0]}) data2 = pd.DataFrame({“name”: [“prob1”, “prob2”], “exit”: [“converged”, “converged”], “time”: [1.5, 1.5]}) solver1 = SolverData(“Solver1”, data1, success=[“converged”]) solver2 = SolverData(“Solver2”, data2, success=[“converged”]) profile = ProfileData(solver1, solver2)

Check computed ratios and cumulative distribution exist

profile.ratio is not None True profile.cumulative is not None True

Source code in perprof/profile_data.py
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
146
147
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
def process(self) -> None:
    """Process solver data to compute performance profile.

    Computes ratio matrix, breakpoints, and cumulative distribution:
    1. Merge solver data into unified DataFrame
    2. Set failed convergence times to infinity
    3. Compute ratio matrix: time[solver,problem] / min_time[problem]
    4. Generate breakpoints from unique ratio values
    5. Compute cumulative distribution

    Raises:
        ValueError: If fewer than 2 solvers are provided.

    Example:
        >>> import pandas as pd
        >>> from perprof.profile_data import ProfileData
        >>> from perprof.solver_data import SolverData
        >>>
        >>> # Create test data
        >>> data1 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.0, 2.0]})
        >>> data2 = pd.DataFrame({"name": ["prob1", "prob2"], "exit": ["converged", "converged"], "time": [1.5, 1.5]})
        >>> solver1 = SolverData("Solver1", data1, success=["converged"])
        >>> solver2 = SolverData("Solver2", data2, success=["converged"])
        >>> profile = ProfileData(solver1, solver2)
        >>>
        >>> # Check computed ratios and cumulative distribution exist
        >>> profile.ratio is not None
        True
        >>> profile.cumulative is not None
        True
    """
    if len(self.solvers) <= 1:
        raise ValueError("A Profile needs two solvers, at least")

    # create the reduced dataset: |subset| x |solvers|
    cols = ["name", "time"]
    self._solvers_data = self.solvers[0].data[cols].copy()
    for solver in self.solvers[1:]:
        self._solvers_data = self._solvers_data.join(
            solver.data[cols].set_index("name"),
            on="name",
            rsuffix="_" + solver.algname,
        )
    self._solvers_data = self._solvers_data.rename(
        columns={"time": "time_" + self.solvers[0].algname}
    )

    # set to inf the ones that fail convergence
    for solver in self.solvers:
        mask = ~solver.data.exit.isin(solver.success)
        if np.any(mask):
            self._solvers_data.loc[mask, "time_" + solver.algname] = float("inf")

    self._solvers_data.fillna(float("inf"))
    if self.subset:
        self._solvers_data = self._solvers_data[
            self._solvers_data.name.isin(self.subset)
        ]

    # Compute the minimum time
    self._best_times = self._solvers_data.iloc[:, 1:].min(axis=1).values

    # Compute the cumulative distribution
    self.ratio = (
        self._solvers_data.iloc[:, 1:] / self._best_times[:, np.newaxis]
    ).values
    self.ratio[np.isnan(self.ratio)] = float("inf")
    self.breakpoints = np.sort(np.unique(self.ratio.reshape(-1)))
    # This removes inf and nan
    self.breakpoints = self.breakpoints[self.breakpoints < float("inf")]
    # self
    self.cumulative = (
        self.ratio[np.newaxis, :, :] <= self.breakpoints[:, np.newaxis, np.newaxis]
    )
    self.cumulative = self.cumulative.sum(axis=1) / self.ratio.shape[0]

Functions