simdec 1.5.0__tar.gz → 1.5.2__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.
@@ -35,3 +35,11 @@ sdist/*
35
35
  docs/html
36
36
  docs/jupyter_execute
37
37
  app.html
38
+
39
+ # Virtual environment
40
+ venv/
41
+ .venv/
42
+ .wasm-build-venv/
43
+
44
+ # Local Netlify folder
45
+ .netlify
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: simdec
3
- Version: 1.5.0
3
+ Version: 1.5.2
4
4
  Summary: Sensitivity analysis using simulation decomposition
5
5
  Project-URL: homepage, https://www.simdec.fi/
6
6
  Project-URL: documentation, https://simdec.readthedocs.io
@@ -30,7 +30,7 @@ Requires-Dist: panel>=1.4.5; extra == 'dashboard'
30
30
  Provides-Extra: dev
31
31
  Requires-Dist: accessible-pygments; extra == 'dev'
32
32
  Requires-Dist: cryptography; extra == 'dev'
33
- Requires-Dist: hatch; extra == 'dev'
33
+ Requires-Dist: ipython>=9.1; extra == 'dev'
34
34
  Requires-Dist: myst-nb; extra == 'dev'
35
35
  Requires-Dist: numpydoc; extra == 'dev'
36
36
  Requires-Dist: panel>=1.4.5; extra == 'dev'
@@ -40,6 +40,8 @@ Requires-Dist: pytest; extra == 'dev'
40
40
  Requires-Dist: pytest-cov; extra == 'dev'
41
41
  Requires-Dist: sphinx; extra == 'dev'
42
42
  Requires-Dist: watchfiles; extra == 'dev'
43
+ Provides-Extra: display
44
+ Requires-Dist: ipython>=9.1; extra == 'display'
43
45
  Provides-Extra: doc
44
46
  Requires-Dist: accessible-pygments; extra == 'doc'
45
47
  Requires-Dist: myst-nb; extra == 'doc'
@@ -95,5 +97,6 @@ Foundation, and Finnish Foundation for Economic Education.
95
97
  If you use SimDec in your research we would appreciate a citation to the
96
98
  following publications:
97
99
 
100
+ - Kozlova, M., Ahola, A., Roy, P., & Yeomans, J. S. (2025). Simple Binning Algorithm and SimDec Visualization for Comprehensive Sensitivity Analysis of Complex Computational Models. _Journal of Environmental Informatics Letters_, 13(1), 38-56. [https://arxiv.org/pdf/2310.13446](https://arxiv.org/pdf/2310.13446)
98
101
  - Kozlova, M., Moss, R. J., Yeomans, J. S., & Caers, J. (2024). Uncovering Heterogeneous Effects in Computational Models for Sustainable Decision-making. _Environmental Modelling & Software_, 171, 105898. [https://doi.org/10.1016/j.envsoft.2023.105898](https://doi.org/10.1016/j.envsoft.2023.105898)
99
102
  - Kozlova, M., Moss, R. J., Roy, P., Alam, A., & Yeomans, J. S. (2024). SimDec algorithm and guidelines for its usage and interpretation. In M. Kozlova & J. S. Yeomans (Eds.), _Sensitivity Analysis for Business, Technology, and Policymaking. Made Easy with Simulation Decomposition_. Routledge. [Available here](https://github.com/Simulation-Decomposition/SimDec-book/blob/74ce72c1d3dda650eba1c59e3b215a4bb35c6be0/chapters/02_SimDec_algorithm_and_instructions/Ch2.pdf).
@@ -42,5 +42,6 @@ Foundation, and Finnish Foundation for Economic Education.
42
42
  If you use SimDec in your research we would appreciate a citation to the
43
43
  following publications:
44
44
 
45
+ - Kozlova, M., Ahola, A., Roy, P., & Yeomans, J. S. (2025). Simple Binning Algorithm and SimDec Visualization for Comprehensive Sensitivity Analysis of Complex Computational Models. _Journal of Environmental Informatics Letters_, 13(1), 38-56. [https://arxiv.org/pdf/2310.13446](https://arxiv.org/pdf/2310.13446)
45
46
  - Kozlova, M., Moss, R. J., Yeomans, J. S., & Caers, J. (2024). Uncovering Heterogeneous Effects in Computational Models for Sustainable Decision-making. _Environmental Modelling & Software_, 171, 105898. [https://doi.org/10.1016/j.envsoft.2023.105898](https://doi.org/10.1016/j.envsoft.2023.105898)
46
47
  - Kozlova, M., Moss, R. J., Roy, P., Alam, A., & Yeomans, J. S. (2024). SimDec algorithm and guidelines for its usage and interpretation. In M. Kozlova & J. S. Yeomans (Eds.), _Sensitivity Analysis for Business, Technology, and Policymaking. Made Easy with Simulation Decomposition_. Routledge. [Available here](https://github.com/Simulation-Decomposition/SimDec-book/blob/74ce72c1d3dda650eba1c59e3b215a4bb35c6be0/chapters/02_SimDec_algorithm_and_instructions/Ch2.pdf).
@@ -0,0 +1,24 @@
1
+ [build]
2
+ command = "npm run build"
3
+ publish = "dist/pyodide"
4
+
5
+ [dev]
6
+ ignore = ["dist", ".wasm-build-venv", "panel/data"]
7
+ port = 8888
8
+
9
+ [build.environment]
10
+ PYTHON_VERSION = "3.11"
11
+ NODE_VERSION = "20"
12
+ CACHE_VERSION = "2"
13
+
14
+ [[headers]]
15
+ for = "/*"
16
+ [headers.values]
17
+ Cross-Origin-Embedder-Policy = "require-corp"
18
+ Cross-Origin-Opener-Policy = "same-origin"
19
+
20
+ [[headers]]
21
+ for = "/*.whl"
22
+ [headers.values]
23
+ Content-Type = "application/zip"
24
+ Access-Control-Allow-Origin = "*"
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "simdec-wasm-site",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "description": "Static Panel WASM build wrapper",
6
+ "scripts": {
7
+ "build": "bash ./scripts/build_wasm.sh",
8
+ "serve": "mkdir -p dist/pyodide && python3 -m http.server 8000 --directory dist/pyodide"
9
+ }
10
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simdec"
7
- version = "1.5.0"
7
+ version = "1.5.2"
8
8
  description = "Sensitivity analysis using simulation decomposition"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -41,6 +41,10 @@ dashboard = [
41
41
  "cryptography",
42
42
  ]
43
43
 
44
+ display = [
45
+ "ipython>=9.1"
46
+ ]
47
+
44
48
  test = [
45
49
  "pytest",
46
50
  "pytest-cov",
@@ -55,10 +59,9 @@ doc = [
55
59
  ]
56
60
 
57
61
  dev = [
58
- "simdec[doc,test,dashboard]",
62
+ "simdec[doc,test,dashboard, display]",
59
63
  "watchfiles",
60
64
  "pre-commit",
61
- "hatch",
62
65
  ]
63
66
 
64
67
  [project.urls]
@@ -0,0 +1,140 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # Logging
4
+ set -x
5
+
6
+ # Setup paths and environment
7
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
8
+ OUT_DIR="${ROOT_DIR}/dist/pyodide"
9
+
10
+ # Wipe old build before starting
11
+ if [ -d "${OUT_DIR}" ]; then
12
+ echo "Cleaning old build..."
13
+ rm -rf "${OUT_DIR}"
14
+ fi
15
+
16
+ cd "${ROOT_DIR}"
17
+
18
+ # Determine which Python interpreter to use
19
+ HOST_PYTHON="${PYTHON_BIN:-}"
20
+ if [[ -z "${HOST_PYTHON}" ]]; then
21
+ if command -v python3 >/dev/null 2>&1; then
22
+ HOST_PYTHON="python3"
23
+ elif command -v python >/dev/null 2>&1; then
24
+ HOST_PYTHON="python"
25
+ else
26
+ echo "Error: No python interpreter found. Install python3 and retry."
27
+ exit 1
28
+ fi
29
+ fi
30
+
31
+ # Create build virtual environment
32
+ VENV_DIR="${ROOT_DIR}/.wasm-build-venv"
33
+ if [[ ! -d "${VENV_DIR}" ]]; then
34
+ echo "Creating build virtual environment at ${VENV_DIR}..."
35
+ "${HOST_PYTHON}" -m venv "${VENV_DIR}"
36
+ fi
37
+
38
+ PYTHON_BIN="${VENV_DIR}/bin/python"
39
+
40
+ # Install build dependencies and build the wheel
41
+ echo "Installing build tools and generating local wheel..."
42
+ "${PYTHON_BIN}" -m pip install --upgrade pip build \
43
+ "panel==1.8.10" \
44
+ "bokeh==3.8.0" \
45
+ "matplotlib==3.10.8" \
46
+ "seaborn==0.13.2" \
47
+ "scipy==1.17.1" \
48
+ "SALib==1.5.2"
49
+ # Clean old builds to avoid picking up the wrong wheel
50
+ rm -rf dist/*.whl
51
+ "${PYTHON_BIN}" -m build --wheel .
52
+
53
+ # Identify the generated wheel file
54
+ SIMDEC_WHEEL_PATH=$(ls dist/*.whl | head -n 1 || echo "")
55
+
56
+ if [[ -z "${SIMDEC_WHEEL_PATH}" ]]; then
57
+ echo "Error: No wheel file found in dist/. Build failed."
58
+ exit 1
59
+ fi
60
+
61
+ WHEEL_FILENAME=$(basename "${SIMDEC_WHEEL_PATH}")
62
+
63
+ # Prepare output directory - USE ABSOLUTE PATH
64
+ OUT_DIR="${ROOT_DIR}/dist/pyodide"
65
+ mkdir -p "${OUT_DIR}/_static"
66
+ mkdir -p "${OUT_DIR}/data"
67
+
68
+ # Copy the wheel into the output directory so it's accessible via HTTP
69
+ cp "${SIMDEC_WHEEL_PATH}" "${OUT_DIR}/"
70
+
71
+ # Copy the assets to where the Python scripts expect them
72
+ mkdir -p panel/_static
73
+ if [ -d "docs/_static" ]; then
74
+ # Use || true to prevent failure if it's empty
75
+ cp -r docs/_static/* panel/_static/ || true
76
+ fi
77
+
78
+ echo "Converting Panel apps to Pyodide worker output..."
79
+ export PYTHONPATH="${ROOT_DIR}/src:${PYTHONPATH:-}"
80
+
81
+ # Change directory into 'panel' so relative paths resolve correctly
82
+ cd "${ROOT_DIR}/panel"
83
+
84
+ # Bring the wheel into the current folder so we can pass just the filename
85
+ cp "${ROOT_DIR}/${SIMDEC_WHEEL_PATH}" .
86
+
87
+ # Safe file checking that won't trigger set -e
88
+ echo "Checking necessary files locally before conversion..."
89
+ if [[ ! -f "simdec_app.py" ]]; then echo "Warning: simdec_app.py not found in panel directory!"; fi
90
+ if [[ ! -f "sampling.py" ]]; then echo "Warning: sampling.py not found in panel directory!"; fi
91
+
92
+ # Ensure data/stress.csv exists relative to the current 'panel' dir
93
+ if [[ ! -f "data/stress.csv" ]]; then
94
+ echo "Warning: panel/data/stress.csv missing."
95
+ if [[ -f "../data/stress.csv" ]]; then
96
+ echo "Found stress.csv in root. Copying to panel/data..."
97
+ mkdir -p data
98
+ cp "../data/stress.csv" data/
99
+ else
100
+ echo "Error: stress.csv not found anywhere! Conversion may fail."
101
+ fi
102
+ fi
103
+
104
+ # Copy stress.csv to output directory
105
+ cp "data/stress.csv" "${OUT_DIR}/data/"
106
+
107
+ # Run conversion
108
+ "${PYTHON_BIN}" -m panel convert \
109
+ simdec_app.py \
110
+ sampling.py \
111
+ --to pyodide-worker \
112
+ --out "${OUT_DIR}" \
113
+ --requirements "${WHEEL_FILENAME}" numpy pandas matplotlib seaborn scipy SALib \
114
+ --resources data/stress.csv
115
+
116
+ cp "./${WHEEL_FILENAME}" "${OUT_DIR}/"
117
+
118
+ # Clean up the copied wheel
119
+ rm "${WHEEL_FILENAME}"
120
+
121
+ # Step back out to the root directory for the rest of the script
122
+ cd "${ROOT_DIR}"
123
+
124
+ # Copy custom index page and static assets
125
+ echo "Copying custom index page and static assets..."
126
+
127
+ # Copy images/thumbnails from docs/_static if they exist
128
+ if [ -d "docs/_static" ]; then
129
+ cp -r docs/_static/* "${OUT_DIR}/_static/" || true
130
+ fi
131
+
132
+ # Overwrite default index.html with your custom homepage
133
+ if [ -f "panel/index.html" ]; then
134
+ cp panel/index.html "${OUT_DIR}/index.html"
135
+ else
136
+ echo "Warning: panel/index.html not found. Using default Panel index."
137
+ fi
138
+
139
+ echo "---"
140
+ echo "WASM site successfully generated at ${OUT_DIR}"
@@ -1,5 +1,6 @@
1
1
  """SimDec main namespace."""
2
2
  from simdec.decomposition import *
3
+ from simdec.heterogeneity_indices import *
3
4
  from simdec.sensitivity_indices import *
4
5
  from simdec.visualization import *
5
6
 
@@ -8,6 +9,8 @@ __all__ = [
8
9
  "states_expansion",
9
10
  "decomposition",
10
11
  "visualization",
12
+ "two_output_visualization",
11
13
  "tableau",
12
14
  "palette",
15
+ "heterogeneity_indices",
13
16
  ]
@@ -68,7 +68,7 @@ def decomposition(
68
68
  output: pd.DataFrame,
69
69
  *,
70
70
  sensitivity_indices: np.ndarray,
71
- dec_limit: float = 1,
71
+ dec_limit: float | None = None,
72
72
  auto_ordering: bool = True,
73
73
  states: list[int] | None = None,
74
74
  statistic: Literal["mean", "median"] | None = "mean",
@@ -79,7 +79,7 @@ def decomposition(
79
79
  ----------
80
80
  inputs : DataFrame of shape (n_runs, n_factors)
81
81
  Input variables.
82
- output : DataFrame of shape (n_runs, 1)
82
+ output : DataFrame of shape (n_runs, 1) or (n_runs,)
83
83
  Target variable.
84
84
  sensitivity_indices : ndarray of shape (n_factors, 1)
85
85
  Sensitivity indices, combined effect of each input.
@@ -116,7 +116,7 @@ def decomposition(
116
116
  inputs[cat_col] = codes
117
117
 
118
118
  inputs = inputs.to_numpy()
119
- output = output.to_numpy()
119
+ output = output.to_numpy().flatten()
120
120
 
121
121
  # 1. variables for decomposition
122
122
  var_order = np.argsort(sensitivity_indices)[::-1]
@@ -125,26 +125,41 @@ def decomposition(
125
125
  sensitivity_indices = sensitivity_indices[var_order]
126
126
 
127
127
  if auto_ordering:
128
- n_var_dec = np.where(np.cumsum(sensitivity_indices) < dec_limit)[0].size
128
+ # handle edge case where sensitivity indices don't sum exactly to 1.0
129
+ if dec_limit is None:
130
+ dec_limit = 0.8 * np.sum(sensitivity_indices)
131
+
132
+ cumulative_sum = np.cumsum(sensitivity_indices)
133
+ indices_over_limit = np.where(cumulative_sum >= dec_limit)[0]
134
+
135
+ if indices_over_limit.size > 0:
136
+ n_var_dec = indices_over_limit[0] + 1
137
+ else:
138
+ n_var_dec = sensitivity_indices.size
139
+
129
140
  n_var_dec = max(1, n_var_dec) # keep at least one variable
130
- n_var_dec = min(5, n_var_dec) # use at most 5 variables
141
+ n_var_dec = min(4, n_var_dec) # use at most 4 variables
131
142
  else:
132
143
  n_var_dec = inputs.shape[1]
133
144
 
134
- # 2. states formation
145
+ # 2. variable selection and reordering
146
+ if auto_ordering:
147
+ var_names = var_names[var_order[:n_var_dec]].tolist()
148
+ inputs = inputs[:, var_order[:n_var_dec]]
149
+ else:
150
+ var_names = var_names[:n_var_dec].tolist()
151
+ inputs = inputs[:, :n_var_dec]
152
+
153
+ # 3. states formation (after reordering/selection)
135
154
  if states is None:
136
- states = 3 if n_var_dec < 3 else 2
155
+ states = 3 if n_var_dec <= 2 else 2
137
156
  states = [states] * n_var_dec
138
157
 
139
158
  for i in range(n_var_dec):
140
159
  n_unique = np.unique(inputs[:, i]).size
141
160
  states[i] = n_unique if n_unique <= 5 else states[i]
142
161
 
143
- if auto_ordering:
144
- var_names = var_names[var_order[:n_var_dec]].tolist()
145
- inputs = inputs[:, var_order[:n_var_dec]]
146
-
147
- # 3. decomposition
162
+ # 4. decomposition
148
163
  bins = []
149
164
 
150
165
  statistic_methods = {
@@ -153,8 +168,8 @@ def decomposition(
153
168
  }
154
169
  try:
155
170
  statistic_method = statistic_methods[statistic]
156
- except IndexError:
157
- msg = f"'statistic' must be one of {statistic_methods.values()}"
171
+ except KeyError:
172
+ msg = f"'statistic' must be one of {statistic_methods.keys()}"
158
173
  raise ValueError(msg)
159
174
 
160
175
  def statistic_(inputs):