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
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()
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()
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'
.
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.
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
.
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
.
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
.
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
.
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.
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).
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:
- 'circumscribed' or 'ccc' (Default)
- 'inscribed' or 'cci'
- 'faced' or 'ccf'
313 @property 314 def design(self) -> pd.DataFrame: 315 """Get the design DataFrame.""" 316 return self._design
Get the design DataFrame.
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.
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.
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.
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.
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.