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