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

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

order: int
235    @property
236    def order(self) -> int:
237        """Order of the model (for `'Optimal'` design). Default is `2`."""
238        return self._order

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

randomize: bool
312    @property
313    def randomize(self) -> bool:
314        """Whether to randomize the run order. Default is `True`."""
315        return self._randomize

Whether to randomize the run order. Default is True.

reduction: int
322    @property
323    def reduction(self) -> int:
324        """Reduction factor for `'Fractional Factorial'` designs. Default is `2`."""
325        return self._reduction

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

center: tuple
240    @property
241    def center(self) -> tuple:
242        """Center for the Central Composite Design. Must be a tuple of two values."""
243        return self._center

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

alpha: str
256    @property
257    def alpha(self) -> str:
258        """Alpha for the Central Composite Design. Default is `'o'` (orthogonal).
259        Can be either `'o'` or `'r'` (rotatable)."""
260        return self._alpha

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

face: str
269    @property
270    def face(self) -> str:
271        """The relation between the start points and the corner (factorial) points for the Central Composite Design.
272        
273        There are three possible options for this input:
274        
275        1. 'circumscribed' or 'ccc' (Default)
276        2. 'inscribed' or 'cci'
277        3. 'faced' or 'ccf'"""
278        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
332    @property
333    def design(self) -> pd.DataFrame:
334        """Get the design DataFrame."""
335        return self._design

Get the design DataFrame.

lows: Dict[str, float]
287    @property
288    def lows(self) -> Dict[str, float]:
289        """Get the lower bounds for the parameters."""
290        return self._lows

Get the lower bounds for the parameters.

feature_constraints
342    @property
343    def feature_constraints(self):
344        """
345        Get the feature constraints of the experiment for Sobol sequence.
346        """
347        return self._feature_constraints

Get the feature constraints of the experiment for Sobol sequence.

highs: Dict[str, float]
297    @property
298    def highs(self) -> Dict[str, float]:
299        """Get the upper bounds for the parameters."""
300        return self._highs

Get the upper bounds for the parameters.

seed: int
194    @property
195    def seed(self) -> int:
196        """Random seed for reproducibility. Default is 42."""
197        return self._seed

Random seed for reproducibility. Default is 42.

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

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

def plot(self):
508    def plot(self):
509        """
510        Plot the design of experiments.
511
512        Returns
513        -------
514        List of plotly.graph_objs._figure.Figure
515            A list of Plotly figures representing the design of experiments.
516        """
517        fig = []
518        count = 0
519        if len(self.design) > 0:
520            if len(self.parameters) <= 2:
521                # Create 2D scatter plots
522                for i, faci in enumerate(self.parameters):
523                    for j, facj in enumerate(self.parameters):
524                        if j > i:
525                            fig.append(px.scatter(
526                                self.design,
527                                x=facj['name'],
528                                y=faci['name'],
529                                title=f"""{faci['name']} vs {facj['name']}""",
530                                labels={facj['name']: facj['name'], faci['name']: faci['name']}
531                            ))
532                            fig[count].update_traces(marker=dict(size=10))
533                            fig[count].update_layout(
534                                margin=dict(l=10, r=10, t=50, b=50),
535                                xaxis=dict(
536                                    showgrid=True,
537                                    gridcolor="lightgray",
538                                    zeroline=False,
539                                    showline=True,
540                                    linewidth=1,
541                                    linecolor="black",
542                                    mirror=True
543                                ),
544                                yaxis=dict(
545                                    showgrid=True,
546                                    gridcolor="lightgray",
547                                    zeroline=False,
548                                    showline=True,
549                                    linewidth=1,
550                                    linecolor="black",
551                                    mirror=True
552                                ),
553                            )
554                            count += 1
555            else:
556                # Create 3D scatter plots
557                for k, (faci, facj, fack) in enumerate(combinations(self.parameters, 3)):
558                    fig.append(go.Figure(data=[go.Scatter3d(
559                        x=self.design[facj['name']],
560                        y=self.design[faci['name']],
561                        z=self.design[fack['name']],
562                        mode='markers',
563                        marker=dict(size=10, color='royalblue', opacity=0.7),
564                    )]))
565                    fig[count].update_layout(
566                        template='ggplot2',
567                        height=500,
568                        width=500,
569                        scene=dict(
570                            xaxis_title=facj['name'],
571                            yaxis_title=faci['name'],
572                            zaxis_title=fack['name'],
573                        ),
574                        title=f"{faci['name']} vs {facj['name']}<br>vs {fack['name']}",
575                        margin=dict(l=10, r=10, t=50, b=50)
576                    )
577                    count += 1
578        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.