softDOE 0.1.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.
- softdoe-0.1.0/LICENSE +21 -0
- softdoe-0.1.0/PKG-INFO +145 -0
- softdoe-0.1.0/README.md +129 -0
- softdoe-0.1.0/pyproject.toml +30 -0
- softdoe-0.1.0/setup.cfg +4 -0
- softdoe-0.1.0/setup.py +38 -0
- softdoe-0.1.0/src/softDOE/__init__.py +6 -0
- softdoe-0.1.0/src/softDOE/_core.cpp +239 -0
- softdoe-0.1.0/src/softDOE/_measure.py +84 -0
- softdoe-0.1.0/src/softDOE/_soft.py +271 -0
- softdoe-0.1.0/src/softDOE.egg-info/PKG-INFO +145 -0
- softdoe-0.1.0/src/softDOE.egg-info/SOURCES.txt +21 -0
- softdoe-0.1.0/src/softDOE.egg-info/dependency_links.txt +1 -0
- softdoe-0.1.0/src/softDOE.egg-info/requires.txt +6 -0
- softdoe-0.1.0/src/softDOE.egg-info/top_level.txt +1 -0
- softdoe-0.1.0/tests/test_1_1.py +125 -0
- softdoe-0.1.0/tests/test_1_2.py +122 -0
- softdoe-0.1.0/tests/test_1_3.py +191 -0
- softdoe-0.1.0/tests/test_2_1.py +120 -0
- softdoe-0.1.0/tests/test_2_2.py +326 -0
- softdoe-0.1.0/tests/test_3_measure.py +90 -0
- softdoe-0.1.0/tests/test_measure.py +34 -0
- softdoe-0.1.0/tests/test_soft.py +51 -0
softdoe-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wei-Yang Yu and V. Roshan Joseph
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
softdoe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: softDOE
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Space-filling one-factor-at-a-time designs.
|
|
5
|
+
Author: Wei-Yang Yu and V. Roshan Joseph
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: scipy
|
|
11
|
+
Requires-Dist: pyDOE
|
|
12
|
+
Provides-Extra: test
|
|
13
|
+
Requires-Dist: pytest; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
Dynamic: requires-python
|
|
16
|
+
|
|
17
|
+
# softDOE
|
|
18
|
+
|
|
19
|
+
`softDOE` is a Python package for constructing space-filling
|
|
20
|
+
one-factor-at-a-time (SOFT) designs.
|
|
21
|
+
|
|
22
|
+
## What is included
|
|
23
|
+
|
|
24
|
+
`softDOE` currently includes:
|
|
25
|
+
|
|
26
|
+
* `soft()`: generate SOFT designs.
|
|
27
|
+
* `measure()`: compute screening measures.
|
|
28
|
+
|
|
29
|
+
Generated designs are returned as NumPy arrays with entries in `(0, 1)`.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install `softDOE` from PyPI:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install softDOE
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
`softDOE` requires Python 3.9 or later. The package depends on NumPy, SciPy,
|
|
41
|
+
and pyDOE, and uses a compiled C++ extension built with pybind11.
|
|
42
|
+
|
|
43
|
+
## Quick example
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import numpy as np
|
|
47
|
+
import softDOE
|
|
48
|
+
|
|
49
|
+
# SOFT design
|
|
50
|
+
design = softDOE.soft(p=10, l=4)
|
|
51
|
+
|
|
52
|
+
# Friedman function
|
|
53
|
+
def friedman(x):
|
|
54
|
+
x = np.asarray(x)
|
|
55
|
+
return (
|
|
56
|
+
10 * np.sin(np.pi * x[0] * x[1])
|
|
57
|
+
+ 20 * (x[2] - 0.5) ** 2
|
|
58
|
+
+ 10 * x[3]
|
|
59
|
+
+ 5 * x[4]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Evaluate Friedman function at each design point.
|
|
63
|
+
y = np.array([friedman(row) for row in design])
|
|
64
|
+
|
|
65
|
+
# Sensitivity measures
|
|
66
|
+
result = softDOE.measure(design, y)
|
|
67
|
+
|
|
68
|
+
print(result["mustar"])
|
|
69
|
+
print(result["t"])
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Generating SOFT designs
|
|
73
|
+
|
|
74
|
+
Use `softDOE.soft()` to generate a SOFT design:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
design = softDOE.soft(p=5, l=4, structure="standard", m=500, seed=123)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Arguments:
|
|
81
|
+
|
|
82
|
+
* `p`: number of factors.
|
|
83
|
+
* `l`: number of base runs. This value must be even.
|
|
84
|
+
* `structure`: either `"standard"` or `"strict"`. The default is
|
|
85
|
+
`"standard"`.
|
|
86
|
+
* `m`: number of Monte Carlo samples used when evaluating the design
|
|
87
|
+
objective.
|
|
88
|
+
* `seed`: optional seed for reproducible design generation.
|
|
89
|
+
|
|
90
|
+
The returned design has shape:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
(l * (p + 1), p)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Computing screening measures
|
|
97
|
+
|
|
98
|
+
Use `softDOE.measure()` with a design matrix and response vector:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
result = softDOE.measure(design, y)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The response vector `y` must have one value for each row of the design. The
|
|
105
|
+
function returns a dictionary with two entries:
|
|
106
|
+
|
|
107
|
+
* `mustar`: $\mu^*$ measure of Campolongo et al. (2007).
|
|
108
|
+
* `t`: total Sobol' index of Sobol' (1993).
|
|
109
|
+
|
|
110
|
+
## Testing
|
|
111
|
+
|
|
112
|
+
After installing the test dependencies, run the test suite with:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
python -m pytest
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Citation
|
|
119
|
+
|
|
120
|
+
If you use `softDOE`, please cite:
|
|
121
|
+
|
|
122
|
+
```bibtex
|
|
123
|
+
@article{yu2026space,
|
|
124
|
+
title={Space-Filling One-Factor-At-A-Time Designs},
|
|
125
|
+
author={Yu, Wei-Yang and Joseph, V Roshan},
|
|
126
|
+
journal={arXiv preprint arXiv:2606.02533},
|
|
127
|
+
year={2026}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## References
|
|
132
|
+
|
|
133
|
+
* Sobol', I. M. (1993), "On sensitivity estimation for nonlinear mathematical
|
|
134
|
+
models," Mathematical Modeling and Computational Experiments, 1, 407-414.
|
|
135
|
+
* Campolongo, F., Cariboni, J., and Saltelli, A. (2007), "An effective
|
|
136
|
+
screening design for sensitivity analysis of large models," Environmental
|
|
137
|
+
Modelling and Software, 22, 1509-1518.
|
|
138
|
+
|
|
139
|
+
## Authors
|
|
140
|
+
|
|
141
|
+
Wei-Yang Yu and V. Roshan Joseph.
|
|
142
|
+
|
|
143
|
+
## License
|
|
144
|
+
|
|
145
|
+
`softDOE` is licensed under the MIT License.
|
softdoe-0.1.0/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# softDOE
|
|
2
|
+
|
|
3
|
+
`softDOE` is a Python package for constructing space-filling
|
|
4
|
+
one-factor-at-a-time (SOFT) designs.
|
|
5
|
+
|
|
6
|
+
## What is included
|
|
7
|
+
|
|
8
|
+
`softDOE` currently includes:
|
|
9
|
+
|
|
10
|
+
* `soft()`: generate SOFT designs.
|
|
11
|
+
* `measure()`: compute screening measures.
|
|
12
|
+
|
|
13
|
+
Generated designs are returned as NumPy arrays with entries in `(0, 1)`.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Install `softDOE` from PyPI:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install softDOE
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
`softDOE` requires Python 3.9 or later. The package depends on NumPy, SciPy,
|
|
25
|
+
and pyDOE, and uses a compiled C++ extension built with pybind11.
|
|
26
|
+
|
|
27
|
+
## Quick example
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import numpy as np
|
|
31
|
+
import softDOE
|
|
32
|
+
|
|
33
|
+
# SOFT design
|
|
34
|
+
design = softDOE.soft(p=10, l=4)
|
|
35
|
+
|
|
36
|
+
# Friedman function
|
|
37
|
+
def friedman(x):
|
|
38
|
+
x = np.asarray(x)
|
|
39
|
+
return (
|
|
40
|
+
10 * np.sin(np.pi * x[0] * x[1])
|
|
41
|
+
+ 20 * (x[2] - 0.5) ** 2
|
|
42
|
+
+ 10 * x[3]
|
|
43
|
+
+ 5 * x[4]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Evaluate Friedman function at each design point.
|
|
47
|
+
y = np.array([friedman(row) for row in design])
|
|
48
|
+
|
|
49
|
+
# Sensitivity measures
|
|
50
|
+
result = softDOE.measure(design, y)
|
|
51
|
+
|
|
52
|
+
print(result["mustar"])
|
|
53
|
+
print(result["t"])
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Generating SOFT designs
|
|
57
|
+
|
|
58
|
+
Use `softDOE.soft()` to generate a SOFT design:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
design = softDOE.soft(p=5, l=4, structure="standard", m=500, seed=123)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Arguments:
|
|
65
|
+
|
|
66
|
+
* `p`: number of factors.
|
|
67
|
+
* `l`: number of base runs. This value must be even.
|
|
68
|
+
* `structure`: either `"standard"` or `"strict"`. The default is
|
|
69
|
+
`"standard"`.
|
|
70
|
+
* `m`: number of Monte Carlo samples used when evaluating the design
|
|
71
|
+
objective.
|
|
72
|
+
* `seed`: optional seed for reproducible design generation.
|
|
73
|
+
|
|
74
|
+
The returned design has shape:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
(l * (p + 1), p)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Computing screening measures
|
|
81
|
+
|
|
82
|
+
Use `softDOE.measure()` with a design matrix and response vector:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
result = softDOE.measure(design, y)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The response vector `y` must have one value for each row of the design. The
|
|
89
|
+
function returns a dictionary with two entries:
|
|
90
|
+
|
|
91
|
+
* `mustar`: $\mu^*$ measure of Campolongo et al. (2007).
|
|
92
|
+
* `t`: total Sobol' index of Sobol' (1993).
|
|
93
|
+
|
|
94
|
+
## Testing
|
|
95
|
+
|
|
96
|
+
After installing the test dependencies, run the test suite with:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
python -m pytest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Citation
|
|
103
|
+
|
|
104
|
+
If you use `softDOE`, please cite:
|
|
105
|
+
|
|
106
|
+
```bibtex
|
|
107
|
+
@article{yu2026space,
|
|
108
|
+
title={Space-Filling One-Factor-At-A-Time Designs},
|
|
109
|
+
author={Yu, Wei-Yang and Joseph, V Roshan},
|
|
110
|
+
journal={arXiv preprint arXiv:2606.02533},
|
|
111
|
+
year={2026}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## References
|
|
116
|
+
|
|
117
|
+
* Sobol', I. M. (1993), "On sensitivity estimation for nonlinear mathematical
|
|
118
|
+
models," Mathematical Modeling and Computational Experiments, 1, 407-414.
|
|
119
|
+
* Campolongo, F., Cariboni, J., and Saltelli, A. (2007), "An effective
|
|
120
|
+
screening design for sensitivity analysis of large models," Environmental
|
|
121
|
+
Modelling and Software, 22, 1509-1518.
|
|
122
|
+
|
|
123
|
+
## Authors
|
|
124
|
+
|
|
125
|
+
Wei-Yang Yu and V. Roshan Joseph.
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
`softDOE` is licensed under the MIT License.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel", "pybind11>=2.10", "numpy"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "softDOE"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Space-filling one-factor-at-a-time designs."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Wei-Yang Yu and V. Roshan Joseph"}
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"numpy",
|
|
16
|
+
"scipy",
|
|
17
|
+
"pyDOE"
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
test = ["pytest"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools]
|
|
24
|
+
package-dir = {"" = "src"}
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.pytest.ini_options]
|
|
30
|
+
testpaths = ["tests"]
|
softdoe-0.1.0/setup.cfg
ADDED
softdoe-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from setuptools import Extension, setup
|
|
4
|
+
from setuptools.command.build_ext import build_ext
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BuildExt(build_ext):
|
|
8
|
+
def build_extensions(self) -> None:
|
|
9
|
+
import pybind11
|
|
10
|
+
|
|
11
|
+
for ext in self.extensions:
|
|
12
|
+
ext.include_dirs.append(pybind11.get_include())
|
|
13
|
+
ext.extra_compile_args = ["-std=c++17"]
|
|
14
|
+
super().build_extensions()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
ext_modules = [
|
|
18
|
+
Extension(
|
|
19
|
+
"softDOE._core",
|
|
20
|
+
["src/softDOE/_core.cpp"],
|
|
21
|
+
language="c++",
|
|
22
|
+
)
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
setup(
|
|
27
|
+
name="softDOE",
|
|
28
|
+
version="0.1.0",
|
|
29
|
+
description="Space-filling one-factor-at-a-time designs.",
|
|
30
|
+
author="Wei-Yang Yu and V. Roshan Joseph",
|
|
31
|
+
package_dir={"": "src"},
|
|
32
|
+
packages=["softDOE"],
|
|
33
|
+
python_requires=">=3.9",
|
|
34
|
+
install_requires=["numpy", "scipy", "pyDOE"],
|
|
35
|
+
extras_require={"test": ["pytest"]},
|
|
36
|
+
ext_modules=ext_modules,
|
|
37
|
+
cmdclass={"build_ext": BuildExt},
|
|
38
|
+
)
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
#include <cmath>
|
|
2
|
+
#include <stdexcept>
|
|
3
|
+
|
|
4
|
+
#include <pybind11/numpy.h>
|
|
5
|
+
#include <pybind11/pybind11.h>
|
|
6
|
+
|
|
7
|
+
namespace py = pybind11;
|
|
8
|
+
|
|
9
|
+
namespace {
|
|
10
|
+
|
|
11
|
+
using Array = py::array_t<double, py::array::c_style | py::array::forcecast>;
|
|
12
|
+
|
|
13
|
+
// Evaluate the MIM correlation kernel between row ia of A and row ib of B.
|
|
14
|
+
double corr_idx(const double* A, py::ssize_t ia, const double* B, py::ssize_t ib,
|
|
15
|
+
py::ssize_t p, double alpha, double beta) {
|
|
16
|
+
const double invbeta = 1.0 / beta;
|
|
17
|
+
|
|
18
|
+
if (alpha == 1.0) {
|
|
19
|
+
double c = 1.0;
|
|
20
|
+
for (py::ssize_t k = 0; k < p; ++k) {
|
|
21
|
+
const double d = A[ia * p + k] - B[ib * p + k];
|
|
22
|
+
c *= 1.0 / (1.0 + (d * d) * invbeta);
|
|
23
|
+
}
|
|
24
|
+
return c;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const double negalpha = -alpha;
|
|
28
|
+
double logc = 0.0;
|
|
29
|
+
for (py::ssize_t k = 0; k < p; ++k) {
|
|
30
|
+
const double d = A[ia * p + k] - B[ib * p + k];
|
|
31
|
+
logc += negalpha * std::log1p((d * d) * invbeta);
|
|
32
|
+
}
|
|
33
|
+
return std::exp(logc);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate that a Python/NumPy input is a two-dimensional matrix.
|
|
37
|
+
void require_matrix(const py::buffer_info& info, const char* name) {
|
|
38
|
+
if (info.ndim != 2) {
|
|
39
|
+
throw std::invalid_argument(std::string(name) + " must be two-dimensional.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
} // namespace
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Testing helpers
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
// Expose corr_idx to Python for parity tests against R's internal corr_idx_fast.
|
|
50
|
+
// This is intentionally private (_corr_idx) and is not part of the public API.
|
|
51
|
+
double corr_idx_binding(Array A, py::ssize_t ia, Array B, py::ssize_t ib,
|
|
52
|
+
double alpha, double beta) {
|
|
53
|
+
const py::buffer_info a_info = A.request();
|
|
54
|
+
const py::buffer_info b_info = B.request();
|
|
55
|
+
require_matrix(a_info, "A");
|
|
56
|
+
require_matrix(b_info, "B");
|
|
57
|
+
|
|
58
|
+
if (a_info.shape[1] != b_info.shape[1]) {
|
|
59
|
+
throw std::invalid_argument("A and B must have the same number of columns.");
|
|
60
|
+
}
|
|
61
|
+
if (ia < 0 || ia >= a_info.shape[0]) {
|
|
62
|
+
throw std::out_of_range("ia is out of range for A.");
|
|
63
|
+
}
|
|
64
|
+
if (ib < 0 || ib >= b_info.shape[0]) {
|
|
65
|
+
throw std::out_of_range("ib is out of range for B.");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const auto* a = static_cast<const double*>(a_info.ptr);
|
|
69
|
+
const auto* b = static_cast<const double*>(b_info.ptr);
|
|
70
|
+
return corr_idx(a, ia, b, ib, a_info.shape[1], alpha, beta);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build the MIM correlation matrix for X, or append new_X to a precomputed
|
|
74
|
+
// old_R block when incrementally growing a design.
|
|
75
|
+
Array corr_matrix(Array X, py::object new_X_obj, py::object old_R_obj,
|
|
76
|
+
double alpha, double beta) {
|
|
77
|
+
const py::buffer_info x_info = X.request();
|
|
78
|
+
require_matrix(x_info, "X");
|
|
79
|
+
const auto n = x_info.shape[0];
|
|
80
|
+
const auto p = x_info.shape[1];
|
|
81
|
+
const auto* x = static_cast<const double*>(x_info.ptr);
|
|
82
|
+
|
|
83
|
+
if (new_X_obj.is_none()) {
|
|
84
|
+
Array out({n, n});
|
|
85
|
+
auto out_info = out.mutable_unchecked<2>();
|
|
86
|
+
for (py::ssize_t i = 0; i < n; ++i) {
|
|
87
|
+
out_info(i, i) = 1.0;
|
|
88
|
+
for (py::ssize_t j = i + 1; j < n; ++j) {
|
|
89
|
+
const double c = corr_idx(x, i, x, j, p, alpha, beta);
|
|
90
|
+
out_info(i, j) = c;
|
|
91
|
+
out_info(j, i) = c;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (old_R_obj.is_none()) {
|
|
98
|
+
throw std::invalid_argument("old_R must be provided when new_X is not None.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
Array new_X = py::cast<Array>(new_X_obj);
|
|
102
|
+
Array old_R = py::cast<Array>(old_R_obj);
|
|
103
|
+
const py::buffer_info x2_info = new_X.request();
|
|
104
|
+
const py::buffer_info r0_info = old_R.request();
|
|
105
|
+
require_matrix(x2_info, "new_X");
|
|
106
|
+
require_matrix(r0_info, "old_R");
|
|
107
|
+
|
|
108
|
+
const auto k = x2_info.shape[0];
|
|
109
|
+
if (x2_info.shape[1] != p) {
|
|
110
|
+
throw std::invalid_argument("new_X must have the same number of columns as X.");
|
|
111
|
+
}
|
|
112
|
+
if (r0_info.shape[0] != n || r0_info.shape[1] != n) {
|
|
113
|
+
throw std::invalid_argument("old_R must have shape (X rows, X rows).");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const auto* x2 = static_cast<const double*>(x2_info.ptr);
|
|
117
|
+
const auto* r0 = static_cast<const double*>(r0_info.ptr);
|
|
118
|
+
const auto new_n = n + k;
|
|
119
|
+
Array out({new_n, new_n});
|
|
120
|
+
auto out_info = out.mutable_unchecked<2>();
|
|
121
|
+
|
|
122
|
+
for (py::ssize_t i = 0; i < new_n; ++i) {
|
|
123
|
+
for (py::ssize_t j = 0; j < new_n; ++j) {
|
|
124
|
+
out_info(i, j) = 0.0;
|
|
125
|
+
}
|
|
126
|
+
out_info(i, i) = 1.0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (py::ssize_t i = 0; i < n; ++i) {
|
|
130
|
+
for (py::ssize_t j = 0; j < n; ++j) {
|
|
131
|
+
out_info(i, j) = r0[i * n + j];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (py::ssize_t i = 0; i < k; ++i) {
|
|
136
|
+
const py::ssize_t ii = n + i;
|
|
137
|
+
for (py::ssize_t j = 0; j < n; ++j) {
|
|
138
|
+
const double c = corr_idx(x2, i, x, j, p, alpha, beta);
|
|
139
|
+
out_info(ii, j) = c;
|
|
140
|
+
out_info(j, ii) = c;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (py::ssize_t i = 0; i < k; ++i) {
|
|
145
|
+
const py::ssize_t ii = n + i;
|
|
146
|
+
for (py::ssize_t j = i + 1; j < k; ++j) {
|
|
147
|
+
const py::ssize_t jj = n + j;
|
|
148
|
+
const double c = corr_idx(x2, i, x2, j, p, alpha, beta);
|
|
149
|
+
out_info(ii, jj) = c;
|
|
150
|
+
out_info(jj, ii) = c;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Build correlations between QMC/evaluation points and design points X, or
|
|
158
|
+
// append rows for new_X to a precomputed design-vs-QMC correlation block.
|
|
159
|
+
Array q_matrix(Array qmc, Array X, py::object old_R_obj, py::object new_X_obj,
|
|
160
|
+
double alpha, double beta) {
|
|
161
|
+
const py::buffer_info q_info = qmc.request();
|
|
162
|
+
const py::buffer_info x_info = X.request();
|
|
163
|
+
require_matrix(q_info, "qmc");
|
|
164
|
+
require_matrix(x_info, "X");
|
|
165
|
+
|
|
166
|
+
const auto m = q_info.shape[0];
|
|
167
|
+
const auto p = q_info.shape[1];
|
|
168
|
+
const auto n = x_info.shape[0];
|
|
169
|
+
if (x_info.shape[1] != p) {
|
|
170
|
+
throw std::invalid_argument("qmc and X must have the same number of columns.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const auto* q = static_cast<const double*>(q_info.ptr);
|
|
174
|
+
const auto* x = static_cast<const double*>(x_info.ptr);
|
|
175
|
+
|
|
176
|
+
if (new_X_obj.is_none()) {
|
|
177
|
+
Array out({n, m});
|
|
178
|
+
auto out_info = out.mutable_unchecked<2>();
|
|
179
|
+
for (py::ssize_t j = 0; j < m; ++j) {
|
|
180
|
+
for (py::ssize_t i = 0; i < n; ++i) {
|
|
181
|
+
out_info(i, j) = corr_idx(q, j, x, i, p, alpha, beta);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (old_R_obj.is_none()) {
|
|
188
|
+
throw std::invalid_argument("old_R must be provided when new_X is not None.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Array old_R = py::cast<Array>(old_R_obj);
|
|
192
|
+
Array new_X = py::cast<Array>(new_X_obj);
|
|
193
|
+
const py::buffer_info r0_info = old_R.request();
|
|
194
|
+
const py::buffer_info x2_info = new_X.request();
|
|
195
|
+
require_matrix(r0_info, "old_R");
|
|
196
|
+
require_matrix(x2_info, "new_X");
|
|
197
|
+
|
|
198
|
+
const auto k = x2_info.shape[0];
|
|
199
|
+
if (x2_info.shape[1] != p) {
|
|
200
|
+
throw std::invalid_argument("new_X must have the same number of columns as X.");
|
|
201
|
+
}
|
|
202
|
+
if (r0_info.shape[0] != n || r0_info.shape[1] != m) {
|
|
203
|
+
throw std::invalid_argument("old_R must have shape (X rows, qmc rows).");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const auto* r0 = static_cast<const double*>(r0_info.ptr);
|
|
207
|
+
const auto* x2 = static_cast<const double*>(x2_info.ptr);
|
|
208
|
+
Array out({n + k, m});
|
|
209
|
+
auto out_info = out.mutable_unchecked<2>();
|
|
210
|
+
|
|
211
|
+
for (py::ssize_t i = 0; i < n; ++i) {
|
|
212
|
+
for (py::ssize_t j = 0; j < m; ++j) {
|
|
213
|
+
out_info(i, j) = r0[i * m + j];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (py::ssize_t j = 0; j < m; ++j) {
|
|
218
|
+
for (py::ssize_t i = 0; i < k; ++i) {
|
|
219
|
+
out_info(n + i, j) = corr_idx(q, j, x2, i, p, alpha, beta);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Register the compiled backend functions exposed to the Python module.
|
|
227
|
+
PYBIND11_MODULE(_core, m) {
|
|
228
|
+
m.doc() = "Compiled correlation kernels for softDOE.";
|
|
229
|
+
m.def("_corr_idx", &corr_idx_binding, py::arg("A"), py::arg("ia"),
|
|
230
|
+
py::arg("B"), py::arg("ib"), py::arg("alpha") = 1.0,
|
|
231
|
+
py::arg("beta") = 1.0,
|
|
232
|
+
"Evaluate the MIM kernel between row ia of matrix A and row ib of matrix B.");
|
|
233
|
+
m.def("corr_matrix", &corr_matrix, py::arg("X"), py::arg("new_X") = py::none(),
|
|
234
|
+
py::arg("old_R") = py::none(), py::arg("alpha") = 1.0,
|
|
235
|
+
py::arg("beta") = 1.0);
|
|
236
|
+
m.def("q_matrix", &q_matrix, py::arg("qmc"), py::arg("X"),
|
|
237
|
+
py::arg("old_R") = py::none(), py::arg("new_X") = py::none(),
|
|
238
|
+
py::arg("alpha") = 1.0, py::arg("beta") = 1.0);
|
|
239
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def measure(design, y):
|
|
7
|
+
"""Compute screening measures for a SOFT/Sobol'-structured design.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
design : array_like, shape (l * (p + 1), p)
|
|
12
|
+
Design matrix with one base block followed by one-factor-at-a-time
|
|
13
|
+
blocks.
|
|
14
|
+
y : array_like, shape (l * (p + 1),)
|
|
15
|
+
Response vector.
|
|
16
|
+
|
|
17
|
+
Returns
|
|
18
|
+
-------
|
|
19
|
+
dict
|
|
20
|
+
``{"mustar": array, "t": array}``, matching the R function output.
|
|
21
|
+
"""
|
|
22
|
+
design = np.asarray(design, dtype=float)
|
|
23
|
+
y = np.asarray(y, dtype=float).reshape(-1)
|
|
24
|
+
|
|
25
|
+
if design.ndim != 2:
|
|
26
|
+
raise ValueError("design must be a two-dimensional array.")
|
|
27
|
+
|
|
28
|
+
n, p = design.shape
|
|
29
|
+
if p < 1:
|
|
30
|
+
raise ValueError("design must contain at least one factor.")
|
|
31
|
+
if n % (p + 1) != 0:
|
|
32
|
+
raise ValueError("design must have l * (p + 1) rows.")
|
|
33
|
+
if y.shape[0] != n:
|
|
34
|
+
raise ValueError("y must have the same length as design rows.")
|
|
35
|
+
|
|
36
|
+
l = n // (p + 1)
|
|
37
|
+
design_blocks = [design[i * l : (i + 1) * l, :] for i in range(p + 1)]
|
|
38
|
+
y_blocks = [y[i * l : (i + 1) * l] for i in range(p + 1)]
|
|
39
|
+
|
|
40
|
+
y_diff = [None] * p
|
|
41
|
+
delta = [None] * p
|
|
42
|
+
|
|
43
|
+
for i in range(p):
|
|
44
|
+
matrix_idx, factor_idx = _find_a(design_blocks, i + 1)
|
|
45
|
+
y_diff[factor_idx] = np.abs(y_blocks[i + 1] - y_blocks[matrix_idx])
|
|
46
|
+
delta[factor_idx] = np.abs(
|
|
47
|
+
design_blocks[i + 1][:, factor_idx]
|
|
48
|
+
- design_blocks[matrix_idx][:, factor_idx]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if any(v is None for v in y_diff) or any(v is None for v in delta):
|
|
52
|
+
raise ValueError("design does not have a valid one-factor-at-a-time block structure.")
|
|
53
|
+
|
|
54
|
+
with np.errstate(divide="raise", invalid="raise"):
|
|
55
|
+
try:
|
|
56
|
+
mustar = np.array(
|
|
57
|
+
[np.sum(diff / step) / l for diff, step in zip(y_diff, delta)],
|
|
58
|
+
dtype=float,
|
|
59
|
+
)
|
|
60
|
+
except FloatingPointError as exc:
|
|
61
|
+
raise ValueError("design contains zero OFAT step sizes.") from exc
|
|
62
|
+
|
|
63
|
+
var_y = np.var(y, ddof=1)
|
|
64
|
+
if var_y == 0:
|
|
65
|
+
t = np.full(p, np.nan)
|
|
66
|
+
else:
|
|
67
|
+
vtot = np.array([np.sum(diff**2) for diff in y_diff], dtype=float) / (2 * l)
|
|
68
|
+
t = vtot / var_y
|
|
69
|
+
|
|
70
|
+
return {"mustar": mustar, "t": t}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _find_a(blocks, idx):
|
|
74
|
+
b = blocks[idx]
|
|
75
|
+
p = b.shape[1]
|
|
76
|
+
for j in range(idx):
|
|
77
|
+
a = blocks[j]
|
|
78
|
+
col_same = np.array(
|
|
79
|
+
[np.allclose(a[:, k], b[:, k], rtol=0.0, atol=1e-12) for k in range(p)]
|
|
80
|
+
)
|
|
81
|
+
changed = np.flatnonzero(~col_same)
|
|
82
|
+
if changed.size == 1:
|
|
83
|
+
return j, int(changed[0])
|
|
84
|
+
raise ValueError("could not find the matching prior OFAT block.")
|