process-improve 1.40.2__tar.gz → 1.44.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. {process_improve-1.40.2 → process_improve-1.44.0}/PKG-INFO +1 -1
  2. {process_improve-1.40.2 → process_improve-1.44.0}/pyproject.toml +1 -1
  3. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/__init__.py +5 -1
  4. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs.py +36 -7
  5. process_improve-1.44.0/src/process_improve/experiments/designs_omars.py +274 -0
  6. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs_response_surface.py +164 -1
  7. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/evaluate.py +429 -50
  8. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/data/design_types.yaml +54 -0
  9. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/monitoring/control_charts.py +8 -6
  10. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_pca.py +3 -1
  11. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_pls.py +3 -1
  12. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_preprocessing.py +11 -1
  13. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_tpls.py +1 -1
  14. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/univariate/metrics.py +5 -1
  15. {process_improve-1.40.2 → process_improve-1.44.0}/README.md +0 -0
  16. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/__init__.py +0 -0
  17. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/_extras.py +0 -0
  18. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/_linalg.py +0 -0
  19. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/_random.py +0 -0
  20. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/__init__.py +0 -0
  21. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/alignment_helpers.py +0 -0
  22. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/data_input.py +0 -0
  23. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/features.py +0 -0
  24. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/plotting.py +0 -0
  25. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/preprocessing.py +0 -0
  26. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/batch/tools.py +0 -0
  27. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/bivariate/__init__.py +0 -0
  28. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/bivariate/_elbow_peak.py +0 -0
  29. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/bivariate/methods.py +0 -0
  30. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/bivariate/tools.py +0 -0
  31. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/config.py +0 -0
  32. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/batch/batch-fake-data.csv +0 -0
  33. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/batch/details.txt +0 -0
  34. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/batch/dryer.csv +0 -0
  35. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/batch/nylon.csv +0 -0
  36. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/experiments/boilingpot.csv +0 -0
  37. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/experiments/test_doe1.csv +0 -0
  38. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/monitoring/batch-yield-and-purity.csv +0 -0
  39. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/monitoring/rubber-colour.csv +0 -0
  40. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/C.csv +0 -0
  41. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/Hotellings_T2_A3.csv +0 -0
  42. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/Hotellings_T2_A6.csv +0 -0
  43. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/LDPE.csv +0 -0
  44. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/P.csv +0 -0
  45. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/R.csv +0 -0
  46. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/T.csv +0 -0
  47. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/U.csv +0 -0
  48. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/W.csv +0 -0
  49. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/LDPE/Yhat_A6.csv +0 -0
  50. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/NOTICE +0 -0
  51. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/extraction1.csv +0 -0
  52. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/extraction2.csv +0 -0
  53. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/extraction3.csv +0 -0
  54. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/ftir1.csv +0 -0
  55. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/ftir2.csv +0 -0
  56. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/dtu-pectin/ftir3.csv +0 -0
  57. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/kamyr.csv +0 -0
  58. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tablet-spectra.csv +0 -0
  59. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/SOURCE +0 -0
  60. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group1.csv +0 -0
  61. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group2.csv +0 -0
  62. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group3.csv +0 -0
  63. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group4.csv +0 -0
  64. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/formulas_Group5.csv +0 -0
  65. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/process_conditions.csv +0 -0
  66. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group1.csv +0 -0
  67. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group2.csv +0 -0
  68. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group3.csv +0 -0
  69. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group4.csv +0 -0
  70. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/properties_Group5.csv +0 -0
  71. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/datasets/multivariate/tpls-pyphi/quality_indicators.csv +0 -0
  72. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/__init__.py +0 -0
  73. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/_shared.py +0 -0
  74. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/box_cox.py +0 -0
  75. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/curvature.py +0 -0
  76. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/diagnostics.py +0 -0
  77. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/lack_of_fit.py +0 -0
  78. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/lenth.py +0 -0
  79. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/model_selection.py +0 -0
  80. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/ols_extractors.py +0 -0
  81. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_analyses/prediction.py +0 -0
  82. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_lm.py +0 -0
  83. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/__init__.py +0 -0
  84. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/analyze_experiment.py +0 -0
  85. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/augment_design.py +0 -0
  86. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/create_factorial_design.py +0 -0
  87. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/doe_knowledge.py +0 -0
  88. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/evaluate_design.py +0 -0
  89. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/fit_linear_model.py +0 -0
  90. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/generate_design.py +0 -0
  91. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/optimize_responses.py +0 -0
  92. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/recommend_strategy.py +0 -0
  93. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/_tools/visualize_doe.py +0 -0
  94. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/analysis.py +0 -0
  95. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/augment.py +0 -0
  96. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/datasets.py +0 -0
  97. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs_factorial.py +0 -0
  98. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs_mixture.py +0 -0
  99. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs_optimal.py +0 -0
  100. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs_screening.py +0 -0
  101. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/designs_utils.py +0 -0
  102. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/factor.py +0 -0
  103. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/__init__.py +0 -0
  104. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/api.py +0 -0
  105. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/data/concepts.yaml +0 -0
  106. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/data/decision_rules.yaml +0 -0
  107. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/data/diagnostics.yaml +0 -0
  108. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/engine.py +0 -0
  109. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/knowledge/models.py +0 -0
  110. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/models.py +0 -0
  111. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/optimal.py +0 -0
  112. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/optimization.py +0 -0
  113. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/simulations.py +0 -0
  114. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/strategy/__init__.py +0 -0
  115. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/strategy/budget.py +0 -0
  116. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/strategy/domain_templates.py +0 -0
  117. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/strategy/engine.py +0 -0
  118. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/strategy/models.py +0 -0
  119. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/structures.py +0 -0
  120. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/tools.py +0 -0
  121. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/__init__.py +0 -0
  122. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/api.py +0 -0
  123. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/__init__.py +0 -0
  124. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/cube_plot.py +0 -0
  125. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/design_quality.py +0 -0
  126. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/diagnostics.py +0 -0
  127. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/effects.py +0 -0
  128. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/optimization_plots.py +0 -0
  129. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/registry.py +0 -0
  130. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/significance.py +0 -0
  131. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/square_plot.py +0 -0
  132. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/experiments/visualization/plots/surfaces.py +0 -0
  133. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/mcp_server.py +0 -0
  134. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/monitoring/__init__.py +0 -0
  135. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/monitoring/metrics.py +0 -0
  136. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/monitoring/tools.py +0 -0
  137. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/__init__.py +0 -0
  138. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_base.py +0 -0
  139. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_common.py +0 -0
  140. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_diagnostics.py +0 -0
  141. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_limits.py +0 -0
  142. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_mbpca.py +0 -0
  143. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_mbpls.py +0 -0
  144. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_nipals.py +0 -0
  145. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_pca_pls.py +0 -0
  146. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/_resampling.py +0 -0
  147. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/methods.py +0 -0
  148. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/plots.py +0 -0
  149. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/multivariate/tools.py +0 -0
  150. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/py.typed +0 -0
  151. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/regression/__init__.py +0 -0
  152. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/regression/_robust_regression.py +0 -0
  153. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/regression/methods.py +0 -0
  154. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/regression/tools.py +0 -0
  155. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/simulation/__init__.py +0 -0
  156. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/simulation/context.py +0 -0
  157. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/simulation/model.py +0 -0
  158. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/simulation/tools.py +0 -0
  159. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/tool_safety.py +0 -0
  160. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/tool_spec.py +0 -0
  161. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/univariate/__init__.py +0 -0
  162. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/univariate/tools.py +0 -0
  163. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/__init__.py +0 -0
  164. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/adapters/__init__.py +0 -0
  165. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/adapters/base.py +0 -0
  166. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/adapters/echarts_adapter.py +0 -0
  167. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/adapters/plotly_adapter.py +0 -0
  168. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/charts/__init__.py +0 -0
  169. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/charts/boxplot.py +0 -0
  170. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/colors.py +0 -0
  171. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/raincloud.py +0 -0
  172. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/spec.py +0 -0
  173. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/themes.py +0 -0
  174. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/tools.py +0 -0
  175. {process_improve-1.40.2 → process_improve-1.44.0}/src/process_improve/visualization/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: process-improve
3
- Version: 1.40.2
3
+ Version: 1.44.0
4
4
  Summary: Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.
5
5
  Keywords: Designed Experiments,Latent Variables,PCA,PLS,Multivariate Data Analysis,Batch data analysis
6
6
  Author: Kevin Dunn
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "process-improve"
3
- version = "1.40.2"
3
+ version = "1.44.0"
4
4
  description = 'Designed Experiments; Latent Variables (PCA, PLS, multivariate methods with missing data); Process Monitoring; Batch data analysis.'
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -4,7 +4,8 @@ from process_improve.experiments.analysis import analyze_experiment
4
4
  from process_improve.experiments.augment import augment_design
5
5
  from process_improve.experiments.designs import generate_design
6
6
  from process_improve.experiments.designs_factorial import full_factorial
7
- from process_improve.experiments.evaluate import evaluate_design
7
+ from process_improve.experiments.designs_omars import is_omars, omars_properties
8
+ from process_improve.experiments.evaluate import evaluate_all, evaluate_design
8
9
  from process_improve.experiments.factor import Constraint, DesignResult, Factor, Response, ResponseGoal
9
10
  from process_improve.experiments.knowledge import doe_knowledge
10
11
  from process_improve.experiments.models import Model, lm, predict, summary
@@ -33,13 +34,16 @@ __all__ = [
33
34
  "augment_design",
34
35
  "c",
35
36
  "doe_knowledge",
37
+ "evaluate_all",
36
38
  "evaluate_design",
37
39
  "expand_grid",
38
40
  "full_factorial",
39
41
  "gather",
40
42
  "generate_design",
43
+ "is_omars",
41
44
  "lm",
42
45
  "main_effects_plot",
46
+ "omars_properties",
43
47
  "optimize_responses",
44
48
  "predict",
45
49
  "recommend_strategy",
@@ -31,6 +31,7 @@ try:
31
31
  from pyDOE3 import ff2n
32
32
  except ImportError: # pragma: no cover - exercised via env-without-pyDOE3
33
33
  from process_improve._extras import _MissingExtra
34
+
34
35
  ff2n = _MissingExtra("pyDOE3", "expt") # type: ignore[assignment]
35
36
 
36
37
  from process_improve.experiments.designs_utils import build_design_result
@@ -92,6 +93,9 @@ def _dispatch_ccd(
92
93
  factors,
93
94
  center_points=kwargs.get("center_points", 3),
94
95
  alpha=kwargs.get("alpha"),
96
+ cube=kwargs.get("cube", "full"),
97
+ generators=kwargs.get("generators"),
98
+ resolution=kwargs.get("resolution"),
95
99
  )
96
100
 
97
101
 
@@ -104,6 +108,15 @@ def _dispatch_dsd(
104
108
  return dispatch_dsd(factors)
105
109
 
106
110
 
111
+ def _dispatch_omars(
112
+ factors: list[Factor],
113
+ **kwargs: Any, # noqa: ANN401
114
+ ) -> tuple[np.ndarray, dict]:
115
+ from process_improve.experiments.designs_omars import dispatch_omars # noqa: PLC0415
116
+
117
+ return dispatch_omars(factors)
118
+
119
+
107
120
  def _dispatch_d_optimal(
108
121
  factors: list[Factor],
109
122
  **kwargs: Any, # noqa: ANN401
@@ -178,6 +191,7 @@ _DESIGN_REGISTRY: dict[str, Callable[..., tuple[np.ndarray, dict]]] = {
178
191
  "box_behnken": _dispatch_box_behnken,
179
192
  "ccd": _dispatch_ccd,
180
193
  "dsd": _dispatch_dsd,
194
+ "omars": _dispatch_omars,
181
195
  "d_optimal": _dispatch_d_optimal,
182
196
  "i_optimal": _dispatch_i_optimal,
183
197
  "a_optimal": _dispatch_a_optimal,
@@ -255,6 +269,7 @@ def generate_design( # noqa: PLR0913
255
269
  resolution: int | None = None,
256
270
  generators: list[str] | None = None,
257
271
  alpha: str | float | None = None,
272
+ cube: str = "full",
258
273
  constraints: list[Constraint] | None = None,
259
274
  hard_to_change: list[str] | None = None,
260
275
  random_seed: int = 42,
@@ -271,8 +286,8 @@ def generate_design( # noqa: PLR0913
271
286
  design_type : str or None
272
287
  One of ``"full_factorial"``, ``"fractional_factorial"``,
273
288
  ``"plackett_burman"``, ``"box_behnken"``, ``"ccd"``, ``"dsd"``,
274
- ``"d_optimal"``, ``"i_optimal"``, ``"a_optimal"``, ``"mixture"``,
275
- ``"taguchi"``.
289
+ ``"omars"``, ``"d_optimal"``, ``"i_optimal"``, ``"a_optimal"``,
290
+ ``"mixture"``, ``"taguchi"``.
276
291
  If ``None``, the design type is chosen automatically based on the
277
292
  factor count, budget, and constraints.
278
293
  budget : int or None
@@ -293,6 +308,13 @@ def generate_design( # noqa: PLR0913
293
308
  alpha : str, float, or None
294
309
  Axial distance for CCD designs: ``"rotatable"``,
295
310
  ``"face_centered"``, ``"orthogonal"``, or a numeric value.
311
+ cube : str
312
+ For CCD designs, how to build the cube (factorial) portion:
313
+ ``"full"`` (default) uses the complete 2^k factorial; ``"fractional"``
314
+ uses a resolution-V (or higher) fractional factorial, keeping the run
315
+ count practical for k >= 5. When ``"fractional"`` and *generators* is
316
+ given, those generators define the cube; otherwise a minimum-aberration
317
+ half-fraction is chosen automatically.
296
318
  constraints : list[Constraint] or None
297
319
  Constraints on the factor space.
298
320
  hard_to_change : list[str] or None
@@ -331,10 +353,7 @@ def generate_design( # noqa: PLR0913
331
353
  design_type = _auto_select(factors, budget, constraints, hard_to_change)
332
354
 
333
355
  if design_type not in _DESIGN_REGISTRY:
334
- raise ValueError(
335
- f"Unknown design_type={design_type!r}. "
336
- f"Choose from: {', '.join(sorted(_DESIGN_REGISTRY))}."
337
- )
356
+ raise ValueError(f"Unknown design_type={design_type!r}. Choose from: {', '.join(sorted(_DESIGN_REGISTRY))}.")
338
357
 
339
358
  # --- Dispatch ----------------------------------------------------------
340
359
  dispatch_fn = _DESIGN_REGISTRY[design_type]
@@ -346,6 +365,7 @@ def generate_design( # noqa: PLR0913
346
365
  "resolution": resolution,
347
366
  "generators": generators,
348
367
  "alpha": alpha,
368
+ "cube": cube,
349
369
  "hard_to_change": hard_to_change,
350
370
  "constraints": constraints,
351
371
  }
@@ -355,7 +375,16 @@ def generate_design( # noqa: PLR0913
355
375
  # --- Determine center-point handling -----------------------------------
356
376
  # Designs that embed their own center points (CCD, Box-Behnken)
357
377
  # already include them; don't add more.
358
- designs_with_embedded_centers = {"ccd", "box_behnken", "dsd", "mixture", "d_optimal", "i_optimal", "a_optimal"}
378
+ designs_with_embedded_centers = {
379
+ "ccd",
380
+ "box_behnken",
381
+ "dsd",
382
+ "omars",
383
+ "mixture",
384
+ "d_optimal",
385
+ "i_optimal",
386
+ "a_optimal",
387
+ }
359
388
 
360
389
  # Optimal designs from pyoptex produce a pre-optimized run order
361
390
  # (especially important for split-plot). Skip randomization for these.
@@ -0,0 +1,274 @@
1
+ # (c) Kevin Dunn, 2010-2026. MIT License.
2
+
3
+ """OMARS (Orthogonal Minimally Aliased Response Surface) designs.
4
+
5
+ OMARS designs are three-level designs (coded ``-1 / 0 / +1``) in which every
6
+ main effect is orthogonal to every other main effect **and** to all
7
+ second-order terms (the pure quadratics and the two-factor interactions),
8
+ confining all aliasing to the second-order block. They occupy the middle
9
+ ground between screening designs and full response-surface designs.
10
+
11
+ This module provides two things:
12
+
13
+ * :func:`dispatch_omars` - a constructive generator wired into
14
+ :func:`process_improve.experiments.generate_design` as
15
+ ``design_type="omars"``. It builds the conference-matrix foldover family of
16
+ OMARS designs; the definitive screening design (DSD) is the minimal member
17
+ of that family, so the construction is shared with
18
+ :func:`process_improve.experiments.designs_response_surface.dispatch_dsd`.
19
+ * :func:`omars_properties` and :func:`is_omars` - dependency-free verifiers
20
+ that check the defining OMARS properties on *any* coded design matrix. Use
21
+ them to validate designs we generate, designs produced by a future
22
+ enumerator, or designs supplied from an external source.
23
+
24
+ A public catalogue of enumerated OMARS designs exists, but it is unlicensed
25
+ and is therefore **not** redistributed with this package. The verifiers here
26
+ let a user cross-check a generated design against such a catalogue offline.
27
+
28
+ References
29
+ ----------
30
+ .. [1] Núñez Ares, J. and Goos, P. (2020). "Enumeration and multicriteria
31
+ selection of orthogonal minimally aliased response surface designs."
32
+ *Technometrics*, 62(1):21-36.
33
+ .. [2] Jones, B. and Nachtsheim, C. J. (2011). "A class of three-level
34
+ designs for definitive screening in the presence of second-order
35
+ effects." *Journal of Quality Technology*, 43(1):1-15.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import itertools
41
+ from typing import TYPE_CHECKING
42
+
43
+ import numpy as np
44
+
45
+ if TYPE_CHECKING:
46
+ from process_improve.experiments.factor import Factor
47
+
48
+ # Default numerical tolerance for treating an inner product as zero. The
49
+ # constructive designs are integer-valued so exact-zero comparisons would also
50
+ # work, but a small tolerance keeps the verifiers usable on floating-point or
51
+ # slightly perturbed matrices.
52
+ _DEFAULT_TOL = 1e-9
53
+
54
+
55
+ def _second_order_terms(matrix: np.ndarray) -> tuple[np.ndarray, list[str]]:
56
+ """Build the second-order model terms (quadratics + two-factor interactions).
57
+
58
+ Parameters
59
+ ----------
60
+ matrix : np.ndarray
61
+ Coded design matrix of shape ``(n_runs, n_factors)``.
62
+
63
+ Returns
64
+ -------
65
+ tuple[np.ndarray, list[str]]
66
+ An ``(n_runs, n_terms)`` array whose columns are the ``k`` pure
67
+ quadratics ``x_i^2`` followed by the ``k (k - 1) / 2`` two-factor
68
+ interactions ``x_i x_j``, and a matching list of human-readable term
69
+ names.
70
+ """
71
+ n_factors = matrix.shape[1]
72
+ columns: list[np.ndarray] = []
73
+ names: list[str] = []
74
+ for i in range(n_factors):
75
+ columns.append(matrix[:, i] * matrix[:, i])
76
+ names.append(f"x{i + 1}^2")
77
+ for i, j in itertools.combinations(range(n_factors), 2):
78
+ columns.append(matrix[:, i] * matrix[:, j])
79
+ names.append(f"x{i + 1}*x{j + 1}")
80
+ if not columns: # defensive: a zero-factor matrix has no second-order terms
81
+ return np.empty((matrix.shape[0], 0)), names
82
+ return np.column_stack(columns), names
83
+
84
+
85
+ def _max_abs_correlation(terms: np.ndarray) -> float:
86
+ """Return the largest absolute pairwise correlation among the columns of *terms*.
87
+
88
+ Constant columns (zero variance) are skipped. Returns ``0.0`` when fewer
89
+ than two non-constant columns are present.
90
+ """
91
+ if terms.shape[1] < 2:
92
+ return 0.0
93
+ centered = terms - terms.mean(axis=0, keepdims=True)
94
+ norms = np.linalg.norm(centered, axis=0)
95
+ keep = norms > 0
96
+ if keep.sum() < 2:
97
+ return 0.0
98
+ unit = centered[:, keep] / norms[keep]
99
+ corr = unit.T @ unit
100
+ off_diagonal = corr - np.diag(np.diag(corr))
101
+ return float(np.abs(off_diagonal).max())
102
+
103
+
104
+ def omars_properties(matrix: np.ndarray, *, tol: float = _DEFAULT_TOL) -> dict:
105
+ """Compute the OMARS-defining properties of a coded design matrix.
106
+
107
+ The three properties that define an OMARS design are:
108
+
109
+ 1. **Three levels** - every entry is one of ``-1``, ``0`` or ``+1``.
110
+ 2. **Orthogonal main effects** - the main-effect columns are mutually
111
+ orthogonal and balanced (each column sums to zero).
112
+ 3. **Minimally aliased** - every main effect is orthogonal to every
113
+ second-order term (all quadratics and all two-factor interactions);
114
+ aliasing is confined to the second-order block.
115
+
116
+ Parameters
117
+ ----------
118
+ matrix : np.ndarray
119
+ Coded design matrix of shape ``(n_runs, n_factors)``.
120
+ tol : float
121
+ Absolute tolerance below which an inner product is treated as zero.
122
+
123
+ Returns
124
+ -------
125
+ dict
126
+ A report with the keys ``n_runs``, ``n_factors``, ``levels``,
127
+ ``is_three_level``, ``quadratics_estimable``, ``is_balanced``,
128
+ ``main_effects_orthogonal``, ``main_effects_clear_of_second_order``,
129
+ ``max_main_effect_inner_product``,
130
+ ``max_main_vs_second_order_inner_product``,
131
+ ``max_second_order_correlation`` and ``is_omars``.
132
+
133
+ Notes
134
+ -----
135
+ ``quadratics_estimable`` is ``True`` only when every factor takes the
136
+ middle (``0``) level at least once. A two-level design (e.g. a full
137
+ factorial) would otherwise pass the orthogonality checks but has constant,
138
+ inestimable quadratic terms, so it is not an OMARS design.
139
+
140
+ Examples
141
+ --------
142
+ >>> import numpy as np
143
+ >>> # The minimal 3-factor OMARS / DSD (conference-matrix foldover).
144
+ >>> from process_improve.experiments.factor import Factor
145
+ >>> from process_improve.experiments.designs_omars import dispatch_omars
146
+ >>> coded, _ = dispatch_omars([Factor(name=n, low=-1, high=1) for n in "ABC"])
147
+ >>> omars_properties(coded)["is_omars"]
148
+ True
149
+ """
150
+ matrix = np.asarray(matrix, dtype=float)
151
+ n_runs, n_factors = matrix.shape
152
+
153
+ levels = sorted({round(float(v), 9) for v in np.unique(matrix)})
154
+
155
+ # Three-level validity: every entry is within tol of one of {-1, 0, +1}.
156
+ nearest = np.round(matrix)
157
+ is_three_level = bool(
158
+ np.all(np.abs(matrix - nearest) <= tol) and np.all(np.isin(nearest, (-1.0, 0.0, 1.0)))
159
+ )
160
+
161
+ # OMARS factors are genuinely three-level: each must take the middle (0)
162
+ # level at least once, otherwise its pure quadratic is a constant column
163
+ # and cannot be estimated (as in a two-level factorial).
164
+ uses_middle_level = np.any(np.abs(matrix) <= tol, axis=0)
165
+ quadratics_estimable = bool(np.all(uses_middle_level))
166
+
167
+ column_sums = np.abs(matrix.sum(axis=0))
168
+ is_balanced = bool(np.all(column_sums <= tol))
169
+
170
+ # Main-effect orthogonality: off-diagonal of X'X.
171
+ gram = matrix.T @ matrix
172
+ me_off_diagonal = gram - np.diag(np.diag(gram))
173
+ max_me_inner = float(np.abs(me_off_diagonal).max()) if n_factors > 1 else 0.0
174
+ main_effects_orthogonal = max_me_inner <= tol
175
+
176
+ # Main effects clear of all second-order terms.
177
+ second_order, _ = _second_order_terms(matrix)
178
+ if second_order.shape[1] > 0:
179
+ cross = matrix.T @ second_order
180
+ max_me_vs_so = float(np.abs(cross).max())
181
+ else: # defensive: a zero-factor matrix has no second-order terms
182
+ max_me_vs_so = 0.0
183
+ main_effects_clear = max_me_vs_so <= tol
184
+
185
+ max_so_corr = _max_abs_correlation(second_order)
186
+
187
+ is_omars = bool(
188
+ is_three_level
189
+ and quadratics_estimable
190
+ and is_balanced
191
+ and main_effects_orthogonal
192
+ and main_effects_clear
193
+ )
194
+
195
+ return {
196
+ "n_runs": int(n_runs),
197
+ "n_factors": int(n_factors),
198
+ "levels": levels,
199
+ "is_three_level": is_three_level,
200
+ "quadratics_estimable": quadratics_estimable,
201
+ "is_balanced": is_balanced,
202
+ "main_effects_orthogonal": main_effects_orthogonal,
203
+ "main_effects_clear_of_second_order": main_effects_clear,
204
+ "max_main_effect_inner_product": max_me_inner,
205
+ "max_main_vs_second_order_inner_product": max_me_vs_so,
206
+ "max_second_order_correlation": max_so_corr,
207
+ "is_omars": is_omars,
208
+ }
209
+
210
+
211
+ def is_omars(matrix: np.ndarray, *, tol: float = _DEFAULT_TOL) -> bool:
212
+ """Return ``True`` iff *matrix* satisfies the OMARS-defining properties.
213
+
214
+ Convenience wrapper around :func:`omars_properties`.
215
+
216
+ Parameters
217
+ ----------
218
+ matrix : np.ndarray
219
+ Coded design matrix of shape ``(n_runs, n_factors)``.
220
+ tol : float
221
+ Absolute tolerance below which an inner product is treated as zero.
222
+
223
+ Returns
224
+ -------
225
+ bool
226
+ ``True`` if the design is three-level with orthogonal main effects
227
+ that are clear of all second-order terms.
228
+ """
229
+ return omars_properties(matrix, tol=tol)["is_omars"]
230
+
231
+
232
+ def dispatch_omars(factors: list[Factor], *, verify: bool = True) -> tuple[np.ndarray, dict]:
233
+ """Generate an OMARS design via the conference-matrix foldover construction.
234
+
235
+ The construction is shared with :func:`dispatch_dsd`: for ``k`` factors it
236
+ folds a conference matrix ``C`` of order ``m`` over its negative and adds a
237
+ center run, giving ``[C; -C; 0]``. ``m = k`` for even ``k`` (``2k + 1``
238
+ runs) and ``m = k + 1`` with the last column dropped for odd ``k``
239
+ (``2k + 3`` runs). This yields the minimal OMARS design for ``k`` factors;
240
+ a future enumerator will expand coverage to the larger, non-foldover
241
+ members of the OMARS family.
242
+
243
+ Parameters
244
+ ----------
245
+ factors : list[Factor]
246
+ Continuous factors (at least three).
247
+ verify : bool
248
+ When ``True`` (default) the generated matrix is checked with
249
+ :func:`is_omars` and the result is recorded in the metadata under
250
+ ``"omars_verified"``. The check is cheap and guards against the
251
+ degraded orthogonality of the cyclic conference-matrix fallback.
252
+
253
+ Returns
254
+ -------
255
+ tuple[np.ndarray, dict]
256
+ The coded design matrix and metadata. Metadata includes
257
+ ``"construction"`` (the conference-matrix construction used),
258
+ ``"family"`` and, when *verify* is ``True``, ``"omars_verified"``.
259
+
260
+ Raises
261
+ ------
262
+ ValueError
263
+ If fewer than three factors are supplied.
264
+ """
265
+ from process_improve.experiments.designs_response_surface import dispatch_dsd # noqa: PLC0415
266
+
267
+ if len(factors) < 3:
268
+ raise ValueError("OMARS designs require at least 3 factors.")
269
+
270
+ coded_matrix, dsd_meta = dispatch_dsd(factors)
271
+ meta = {**dsd_meta, "family": "conference_foldover"}
272
+ if verify:
273
+ meta["omars_verified"] = is_omars(coded_matrix)
274
+ return coded_matrix, meta
@@ -17,6 +17,7 @@ try:
17
17
  from pyDOE3 import bbdesign, ccdesign
18
18
  except ImportError: # pragma: no cover - exercised via env-without-pyDOE3
19
19
  from process_improve._extras import _MissingExtra
20
+
20
21
  bbdesign = _MissingExtra("pyDOE3", "expt") # type: ignore[assignment]
21
22
  ccdesign = _MissingExtra("pyDOE3", "expt") # type: ignore[assignment]
22
23
 
@@ -26,10 +27,13 @@ if TYPE_CHECKING:
26
27
  logger = logging.getLogger(__name__)
27
28
 
28
29
 
29
- def dispatch_ccd(
30
+ def dispatch_ccd( # noqa: PLR0913
30
31
  factors: list[Factor],
31
32
  center_points: int = 3,
32
33
  alpha: str | float | None = None,
34
+ cube: str = "full",
35
+ generators: list[str] | None = None,
36
+ resolution: int | None = None,
33
37
  ) -> tuple[np.ndarray, dict]:
34
38
  """Generate a Central Composite Design (CCD).
35
39
 
@@ -43,6 +47,18 @@ def dispatch_ccd(
43
47
  Axial distance. Accepted string values: ``"rotatable"``,
44
48
  ``"face_centered"``, ``"orthogonal"``. A numeric value sets
45
49
  alpha directly. Defaults to ``"orthogonal"``.
50
+ cube : str
51
+ How to build the cube (factorial) portion: ``"full"`` (default) uses
52
+ the complete 2^k factorial; ``"fractional"`` uses a resolution-V (or
53
+ higher) fractional factorial, keeping the run count practical for
54
+ k >= 5.
55
+ generators : list[str] or None
56
+ Explicit cube generators (e.g. ``["E=ABCD"]``), used only when
57
+ ``cube="fractional"``. When omitted, a minimum-aberration
58
+ half-fraction is chosen automatically.
59
+ resolution : int or None
60
+ Desired minimum cube resolution, used only when ``cube="fractional"``
61
+ and *generators* is not given.
46
62
 
47
63
  Returns
48
64
  -------
@@ -55,6 +71,11 @@ def dispatch_ccd(
55
71
  ``center`` parameter). The caller should set ``center_points=0`` in
56
72
  ``build_design_result`` to avoid adding duplicate center points.
57
73
  """
74
+ if cube == "fractional":
75
+ return _dispatch_ccd_fractional(factors, center_points, alpha, generators, resolution)
76
+ if cube != "full":
77
+ raise ValueError(f"cube must be 'full' or 'fractional', got {cube!r}.")
78
+
58
79
  k = len(factors)
59
80
 
60
81
  # Map alpha string to pyDOE3 face and alpha parameters.
@@ -97,6 +118,148 @@ def dispatch_ccd(
97
118
  return coded_matrix, {"alpha_value": alpha_value, "face": face}
98
119
 
99
120
 
121
+ def _resolve_fractional_axial_distance(
122
+ alpha: str | float | None,
123
+ n_cube_runs: int,
124
+ k: int,
125
+ center_points: int,
126
+ ) -> tuple[float, str]:
127
+ """Axial (star-point) distance for a fractional-cube CCD.
128
+
129
+ Mirrors pyDOE3's :func:`star` formulas, but uses the actual number of
130
+ fractional cube runs *n_cube_runs* in place of the full ``2**k``.
131
+
132
+ Parameters
133
+ ----------
134
+ alpha : str, float, or None
135
+ ``"face_centered"`` (alpha = 1), ``"rotatable"``
136
+ (alpha = n_cube_runs ** 0.25), ``"orthogonal"`` / None (the orthogonal
137
+ formula), or a numeric value used directly.
138
+ n_cube_runs : int
139
+ Number of runs in the (fractional) cube portion.
140
+ k : int
141
+ Number of factors.
142
+ center_points : int
143
+ Total number of center points; split between the cube and axial blocks
144
+ for the orthogonal-alpha formula.
145
+
146
+ Returns
147
+ -------
148
+ tuple[float, str]
149
+ The axial distance and a short label for the design metadata.
150
+ """
151
+ if isinstance(alpha, (int, float)) and not isinstance(alpha, bool):
152
+ return float(alpha), "user"
153
+
154
+ alpha_lower = alpha.lower() if isinstance(alpha, str) else None
155
+ if alpha_lower in ("face_centered", "face centered", "ccf", "faced"):
156
+ return 1.0, "faced"
157
+ if alpha_lower in ("rotatable", "r"):
158
+ return float(n_cube_runs**0.25), "rotatable"
159
+ if alpha_lower in ("inscribed", "cci"):
160
+ raise ValueError(
161
+ "alpha='inscribed' is not supported with cube='fractional'; "
162
+ "use 'face_centered', 'rotatable', 'orthogonal', or a numeric alpha."
163
+ )
164
+
165
+ # "orthogonal", None, or any other string: orthogonal axial distance.
166
+ n_center_cube = center_points // 2
167
+ n_center_axial = center_points - n_center_cube
168
+ n_axial = 2 * k
169
+ a = (k * (1 + n_center_axial / n_axial) / (1 + n_center_cube / n_cube_runs)) ** 0.5
170
+ return float(a), "orthogonal"
171
+
172
+
173
+ def _dispatch_ccd_fractional(
174
+ factors: list[Factor],
175
+ center_points: int,
176
+ alpha: str | float | None,
177
+ generators: list[str] | None,
178
+ resolution: int | None,
179
+ ) -> tuple[np.ndarray, dict]:
180
+ """Build a CCD whose cube portion is a resolution-V fractional factorial.
181
+
182
+ The cube is generated by reusing
183
+ :func:`process_improve.experiments.designs_screening.dispatch_fractional_factorial`,
184
+ then the axial (star) runs and the center runs are stacked on top.
185
+
186
+ Parameters
187
+ ----------
188
+ factors : list[Factor]
189
+ Continuous factors (at least 3).
190
+ center_points : int
191
+ Total number of center runs (added once, not split).
192
+ alpha : str, float, or None
193
+ Axial distance specification; see
194
+ :func:`_resolve_fractional_axial_distance`.
195
+ generators : list[str] or None
196
+ Explicit cube generators. When omitted, a minimum-aberration
197
+ half-fraction (last factor = product of all the others) is used.
198
+ resolution : int or None
199
+ Desired minimum cube resolution; used only when *generators* is None.
200
+
201
+ Returns
202
+ -------
203
+ tuple[np.ndarray, dict]
204
+ Coded design matrix and metadata, including ``alpha_value``,
205
+ ``generators_used``, ``defining_relation``, and ``resolution``.
206
+ """
207
+ from process_improve.experiments.designs_screening import dispatch_fractional_factorial # noqa: PLC0415
208
+ from process_improve.experiments.evaluate import ( # noqa: PLC0415
209
+ _defining_relation_from_generators,
210
+ _word_to_str,
211
+ )
212
+
213
+ k = len(factors)
214
+ if k < 3:
215
+ raise ValueError("A fractional-cube CCD requires at least 3 factors; use cube='full' for fewer.")
216
+
217
+ factor_names = [f.name for f in factors]
218
+
219
+ if generators is None and resolution is None:
220
+ # Minimum-aberration half-fraction: last factor = product of all the others.
221
+ generators = [f"{factor_names[-1]}={''.join(factor_names[:-1])}"]
222
+
223
+ cube, frac_meta = dispatch_fractional_factorial(factors, resolution=resolution, generators=generators)
224
+ n_cube_runs = cube.shape[0]
225
+
226
+ # Record the cube's generators, defining relation, and (true) resolution.
227
+ used_generators = frac_meta.get("generators_used") or generators
228
+ res = frac_meta.get("resolution")
229
+ defining_relation: list[str] | None = None
230
+ if used_generators:
231
+ words = _defining_relation_from_generators(used_generators, factor_names)
232
+ defining_relation = [f"I={_word_to_str(w, factor_names)}" for w in words]
233
+ if words:
234
+ res = min(len(w) for w in words)
235
+
236
+ if res is not None and res < 5:
237
+ raise ValueError(
238
+ f"The fractional cube has resolution {res}, but a CCD needs resolution V or higher so the "
239
+ "full quadratic model is estimable. Supply resolution-V generators or use cube='full'."
240
+ )
241
+
242
+ axial_distance, face = _resolve_fractional_axial_distance(alpha, n_cube_runs, k, center_points)
243
+
244
+ # Axial (star) runs: 2k rows at +/- axial_distance, zeros elsewhere.
245
+ star = np.zeros((2 * k, k))
246
+ for i in range(k):
247
+ star[2 * i : 2 * i + 2, i] = (-axial_distance, axial_distance)
248
+
249
+ center = np.zeros((max(0, center_points), k))
250
+
251
+ coded_matrix = np.vstack([cube, star, center])
252
+ meta = {
253
+ "alpha_value": axial_distance,
254
+ "face": face,
255
+ "cube": "fractional",
256
+ "generators_used": used_generators,
257
+ "defining_relation": defining_relation,
258
+ "resolution": res,
259
+ }
260
+ return coded_matrix, meta
261
+
262
+
100
263
  def dispatch_box_behnken(
101
264
  factors: list[Factor],
102
265
  center_points: int = 3,