optimeo.doe

This module provides a class for creating and visualizing a design of experiments (DoE). It supports various types of designs including Full Factorial, Sobol sequence, Fractional Factorial, Definitive Screening Design, Space Filling Latin Hypercube, Randomized Latin Hypercube, Optimal, Plackett-Burman, and Box-Behnken. The class allows the user to specify the parameters, their types, and values, and generates the design accordingly. It also provides a method to plot the design using scatter plots.

You can see an example notebook here.

  1# Copyright (c) 2025 Colin BOUSIGE
  2# Contact: colin.bousige@cnrs.fr
  3#
  4# This program is free software: you can redistribute it and/or modify
  5# it under the terms of the MIT License as published by
  6# the Free Software Foundation, either version 3 of the License, or
  7# any later version. 
  8
  9"""
 10This module provides a class for creating and visualizing a design of experiments (DoE).
 11It supports various types of designs including Full Factorial, Sobol sequence, Fractional Factorial, Definitive Screening Design, Space Filling Latin Hypercube, Randomized Latin Hypercube, Optimal, Plackett-Burman, and Box-Behnken.
 12The class allows the user to specify the parameters, their types, and values, and generates the design accordingly.
 13It also provides a method to plot the design using scatter plots.
 14
 15You can see an example notebook [here](../examples/doe.html).
 16"""
 17
 18
 19import warnings
 20warnings.simplefilter(action='ignore', category=FutureWarning)
 21warnings.simplefilter(action='ignore', category=DeprecationWarning)
 22warnings.simplefilter(action='ignore', category=UserWarning)
 23warnings.simplefilter(action='ignore', category=RuntimeError)
 24import numpy as np
 25import pandas as pd
 26from typing import Any, Dict, List, Optional
 27from dexpy.optimal import build_optimal
 28from dexpy.model import ModelOrder
 29from dexpy.design import coded_to_actual
 30from doepy import build
 31from sklearn.preprocessing import LabelEncoder
 32from pyDOE3 import *
 33import definitive_screening_design as dsd
 34import plotly.express as px
 35from itertools import combinations
 36import plotly.graph_objects as go
 37
 38class DesignOfExperiments:
 39    """
 40    Class to create a design of experiments (DoE) for a given model.
 41    This class allows the user to specify the type of design, the parameters,
 42    and various options for the design generation.
 43    The design can be visualized using scatter plots.
 44    
 45    
 46    Parameters
 47    ----------
 48    
 49    type : str
 50        The type of design to create. Must be one of:
 51        `'Full Factorial'`, `'Sobol sequence'`, `'Fractional Factorial'`,
 52        `'Definitive Screening'`, `'Space Filling Latin Hypercube'`,
 53        `'Randomized Latin Hypercube'`, `'Optimal'`, `'Plackett-Burman'`,
 54        `'Box-Behnken'` or `'Central Composite'`.
 55    parameters : List[Dict[str, Dict[str, Any]]]
 56        List of parameters for the design, each with a dictionary of properties.
 57        Each dictionary should contain 'name', 'type', and 'values'.
 58        'values' should be a list of possible values for the parameter.
 59        'type' should be either "int", "integer", "float", "<other>". 
 60        Any <other> will be considered as "categorical".
 61        'values' should be a list of possible values for the parameter.
 62    Nexp : int, optional
 63        Number of experiments in the design, when applicable. Default is 4.
 64    order : int, optional
 65        Order of the model (for 'Optimal' design). Default is 2.
 66    randomize : bool, optional
 67        Whether to randomize the run order. Default is True.
 68    reduction : int, optional
 69        Reduction factor for 'Fractional Factorial' designs. Default is 2.
 70    feature_constraints : Optional[List[Dict[str, Any]]], optional
 71        Feature constraints of the experiment for Sobol sequence. Default is None.
 72        If a single dictionary is provided, it will be converted to a list.
 73        If a string is provided, it will be converted to a list with one element.
 74        If a list is provided, it will be used as is.
 75        If None, no constraints will be applied.
 76    
 77    Attributes
 78    ----------
 79    
 80    type : str
 81        The type of design.
 82    parameters : List[Dict[str, Dict[str, Any]]]
 83        The parameters for the design.
 84    Nexp : int
 85        Number of experiments in the design.
 86    order : int
 87        Order of the model.
 88    randomize : bool
 89        Whether to randomize the run order.
 90    reduction : int
 91        Reduction factor for `'Fractional Factorial'` designs.
 92    design : pd.DataFrame
 93        The design DataFrame.
 94    lows : Dict[str, float]
 95        Lower bounds for the parameters.
 96    highs : Dict[str, float]
 97        Upper bounds for the parameters.
 98    
 99    Methods
100    -------
101    - **create_design()**:
102        Create the design of experiments based on the specified type and parameters.
103    - **plot()**:
104        Plot the design of experiments using plotly.
105    
106    
107    Example
108    -------
109    
110    ```python
111    from doe import DesignOfExperiments
112    parameters = [
113        {'name': 'Temperature', 'type': 'integer', 'values': [20, 30, 40]},
114        {'name': 'Pressure', 'type': 'float', 'values': [1, 2, 3]},
115        {'name': 'Catalyst', 'type': 'categorical', 'values': ['A', 'B', 'C']}
116    ]
117    doe = DesignOfExperiments(
118        type='Full Factorial',
119        parameters=parameters
120    )
121    design = doe.design
122    print(design)
123    figs = doe.plot()
124    for fig in figs:
125        fig.show()
126    ```
127    
128
129    """
130
131    def __init__(self, 
132                 type: str,
133                 parameters: List[Dict[str, Dict[str, Any]]],
134                 Nexp: int = 4, 
135                 order: int = 2, 
136                 randomize: bool = True, 
137                 reduction: int = 2,
138                 feature_constraints: Optional[List[Dict[str, Any]]] = None,
139                 center=(2,2),
140                 alpha='o',
141                 face='ccc'):
142        self.type = type
143        self.parameters = parameters
144        self.Nexp = Nexp
145        self.order = order
146        self.randomize = randomize
147        self.reduction = reduction
148        self.center = center
149        self.alpha = alpha
150        self.face = face
151        self.design = None
152        self.lows = {}
153        self.feature_constraints = feature_constraints
154        self.highs = {}
155        self.create_design()
156
157    def __repr__(self):
158        return self.__str__()
159
160    def __str__(self):
161        """
162        Return a string representation of the DesignOfExperiments instance.
163        """
164        printpar = "\n".join([str(par) for par in self.parameters])
165        return f"""
166- Design of Experiments type: {self.type}
167- Parameters:
168{printpar}
169- Lows: {self._lows}
170- Highs: {self._highs}
171- If applicable:
172    - Randomize: {self.randomize}
173    - Number of Experiments: {self.Nexp}
174    - Order: {self.order}
175    - Reduction: {self.reduction}
176- Design:
177{self.design}
178"""
179
180    @property
181    def type(self) -> str:
182        """The type of design to create. Must be one of: `'Full Factorial'`, `'Sobol sequence'`, `'Fractional Factorial'`, `'Definitive Screening'`, `'Space Filling Latin Hypercube'`, `'Randomized Latin Hypercube'`, `'Optimal'`, `'Plackett-Burman'`, `'Box-Behnken'` or `'Central Composite'`."""
183        return self._type
184
185    @type.setter
186    def type(self, value: str):
187        """Set the type of design."""
188        self._type = value
189
190    @property
191    def parameters(self) -> List[Dict[str, Dict[str, Any]]]:
192        """List of parameters for the design, each with a dictionary of properties.
193            Each dictionary should contain the keys `"name"`, `"type"`, and `"values"`.
194            `"values"` should be a list of possible values for the parameter.
195            `"type"` should be either `"int"`, `"integer"`, `"float"`, `"<other>"`. 
196            Any `"<other>"` will be considered as `"categorical"`.
197            `values` should be a list of possible values for the parameter."""
198        return self._parameters
199
200    @parameters.setter
201    def parameters(self, value: List[Dict[str, Dict[str, Any]]]):
202        """Set the parameters for the design."""
203        self._parameters = value
204
205    @property
206    def Nexp(self) -> int:
207        """Number of experiments in the design, when applicable. Default is `4`."""
208        return self._Nexp
209
210    @Nexp.setter
211    def Nexp(self, value: int):
212        """Set the number of experiments."""
213        self._Nexp = value
214
215    @property
216    def order(self) -> int:
217        """Order of the model (for `'Optimal'` design). Default is `2`."""
218        return self._order
219    
220    @property
221    def center(self) -> tuple:
222        """Center for the Central Composite Design. Must be a tuple of two values."""
223        return self._center
224    
225    @center.setter
226    def center(self, value: tuple):
227        """Set the center of the design."""
228        if not isinstance(value, tuple):
229            raise ValueError("Center must be a tuple of two values.")
230        if len(value) != 2:
231            raise ValueError("Center must be a tuple of two values.")
232        if not all(isinstance(i, (int, float)) for i in value):
233            raise ValueError("Center must be a tuple of two numeric values.")
234        self._center = value
235    
236    @property
237    def alpha(self) -> str:
238        """Alpha for the Central Composite Design. Default is `'o'` (orthogonal).
239        Can be either `'o'` or `'r'` (rotatable)."""
240        return self._alpha
241    
242    @alpha.setter
243    def alpha(self, value: str):
244        """Set the alpha of the design."""
245        if value not in ['o', 'r']:
246            raise ValueError("Alpha must be either 'o' (orthogonal) or 'r' (rotatable).")
247        self._alpha = value
248    
249    @property
250    def face(self) -> str:
251        """The relation between the start points and the corner (factorial) points for the Central Composite Design.
252        
253        There are three possible options for this input:
254        
255        1. 'circumscribed' or 'ccc' (Default)
256        2. 'inscribed' or 'cci'
257        3. 'faced' or 'ccf'"""
258        return self._face
259
260    @face.setter
261    def face(self, value: str):
262        """Set the face of the design."""
263        if value not in ['ccc', 'cci', 'ccf']:
264            raise ValueError("Face must be either 'ccc' (circumscribed), 'cci' (inscribed), or 'ccf' (faced).")
265        self._face = value
266    
267    @property
268    def lows(self) -> Dict[str, float]:
269        """Get the lower bounds for the parameters."""
270        return self._lows
271    
272    @lows.setter
273    def lows(self, value: Dict[str, float]):
274        """Set the lower bounds for the parameters."""
275        self._lows = value
276    
277    @property
278    def highs(self) -> Dict[str, float]:
279        """Get the upper bounds for the parameters."""
280        return self._highs
281    
282    @highs.setter
283    def highs(self, value: Dict[str, float]):
284        """Set the upper bounds for the parameters."""
285        self._highs = value
286
287    @order.setter
288    def order(self, value: int):
289        """Set the order of the model."""
290        self._order = value
291
292    @property
293    def randomize(self) -> bool:
294        """Whether to randomize the run order. Default is `True`."""
295        return self._randomize
296
297    @randomize.setter
298    def randomize(self, value: bool):
299        """Set the randomize flag."""
300        self._randomize = value
301
302    @property
303    def reduction(self) -> int:
304        """Reduction factor for `'Fractional Factorial'` designs. Default is `2`."""
305        return self._reduction
306
307    @reduction.setter
308    def reduction(self, value: int):
309        """Set the reduction factor."""
310        self._reduction = value
311
312    @property
313    def design(self) -> pd.DataFrame:
314        """Get the design DataFrame."""
315        return self._design
316
317    @design.setter
318    def design(self, value: pd.DataFrame):
319        """Set the design DataFrame."""
320        self._design = value
321    
322    @property
323    def feature_constraints(self):
324        """
325        Get the feature constraints of the experiment for Sobol sequence.
326        """
327        return self._feature_constraints
328
329    @feature_constraints.setter
330    def feature_constraints(self, value):
331        """
332        Set the feature constraints of the experiment with validation for Sobol sequence.
333        """
334        if isinstance(value, dict):
335            self._feature_constraints = [value]
336        elif isinstance(value, list):
337            self._feature_constraints = value if len(value) > 0 else None
338        elif isinstance(value, str):
339            self._feature_constraints = [value]
340        else:
341            self._feature_constraints = None
342
343# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
344# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
345# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
346
347    def create_design(self):
348        """
349        Create the design of experiments based on the specified type and parameters.
350        """
351        for par in self.parameters:
352            if par['type'].lower() == "categorical":
353                if self.type != 'Sobol sequence':
354                    le = LabelEncoder()
355                    label = le.fit_transform(par['values'])
356                    par['values'] = label
357                    par['encoder'] = le
358                    self.lows[par['name']] = np.min(par['values'])
359                    self.highs[par['name']] = np.max(par['values'])
360                else:
361                    par['encoder'] = None
362            else:
363                self.lows[par['name']] = np.min(par['values'])
364                self.highs[par['name']] = np.max(par['values'])
365
366        pars = {par['name']: par['values'] for par in self.parameters}
367
368        if self.type == 'Full Factorial':
369            self.design = build.full_fact(pars)
370        elif self.type == 'Sobol sequence':
371            from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy
372            from ax.modelbridge.registry import Models
373            from ax.service.ax_client import AxClient, ObjectiveProperties
374
375            ax_client = AxClient()
376            params = []
377            for par in self.parameters:
378                if par['type'].lower() == "float":
379                    params.append({'name': par['name'],
380                                   'type': 'range',
381                                   'value_type': 'float',
382                                   'bounds': [float(np.min(par['values'])), float(np.max(par['values']))]})
383                elif par['type'].lower() in ["integer", 'int']:
384                    params.append({'name': par['name'],
385                                   'type': 'range',
386                                   'value_type': 'int',
387                                   'bounds': [int(np.min(par['values'])), int(np.max(par['values']))]})
388                else:
389                    params.append({'name': par['name'],
390                                   'type': 'choice',
391                                   'values': par['values']})
392            ax_client.create_experiment(
393                name="DOE",
394                parameters=params,
395                objectives={"response": ObjectiveProperties(minimize=False)},
396                parameter_constraints=self._feature_constraints
397            )
398            gs = GenerationStrategy(
399                steps=[GenerationStep(
400                    model=Models.SOBOL,
401                    num_trials=-1,
402                    should_deduplicate=True,
403                    model_kwargs={"seed": 165478},
404                    model_gen_kwargs={},
405                )]
406            )
407            generator_run = gs.gen(
408                experiment=ax_client.experiment,
409                data=None,
410                n=self.Nexp
411            )
412            if self.Nexp == 1:
413                ax_client.experiment.new_trial(generator_run)
414            else:
415                ax_client.experiment.new_batch_trial(generator_run)
416            trials = ax_client.get_trials_data_frame()
417            self.design = trials[trials['trial_status'] == 'CANDIDATE']
418            self.design = self._design.drop(columns=['trial_index',
419                                                      'trial_status',
420                                                      'arm_name',
421                                                      'generation_method',
422                                                      'generation_node'])
423        elif self.type == 'Fractional Factorial':
424            for par in range(len(self.parameters)):
425                if self.parameters[par]['type'] == "Numerical":
426                    self.parameters[par]['type'] = "Categorical"
427                    le = LabelEncoder()
428                    label = le.fit_transform(self.parameters[par]['values'])
429                    self.parameters[par]['values'] = label
430                    self.parameters[par]['encoder'] = le
431            design = gsd([len(par['values']) for par in self.parameters], self.reduction)
432            self.design = pd.DataFrame(design, columns=[par['name'] for par in self.parameters])
433        elif self.type == 'Definitive Screening':
434            params = {par['name']: [np.min(par['values']), np.max(par['values'])] for par in self.parameters}
435            self.design = dsd.generate(factors_dict=params)
436        elif self.type == 'Space Filling Latin Hypercube':
437            self.design = build.space_filling_lhs(pars, num_samples=self.Nexp)
438        elif self.type == 'Randomized Latin Hypercube':
439            self.design = build.lhs(pars, num_samples=self.Nexp)
440        elif self.type == 'Optimal':
441            reaction_design = build_optimal(
442                len(self.parameters),
443                order=ModelOrder(self.order),
444                run_count=self.Nexp)
445            reaction_design.columns = [par['name'] for par in self.parameters]
446            self.design = coded_to_actual(reaction_design, self._lows, self._highs)
447        elif self.type == 'Plackett-Burman':
448            self.design = build.plackett_burman(pars)
449        elif self.type == 'Box-Behnken':
450            if len(self.parameters) < 3 or any([len(par['values']) < 3 for par in self.parameters]):
451                self.design = pd.DataFrame({})
452                raise Warning("Box-Behnken design is not possible with less than 3 parameters and with less than 3 levels for any parameter.")
453            else:
454                self.design = build.box_behnken(d=pars)
455        elif self.type == 'Central Composite':
456            self.design = build.central_composite(pars, 
457                                                  center=self.center,
458                                                  alpha=self.alpha,
459                                                  face=self.face)
460        else:
461            raise Warning("Unknown design type. Must be one of: 'Full Factorial', 'Sobol sequence', 'Fractional Factorial', 'Definitive Screening', 'Space Filling Latin Hypercube', 'Randomized Latin Hypercube', 'Optimal', 'Plackett-Burman', 'Box-Behnken' or 'Central Composite'.")
462
463        for par in self.parameters:
464            if par['type'] == "Categorical" and self.type != 'Sobol sequence':
465                vals = self._design[par['name']].to_numpy()
466                self.design[par['name']] = par['encoder'].inverse_transform([int(v) for v in vals])
467
468        # randomize the run order
469        self.design['run_order'] = np.arange(len(self._design)) + 1
470        if self.randomize:
471            ord = self._design['run_order'].to_numpy()
472            self.design['run_order'] = np.random.permutation(ord)
473        cols = self._design.columns.tolist()
474        cols = cols[-1:] + cols[:-1]
475        self.design = self._design[cols]
476        # apply the column types
477        for col in self._design.columns:
478            for par in self.parameters:
479                if col == par['name']:
480                    if par['type'].lower() == "float":
481                        self.design[col] = self._design[col].astype(float)
482                    elif par['type'].lower() in ["int", "integer"]:
483                        self.design[col] = self._design[col].astype(int)
484                    else:
485                        self.design[col] = self._design[col].astype(str)
486        return self._design
487
488    def plot(self):
489        """
490        Plot the design of experiments.
491
492        Returns
493        -------
494        List of plotly.graph_objs._figure.Figure
495            A list of Plotly figures representing the design of experiments.
496        """
497        fig = []
498        count = 0
499        if len(self.design) > 0:
500            if len(self.parameters) <= 2:
501                # Create 2D scatter plots
502                for i, faci in enumerate(self.parameters):
503                    for j, facj in enumerate(self.parameters):
504                        if j > i:
505                            fig.append(px.scatter(
506                                self.design,
507                                x=facj['name'],
508                                y=faci['name'],
509                                title=f"""{faci['name']} vs {facj['name']}""",
510                                labels={facj['name']: facj['name'], faci['name']: faci['name']}
511                            ))
512                            fig[count].update_traces(marker=dict(size=10))
513                            fig[count].update_layout(
514                                margin=dict(l=10, r=10, t=50, b=50),
515                                xaxis=dict(
516                                    showgrid=True,
517                                    gridcolor="lightgray",
518                                    zeroline=False,
519                                    showline=True,
520                                    linewidth=1,
521                                    linecolor="black",
522                                    mirror=True
523                                ),
524                                yaxis=dict(
525                                    showgrid=True,
526                                    gridcolor="lightgray",
527                                    zeroline=False,
528                                    showline=True,
529                                    linewidth=1,
530                                    linecolor="black",
531                                    mirror=True
532                                ),
533                            )
534                            count += 1
535            else:
536                # Create 3D scatter plots
537                for k, (faci, facj, fack) in enumerate(combinations(self.parameters, 3)):
538                    fig.append(go.Figure(data=[go.Scatter3d(
539                        x=self.design[facj['name']],
540                        y=self.design[faci['name']],
541                        z=self.design[fack['name']],
542                        mode='markers',
543                        marker=dict(size=10, color='royalblue', opacity=0.7),
544                    )]))
545                    fig[count].update_layout(
546                        template='ggplot2',
547                        height=500,
548                        width=500,
549                        scene=dict(
550                            xaxis_title=facj['name'],
551                            yaxis_title=faci['name'],
552                            zaxis_title=fack['name'],
553                        ),
554                        title=f"{faci['name']} vs {facj['name']}<br>vs {fack['name']}",
555                        margin=dict(l=10, r=10, t=50, b=50)
556                    )
557                    count += 1
558        return fig
class DesignOfExperiments:
 39class DesignOfExperiments:
 40    """
 41    Class to create a design of experiments (DoE) for a given model.
 42    This class allows the user to specify the type of design, the parameters,
 43    and various options for the design generation.
 44    The design can be visualized using scatter plots.
 45    
 46    
 47    Parameters
 48    ----------
 49    
 50    type : str
 51        The type of design to create. Must be one of:
 52        `'Full Factorial'`, `'Sobol sequence'`, `'Fractional Factorial'`,
 53        `'Definitive Screening'`, `'Space Filling Latin Hypercube'`,
 54        `'Randomized Latin Hypercube'`, `'Optimal'`, `'Plackett-Burman'`,
 55        `'Box-Behnken'` or `'Central Composite'`.
 56    parameters : List[Dict[str, Dict[str, Any]]]
 57        List of parameters for the design, each with a dictionary of properties.
 58        Each dictionary should contain 'name', 'type', and 'values'.
 59        'values' should be a list of possible values for the parameter.
 60        'type' should be either "int", "integer", "float", "<other>". 
 61        Any <other> will be considered as "categorical".
 62        'values' should be a list of possible values for the parameter.
 63    Nexp : int, optional
 64        Number of experiments in the design, when applicable. Default is 4.
 65    order : int, optional
 66        Order of the model (for 'Optimal' design). Default is 2.
 67    randomize : bool, optional
 68        Whether to randomize the run order. Default is True.
 69    reduction : int, optional
 70        Reduction factor for 'Fractional Factorial' designs. Default is 2.
 71    feature_constraints : Optional[List[Dict[str, Any]]], optional
 72        Feature constraints of the experiment for Sobol sequence. Default is None.
 73        If a single dictionary is provided, it will be converted to a list.
 74        If a string is provided, it will be converted to a list with one element.
 75        If a list is provided, it will be used as is.
 76        If None, no constraints will be applied.
 77    
 78    Attributes
 79    ----------
 80    
 81    type : str
 82        The type of design.
 83    parameters : List[Dict[str, Dict[str, Any]]]
 84        The parameters for the design.
 85    Nexp : int
 86        Number of experiments in the design.
 87    order : int
 88        Order of the model.
 89    randomize : bool
 90        Whether to randomize the run order.
 91    reduction : int
 92        Reduction factor for `'Fractional Factorial'` designs.
 93    design : pd.DataFrame
 94        The design DataFrame.
 95    lows : Dict[str, float]
 96        Lower bounds for the parameters.
 97    highs : Dict[str, float]
 98        Upper bounds for the parameters.
 99    
100    Methods
101    -------
102    - **create_design()**:
103        Create the design of experiments based on the specified type and parameters.
104    - **plot()**:
105        Plot the design of experiments using plotly.
106    
107    
108    Example
109    -------
110    
111    ```python
112    from doe import DesignOfExperiments
113    parameters = [
114        {'name': 'Temperature', 'type': 'integer', 'values': [20, 30, 40]},
115        {'name': 'Pressure', 'type': 'float', 'values': [1, 2, 3]},
116        {'name': 'Catalyst', 'type': 'categorical', 'values': ['A', 'B', 'C']}
117    ]
118    doe = DesignOfExperiments(
119        type='Full Factorial',
120        parameters=parameters
121    )
122    design = doe.design
123    print(design)
124    figs = doe.plot()
125    for fig in figs:
126        fig.show()
127    ```
128    
129
130    """
131
132    def __init__(self, 
133                 type: str,
134                 parameters: List[Dict[str, Dict[str, Any]]],
135                 Nexp: int = 4, 
136                 order: int = 2, 
137                 randomize: bool = True, 
138                 reduction: int = 2,
139                 feature_constraints: Optional[List[Dict[str, Any]]] = None,
140                 center=(2,2),
141                 alpha='o',
142                 face='ccc'):
143        self.type = type
144        self.parameters = parameters
145        self.Nexp = Nexp
146        self.order = order
147        self.randomize = randomize
148        self.reduction = reduction
149        self.center = center
150        self.alpha = alpha
151        self.face = face
152        self.design = None
153        self.lows = {}
154        self.feature_constraints = feature_constraints
155        self.highs = {}
156        self.create_design()
157
158    def __repr__(self):
159        return self.__str__()
160
161    def __str__(self):
162        """
163        Return a string representation of the DesignOfExperiments instance.
164        """
165        printpar = "\n".join([str(par) for par in self.parameters])
166        return f"""
167- Design of Experiments type: {self.type}
168- Parameters:
169{printpar}
170- Lows: {self._lows}
171- Highs: {self._highs}
172- If applicable:
173    - Randomize: {self.randomize}
174    - Number of Experiments: {self.Nexp}
175    - Order: {self.order}
176    - Reduction: {self.reduction}
177- Design:
178{self.design}
179"""
180
181    @property
182    def type(self) -> str:
183        """The type of design to create. Must be one of: `'Full Factorial'`, `'Sobol sequence'`, `'Fractional Factorial'`, `'Definitive Screening'`, `'Space Filling Latin Hypercube'`, `'Randomized Latin Hypercube'`, `'Optimal'`, `'Plackett-Burman'`, `'Box-Behnken'` or `'Central Composite'`."""
184        return self._type
185
186    @type.setter
187    def type(self, value: str):
188        """Set the type of design."""
189        self._type = value
190
191    @property
192    def parameters(self) -> List[Dict[str, Dict[str, Any]]]:
193        """List of parameters for the design, each with a dictionary of properties.
194            Each dictionary should contain the keys `"name"`, `"type"`, and `"values"`.
195            `"values"` should be a list of possible values for the parameter.
196            `"type"` should be either `"int"`, `"integer"`, `"float"`, `"<other>"`. 
197            Any `"<other>"` will be considered as `"categorical"`.
198            `values` should be a list of possible values for the parameter."""
199        return self._parameters
200
201    @parameters.setter
202    def parameters(self, value: List[Dict[str, Dict[str, Any]]]):
203        """Set the parameters for the design."""
204        self._parameters = value
205
206    @property
207    def Nexp(self) -> int:
208        """Number of experiments in the design, when applicable. Default is `4`."""
209        return self._Nexp
210
211    @Nexp.setter
212    def Nexp(self, value: int):
213        """Set the number of experiments."""
214        self._Nexp = value
215
216    @property
217    def order(self) -> int:
218        """Order of the model (for `'Optimal'` design). Default is `2`."""
219        return self._order
220    
221    @property
222    def center(self) -> tuple:
223        """Center for the Central Composite Design. Must be a tuple of two values."""
224        return self._center
225    
226    @center.setter
227    def center(self, value: tuple):
228        """Set the center of the design."""
229        if not isinstance(value, tuple):
230            raise ValueError("Center must be a tuple of two values.")
231        if len(value) != 2:
232            raise ValueError("Center must be a tuple of two values.")
233        if not all(isinstance(i, (int, float)) for i in value):
234            raise ValueError("Center must be a tuple of two numeric values.")
235        self._center = value
236    
237    @property
238    def alpha(self) -> str:
239        """Alpha for the Central Composite Design. Default is `'o'` (orthogonal).
240        Can be either `'o'` or `'r'` (rotatable)."""
241        return self._alpha
242    
243    @alpha.setter
244    def alpha(self, value: str):
245        """Set the alpha of the design."""
246        if value not in ['o', 'r']:
247            raise ValueError("Alpha must be either 'o' (orthogonal) or 'r' (rotatable).")
248        self._alpha = value
249    
250    @property
251    def face(self) -> str:
252        """The relation between the start points and the corner (factorial) points for the Central Composite Design.
253        
254        There are three possible options for this input:
255        
256        1. 'circumscribed' or 'ccc' (Default)
257        2. 'inscribed' or 'cci'
258        3. 'faced' or 'ccf'"""
259        return self._face
260
261    @face.setter
262    def face(self, value: str):
263        """Set the face of the design."""
264        if value not in ['ccc', 'cci', 'ccf']:
265            raise ValueError("Face must be either 'ccc' (circumscribed), 'cci' (inscribed), or 'ccf' (faced).")
266        self._face = value
267    
268    @property
269    def lows(self) -> Dict[str, float]:
270        """Get the lower bounds for the parameters."""
271        return self._lows
272    
273    @lows.setter
274    def lows(self, value: Dict[str, float]):
275        """Set the lower bounds for the parameters."""
276        self._lows = value
277    
278    @property
279    def highs(self) -> Dict[str, float]:
280        """Get the upper bounds for the parameters."""
281        return self._highs
282    
283    @highs.setter
284    def highs(self, value: Dict[str, float]):
285        """Set the upper bounds for the parameters."""
286        self._highs = value
287
288    @order.setter
289    def order(self, value: int):
290        """Set the order of the model."""
291        self._order = value
292
293    @property
294    def randomize(self) -> bool:
295        """Whether to randomize the run order. Default is `True`."""
296        return self._randomize
297
298    @randomize.setter
299    def randomize(self, value: bool):
300        """Set the randomize flag."""
301        self._randomize = value
302
303    @property
304    def reduction(self) -> int:
305        """Reduction factor for `'Fractional Factorial'` designs. Default is `2`."""
306        return self._reduction
307
308    @reduction.setter
309    def reduction(self, value: int):
310        """Set the reduction factor."""
311        self._reduction = value
312
313    @property
314    def design(self) -> pd.DataFrame:
315        """Get the design DataFrame."""
316        return self._design
317
318    @design.setter
319    def design(self, value: pd.DataFrame):
320        """Set the design DataFrame."""
321        self._design = value
322    
323    @property
324    def feature_constraints(self):
325        """
326        Get the feature constraints of the experiment for Sobol sequence.
327        """
328        return self._feature_constraints
329
330    @feature_constraints.setter
331    def feature_constraints(self, value):
332        """
333        Set the feature constraints of the experiment with validation for Sobol sequence.
334        """
335        if isinstance(value, dict):
336            self._feature_constraints = [value]
337        elif isinstance(value, list):
338            self._feature_constraints = value if len(value) > 0 else None
339        elif isinstance(value, str):
340            self._feature_constraints = [value]
341        else:
342            self._feature_constraints = None
343
344# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
345# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
346# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 
347
348    def create_design(self):
349        """
350        Create the design of experiments based on the specified type and parameters.
351        """
352        for par in self.parameters:
353            if par['type'].lower() == "categorical":
354                if self.type != 'Sobol sequence':
355                    le = LabelEncoder()
356                    label = le.fit_transform(par['values'])
357                    par['values'] = label
358                    par['encoder'] = le
359                    self.lows[par['name']] = np.min(par['values'])
360                    self.highs[par['name']] = np.max(par['values'])
361                else:
362                    par['encoder'] = None
363            else:
364                self.lows[par['name']] = np.min(par['values'])
365                self.highs[par['name']] = np.max(par['values'])
366
367        pars = {par['name']: par['values'] for par in self.parameters}
368
369        if self.type == 'Full Factorial':
370            self.design = build.full_fact(pars)
371        elif self.type == 'Sobol sequence':
372            from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy
373            from ax.modelbridge.registry import Models
374            from ax.service.ax_client import AxClient, ObjectiveProperties
375
376            ax_client = AxClient()
377            params = []
378            for par in self.parameters:
379                if par['type'].lower() == "float":
380                    params.append({'name': par['name'],
381                                   'type': 'range',
382                                   'value_type': 'float',
383                                   'bounds': [float(np.min(par['values'])), float(np.max(par['values']))]})
384                elif par['type'].lower() in ["integer", 'int']:
385                    params.append({'name': par['name'],
386                                   'type': 'range',
387                                   'value_type': 'int',
388                                   'bounds': [int(np.min(par['values'])), int(np.max(par['values']))]})
389                else:
390                    params.append({'name': par['name'],
391                                   'type': 'choice',
392                                   'values': par['values']})
393            ax_client.create_experiment(
394                name="DOE",
395                parameters=params,
396                objectives={"response": ObjectiveProperties(minimize=False)},
397                parameter_constraints=self._feature_constraints
398            )
399            gs = GenerationStrategy(
400                steps=[GenerationStep(
401                    model=Models.SOBOL,
402                    num_trials=-1,
403                    should_deduplicate=True,
404                    model_kwargs={"seed": 165478},
405                    model_gen_kwargs={},
406                )]
407            )
408            generator_run = gs.gen(
409                experiment=ax_client.experiment,
410                data=None,
411                n=self.Nexp
412            )
413            if self.Nexp == 1:
414                ax_client.experiment.new_trial(generator_run)
415            else:
416                ax_client.experiment.new_batch_trial(generator_run)
417            trials = ax_client.get_trials_data_frame()
418            self.design = trials[trials['trial_status'] == 'CANDIDATE']
419            self.design = self._design.drop(columns=['trial_index',
420                                                      'trial_status',
421                                                      'arm_name',
422                                                      'generation_method',
423                                                      'generation_node'])
424        elif self.type == 'Fractional Factorial':
425            for par in range(len(self.parameters)):
426                if self.parameters[par]['type'] == "Numerical":
427                    self.parameters[par]['type'] = "Categorical"
428                    le = LabelEncoder()
429                    label = le.fit_transform(self.parameters[par]['values'])
430                    self.parameters[par]['values'] = label
431                    self.parameters[par]['encoder'] = le
432            design = gsd([len(par['values']) for par in self.parameters], self.reduction)
433            self.design = pd.DataFrame(design, columns=[par['name'] for par in self.parameters])
434        elif self.type == 'Definitive Screening':
435            params = {par['name']: [np.min(par['values']), np.max(par['values'])] for par in self.parameters}
436            self.design = dsd.generate(factors_dict=params)
437        elif self.type == 'Space Filling Latin Hypercube':
438            self.design = build.space_filling_lhs(pars, num_samples=self.Nexp)
439        elif self.type == 'Randomized Latin Hypercube':
440            self.design = build.lhs(pars, num_samples=self.Nexp)
441        elif self.type == 'Optimal':
442            reaction_design = build_optimal(
443                len(self.parameters),
444                order=ModelOrder(self.order),
445                run_count=self.Nexp)
446            reaction_design.columns = [par['name'] for par in self.parameters]
447            self.design = coded_to_actual(reaction_design, self._lows, self._highs)
448        elif self.type == 'Plackett-Burman':
449            self.design = build.plackett_burman(pars)
450        elif self.type == 'Box-Behnken':
451            if len(self.parameters) < 3 or any([len(par['values']) < 3 for par in self.parameters]):
452                self.design = pd.DataFrame({})
453                raise Warning("Box-Behnken design is not possible with less than 3 parameters and with less than 3 levels for any parameter.")
454            else:
455                self.design = build.box_behnken(d=pars)
456        elif self.type == 'Central Composite':
457            self.design = build.central_composite(pars, 
458                                                  center=self.center,
459                                                  alpha=self.alpha,
460                                                  face=self.face)
461        else:
462            raise Warning("Unknown design type. Must be one of: 'Full Factorial', 'Sobol sequence', 'Fractional Factorial', 'Definitive Screening', 'Space Filling Latin Hypercube', 'Randomized Latin Hypercube', 'Optimal', 'Plackett-Burman', 'Box-Behnken' or 'Central Composite'.")
463
464        for par in self.parameters:
465            if par['type'] == "Categorical" and self.type != 'Sobol sequence':
466                vals = self._design[par['name']].to_numpy()
467                self.design[par['name']] = par['encoder'].inverse_transform([int(v) for v in vals])
468
469        # randomize the run order
470        self.design['run_order'] = np.arange(len(self._design)) + 1
471        if self.randomize:
472            ord = self._design['run_order'].to_numpy()
473            self.design['run_order'] = np.random.permutation(ord)
474        cols = self._design.columns.tolist()
475        cols = cols[-1:] + cols[:-1]
476        self.design = self._design[cols]
477        # apply the column types
478        for col in self._design.columns:
479            for par in self.parameters:
480                if col == par['name']:
481                    if par['type'].lower() == "float":
482                        self.design[col] = self._design[col].astype(float)
483                    elif par['type'].lower() in ["int", "integer"]:
484                        self.design[col] = self._design[col].astype(int)
485                    else:
486                        self.design[col] = self._design[col].astype(str)
487        return self._design
488
489    def plot(self):
490        """
491        Plot the design of experiments.
492
493        Returns
494        -------
495        List of plotly.graph_objs._figure.Figure
496            A list of Plotly figures representing the design of experiments.
497        """
498        fig = []
499        count = 0
500        if len(self.design) > 0:
501            if len(self.parameters) <= 2:
502                # Create 2D scatter plots
503                for i, faci in enumerate(self.parameters):
504                    for j, facj in enumerate(self.parameters):
505                        if j > i:
506                            fig.append(px.scatter(
507                                self.design,
508                                x=facj['name'],
509                                y=faci['name'],
510                                title=f"""{faci['name']} vs {facj['name']}""",
511                                labels={facj['name']: facj['name'], faci['name']: faci['name']}
512                            ))
513                            fig[count].update_traces(marker=dict(size=10))
514                            fig[count].update_layout(
515                                margin=dict(l=10, r=10, t=50, b=50),
516                                xaxis=dict(
517                                    showgrid=True,
518                                    gridcolor="lightgray",
519                                    zeroline=False,
520                                    showline=True,
521                                    linewidth=1,
522                                    linecolor="black",
523                                    mirror=True
524                                ),
525                                yaxis=dict(
526                                    showgrid=True,
527                                    gridcolor="lightgray",
528                                    zeroline=False,
529                                    showline=True,
530                                    linewidth=1,
531                                    linecolor="black",
532                                    mirror=True
533                                ),
534                            )
535                            count += 1
536            else:
537                # Create 3D scatter plots
538                for k, (faci, facj, fack) in enumerate(combinations(self.parameters, 3)):
539                    fig.append(go.Figure(data=[go.Scatter3d(
540                        x=self.design[facj['name']],
541                        y=self.design[faci['name']],
542                        z=self.design[fack['name']],
543                        mode='markers',
544                        marker=dict(size=10, color='royalblue', opacity=0.7),
545                    )]))
546                    fig[count].update_layout(
547                        template='ggplot2',
548                        height=500,
549                        width=500,
550                        scene=dict(
551                            xaxis_title=facj['name'],
552                            yaxis_title=faci['name'],
553                            zaxis_title=fack['name'],
554                        ),
555                        title=f"{faci['name']} vs {facj['name']}<br>vs {fack['name']}",
556                        margin=dict(l=10, r=10, t=50, b=50)
557                    )
558                    count += 1
559        return fig

Class to create a design of experiments (DoE) for a given model. This class allows the user to specify the type of design, the parameters, and various options for the design generation. The design can be visualized using scatter plots.

Parameters
  • type (str): The type of design to create. Must be one of: 'Full Factorial', 'Sobol sequence', 'Fractional Factorial', 'Definitive Screening', 'Space Filling Latin Hypercube', 'Randomized Latin Hypercube', 'Optimal', 'Plackett-Burman', 'Box-Behnken' or 'Central Composite'.
  • parameters (List[Dict[str, Dict[str, Any]]]): List of parameters for the design, each with a dictionary of properties. Each dictionary should contain 'name', 'type', and 'values'. 'values' should be a list of possible values for the parameter. 'type' should be either "int", "integer", "float", "". Any will be considered as "categorical". 'values' should be a list of possible values for the parameter.
  • Nexp (int, optional): Number of experiments in the design, when applicable. Default is 4.
  • order (int, optional): Order of the model (for 'Optimal' design). Default is 2.
  • randomize (bool, optional): Whether to randomize the run order. Default is True.
  • reduction (int, optional): Reduction factor for 'Fractional Factorial' designs. Default is 2.
  • feature_constraints (Optional[List[Dict[str, Any]]], optional): Feature constraints of the experiment for Sobol sequence. Default is None. If a single dictionary is provided, it will be converted to a list. If a string is provided, it will be converted to a list with one element. If a list is provided, it will be used as is. If None, no constraints will be applied.
Attributes
  • type (str): The type of design.
  • parameters (List[Dict[str, Dict[str, Any]]]): The parameters for the design.
  • Nexp (int): Number of experiments in the design.
  • order (int): Order of the model.
  • randomize (bool): Whether to randomize the run order.
  • reduction (int): Reduction factor for 'Fractional Factorial' designs.
  • design (pd.DataFrame): The design DataFrame.
  • lows (Dict[str, float]): Lower bounds for the parameters.
  • highs (Dict[str, float]): Upper bounds for the parameters.
Methods
  • create_design(): Create the design of experiments based on the specified type and parameters.
  • plot(): Plot the design of experiments using plotly.
Example
from doe import DesignOfExperiments
parameters = [
    {'name': 'Temperature', 'type': 'integer', 'values': [20, 30, 40]},
    {'name': 'Pressure', 'type': 'float', 'values': [1, 2, 3]},
    {'name': 'Catalyst', 'type': 'categorical', 'values': ['A', 'B', 'C']}
]
doe = DesignOfExperiments(
    type='Full Factorial',
    parameters=parameters
)
design = doe.design
print(design)
figs = doe.plot()
for fig in figs:
    fig.show()
DesignOfExperiments( type: str, parameters: List[Dict[str, Dict[str, Any]]], Nexp: int = 4, order: int = 2, randomize: bool = True, reduction: int = 2, feature_constraints: Optional[List[Dict[str, Any]]] = None, center=(2, 2), alpha='o', face='ccc')
132    def __init__(self, 
133                 type: str,
134                 parameters: List[Dict[str, Dict[str, Any]]],
135                 Nexp: int = 4, 
136                 order: int = 2, 
137                 randomize: bool = True, 
138                 reduction: int = 2,
139                 feature_constraints: Optional[List[Dict[str, Any]]] = None,
140                 center=(2,2),
141                 alpha='o',
142                 face='ccc'):
143        self.type = type
144        self.parameters = parameters
145        self.Nexp = Nexp
146        self.order = order
147        self.randomize = randomize
148        self.reduction = reduction
149        self.center = center
150        self.alpha = alpha
151        self.face = face
152        self.design = None
153        self.lows = {}
154        self.feature_constraints = feature_constraints
155        self.highs = {}
156        self.create_design()
type: str
181    @property
182    def type(self) -> str:
183        """The type of design to create. Must be one of: `'Full Factorial'`, `'Sobol sequence'`, `'Fractional Factorial'`, `'Definitive Screening'`, `'Space Filling Latin Hypercube'`, `'Randomized Latin Hypercube'`, `'Optimal'`, `'Plackett-Burman'`, `'Box-Behnken'` or `'Central Composite'`."""
184        return self._type

The type of design to create. Must be one of: 'Full Factorial', 'Sobol sequence', 'Fractional Factorial', 'Definitive Screening', 'Space Filling Latin Hypercube', 'Randomized Latin Hypercube', 'Optimal', 'Plackett-Burman', 'Box-Behnken' or 'Central Composite'.

parameters: List[Dict[str, Dict[str, Any]]]
191    @property
192    def parameters(self) -> List[Dict[str, Dict[str, Any]]]:
193        """List of parameters for the design, each with a dictionary of properties.
194            Each dictionary should contain the keys `"name"`, `"type"`, and `"values"`.
195            `"values"` should be a list of possible values for the parameter.
196            `"type"` should be either `"int"`, `"integer"`, `"float"`, `"<other>"`. 
197            Any `"<other>"` will be considered as `"categorical"`.
198            `values` should be a list of possible values for the parameter."""
199        return self._parameters

List of parameters for the design, each with a dictionary of properties. Each dictionary should contain the keys "name", "type", and "values". "values" should be a list of possible values for the parameter. "type" should be either "int", "integer", "float", "<other>". Any "<other>" will be considered as "categorical". values should be a list of possible values for the parameter.

Nexp: int
206    @property
207    def Nexp(self) -> int:
208        """Number of experiments in the design, when applicable. Default is `4`."""
209        return self._Nexp

Number of experiments in the design, when applicable. Default is 4.

order: int
216    @property
217    def order(self) -> int:
218        """Order of the model (for `'Optimal'` design). Default is `2`."""
219        return self._order

Order of the model (for 'Optimal' design). Default is 2.

randomize: bool
293    @property
294    def randomize(self) -> bool:
295        """Whether to randomize the run order. Default is `True`."""
296        return self._randomize

Whether to randomize the run order. Default is True.

reduction: int
303    @property
304    def reduction(self) -> int:
305        """Reduction factor for `'Fractional Factorial'` designs. Default is `2`."""
306        return self._reduction

Reduction factor for 'Fractional Factorial' designs. Default is 2.

center: tuple
221    @property
222    def center(self) -> tuple:
223        """Center for the Central Composite Design. Must be a tuple of two values."""
224        return self._center

Center for the Central Composite Design. Must be a tuple of two values.

alpha: str
237    @property
238    def alpha(self) -> str:
239        """Alpha for the Central Composite Design. Default is `'o'` (orthogonal).
240        Can be either `'o'` or `'r'` (rotatable)."""
241        return self._alpha

Alpha for the Central Composite Design. Default is 'o' (orthogonal). Can be either 'o' or 'r' (rotatable).

face: str
250    @property
251    def face(self) -> str:
252        """The relation between the start points and the corner (factorial) points for the Central Composite Design.
253        
254        There are three possible options for this input:
255        
256        1. 'circumscribed' or 'ccc' (Default)
257        2. 'inscribed' or 'cci'
258        3. 'faced' or 'ccf'"""
259        return self._face

The relation between the start points and the corner (factorial) points for the Central Composite Design.

There are three possible options for this input:

  1. 'circumscribed' or 'ccc' (Default)
  2. 'inscribed' or 'cci'
  3. 'faced' or 'ccf'
design: pandas.core.frame.DataFrame
313    @property
314    def design(self) -> pd.DataFrame:
315        """Get the design DataFrame."""
316        return self._design

Get the design DataFrame.

lows: Dict[str, float]
268    @property
269    def lows(self) -> Dict[str, float]:
270        """Get the lower bounds for the parameters."""
271        return self._lows

Get the lower bounds for the parameters.

feature_constraints
323    @property
324    def feature_constraints(self):
325        """
326        Get the feature constraints of the experiment for Sobol sequence.
327        """
328        return self._feature_constraints

Get the feature constraints of the experiment for Sobol sequence.

highs: Dict[str, float]
278    @property
279    def highs(self) -> Dict[str, float]:
280        """Get the upper bounds for the parameters."""
281        return self._highs

Get the upper bounds for the parameters.

def create_design(self):
348    def create_design(self):
349        """
350        Create the design of experiments based on the specified type and parameters.
351        """
352        for par in self.parameters:
353            if par['type'].lower() == "categorical":
354                if self.type != 'Sobol sequence':
355                    le = LabelEncoder()
356                    label = le.fit_transform(par['values'])
357                    par['values'] = label
358                    par['encoder'] = le
359                    self.lows[par['name']] = np.min(par['values'])
360                    self.highs[par['name']] = np.max(par['values'])
361                else:
362                    par['encoder'] = None
363            else:
364                self.lows[par['name']] = np.min(par['values'])
365                self.highs[par['name']] = np.max(par['values'])
366
367        pars = {par['name']: par['values'] for par in self.parameters}
368
369        if self.type == 'Full Factorial':
370            self.design = build.full_fact(pars)
371        elif self.type == 'Sobol sequence':
372            from ax.modelbridge.generation_strategy import GenerationStep, GenerationStrategy
373            from ax.modelbridge.registry import Models
374            from ax.service.ax_client import AxClient, ObjectiveProperties
375
376            ax_client = AxClient()
377            params = []
378            for par in self.parameters:
379                if par['type'].lower() == "float":
380                    params.append({'name': par['name'],
381                                   'type': 'range',
382                                   'value_type': 'float',
383                                   'bounds': [float(np.min(par['values'])), float(np.max(par['values']))]})
384                elif par['type'].lower() in ["integer", 'int']:
385                    params.append({'name': par['name'],
386                                   'type': 'range',
387                                   'value_type': 'int',
388                                   'bounds': [int(np.min(par['values'])), int(np.max(par['values']))]})
389                else:
390                    params.append({'name': par['name'],
391                                   'type': 'choice',
392                                   'values': par['values']})
393            ax_client.create_experiment(
394                name="DOE",
395                parameters=params,
396                objectives={"response": ObjectiveProperties(minimize=False)},
397                parameter_constraints=self._feature_constraints
398            )
399            gs = GenerationStrategy(
400                steps=[GenerationStep(
401                    model=Models.SOBOL,
402                    num_trials=-1,
403                    should_deduplicate=True,
404                    model_kwargs={"seed": 165478},
405                    model_gen_kwargs={},
406                )]
407            )
408            generator_run = gs.gen(
409                experiment=ax_client.experiment,
410                data=None,
411                n=self.Nexp
412            )
413            if self.Nexp == 1:
414                ax_client.experiment.new_trial(generator_run)
415            else:
416                ax_client.experiment.new_batch_trial(generator_run)
417            trials = ax_client.get_trials_data_frame()
418            self.design = trials[trials['trial_status'] == 'CANDIDATE']
419            self.design = self._design.drop(columns=['trial_index',
420                                                      'trial_status',
421                                                      'arm_name',
422                                                      'generation_method',
423                                                      'generation_node'])
424        elif self.type == 'Fractional Factorial':
425            for par in range(len(self.parameters)):
426                if self.parameters[par]['type'] == "Numerical":
427                    self.parameters[par]['type'] = "Categorical"
428                    le = LabelEncoder()
429                    label = le.fit_transform(self.parameters[par]['values'])
430                    self.parameters[par]['values'] = label
431                    self.parameters[par]['encoder'] = le
432            design = gsd([len(par['values']) for par in self.parameters], self.reduction)
433            self.design = pd.DataFrame(design, columns=[par['name'] for par in self.parameters])
434        elif self.type == 'Definitive Screening':
435            params = {par['name']: [np.min(par['values']), np.max(par['values'])] for par in self.parameters}
436            self.design = dsd.generate(factors_dict=params)
437        elif self.type == 'Space Filling Latin Hypercube':
438            self.design = build.space_filling_lhs(pars, num_samples=self.Nexp)
439        elif self.type == 'Randomized Latin Hypercube':
440            self.design = build.lhs(pars, num_samples=self.Nexp)
441        elif self.type == 'Optimal':
442            reaction_design = build_optimal(
443                len(self.parameters),
444                order=ModelOrder(self.order),
445                run_count=self.Nexp)
446            reaction_design.columns = [par['name'] for par in self.parameters]
447            self.design = coded_to_actual(reaction_design, self._lows, self._highs)
448        elif self.type == 'Plackett-Burman':
449            self.design = build.plackett_burman(pars)
450        elif self.type == 'Box-Behnken':
451            if len(self.parameters) < 3 or any([len(par['values']) < 3 for par in self.parameters]):
452                self.design = pd.DataFrame({})
453                raise Warning("Box-Behnken design is not possible with less than 3 parameters and with less than 3 levels for any parameter.")
454            else:
455                self.design = build.box_behnken(d=pars)
456        elif self.type == 'Central Composite':
457            self.design = build.central_composite(pars, 
458                                                  center=self.center,
459                                                  alpha=self.alpha,
460                                                  face=self.face)
461        else:
462            raise Warning("Unknown design type. Must be one of: 'Full Factorial', 'Sobol sequence', 'Fractional Factorial', 'Definitive Screening', 'Space Filling Latin Hypercube', 'Randomized Latin Hypercube', 'Optimal', 'Plackett-Burman', 'Box-Behnken' or 'Central Composite'.")
463
464        for par in self.parameters:
465            if par['type'] == "Categorical" and self.type != 'Sobol sequence':
466                vals = self._design[par['name']].to_numpy()
467                self.design[par['name']] = par['encoder'].inverse_transform([int(v) for v in vals])
468
469        # randomize the run order
470        self.design['run_order'] = np.arange(len(self._design)) + 1
471        if self.randomize:
472            ord = self._design['run_order'].to_numpy()
473            self.design['run_order'] = np.random.permutation(ord)
474        cols = self._design.columns.tolist()
475        cols = cols[-1:] + cols[:-1]
476        self.design = self._design[cols]
477        # apply the column types
478        for col in self._design.columns:
479            for par in self.parameters:
480                if col == par['name']:
481                    if par['type'].lower() == "float":
482                        self.design[col] = self._design[col].astype(float)
483                    elif par['type'].lower() in ["int", "integer"]:
484                        self.design[col] = self._design[col].astype(int)
485                    else:
486                        self.design[col] = self._design[col].astype(str)
487        return self._design

Create the design of experiments based on the specified type and parameters.

def plot(self):
489    def plot(self):
490        """
491        Plot the design of experiments.
492
493        Returns
494        -------
495        List of plotly.graph_objs._figure.Figure
496            A list of Plotly figures representing the design of experiments.
497        """
498        fig = []
499        count = 0
500        if len(self.design) > 0:
501            if len(self.parameters) <= 2:
502                # Create 2D scatter plots
503                for i, faci in enumerate(self.parameters):
504                    for j, facj in enumerate(self.parameters):
505                        if j > i:
506                            fig.append(px.scatter(
507                                self.design,
508                                x=facj['name'],
509                                y=faci['name'],
510                                title=f"""{faci['name']} vs {facj['name']}""",
511                                labels={facj['name']: facj['name'], faci['name']: faci['name']}
512                            ))
513                            fig[count].update_traces(marker=dict(size=10))
514                            fig[count].update_layout(
515                                margin=dict(l=10, r=10, t=50, b=50),
516                                xaxis=dict(
517                                    showgrid=True,
518                                    gridcolor="lightgray",
519                                    zeroline=False,
520                                    showline=True,
521                                    linewidth=1,
522                                    linecolor="black",
523                                    mirror=True
524                                ),
525                                yaxis=dict(
526                                    showgrid=True,
527                                    gridcolor="lightgray",
528                                    zeroline=False,
529                                    showline=True,
530                                    linewidth=1,
531                                    linecolor="black",
532                                    mirror=True
533                                ),
534                            )
535                            count += 1
536            else:
537                # Create 3D scatter plots
538                for k, (faci, facj, fack) in enumerate(combinations(self.parameters, 3)):
539                    fig.append(go.Figure(data=[go.Scatter3d(
540                        x=self.design[facj['name']],
541                        y=self.design[faci['name']],
542                        z=self.design[fack['name']],
543                        mode='markers',
544                        marker=dict(size=10, color='royalblue', opacity=0.7),
545                    )]))
546                    fig[count].update_layout(
547                        template='ggplot2',
548                        height=500,
549                        width=500,
550                        scene=dict(
551                            xaxis_title=facj['name'],
552                            yaxis_title=faci['name'],
553                            zaxis_title=fack['name'],
554                        ),
555                        title=f"{faci['name']} vs {facj['name']}<br>vs {fack['name']}",
556                        margin=dict(l=10, r=10, t=50, b=50)
557                    )
558                    count += 1
559        return fig

Plot the design of experiments.

Returns
  • List of plotly.graph_objs._figure.Figure: A list of Plotly figures representing the design of experiments.