mumo-engine 1.0.0__py3-none-any.whl
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.
- mumo_engine-1.0.0.dist-info/METADATA +248 -0
- mumo_engine-1.0.0.dist-info/RECORD +8 -0
- mumo_engine-1.0.0.dist-info/WHEEL +5 -0
- mumo_engine-1.0.0.dist-info/entry_points.txt +3 -0
- mumo_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
- mumo_engine-1.0.0.dist-info/top_level.txt +2 -0
- mumo_framework.py +545 -0
- mumo_gui.py +365 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mumo-engine
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Boundary-based statistics and risk-corridor calculations
|
|
5
|
+
Author: Mumo Framework Authors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: statistics,risk,boundaries,mumo
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
================================================================================
|
|
20
|
+
MUMO MATHEMATICAL FRAMEWORK ENGINE (mumo-engine)
|
|
21
|
+
================================================================================
|
|
22
|
+
Version: 1.0.0
|
|
23
|
+
Author: Mumo Framework Authors
|
|
24
|
+
License: MIT (LICENSE)
|
|
25
|
+
|
|
26
|
+
--------------------------------------------------------------------------------
|
|
27
|
+
1. EXECUTIVE SUMMARY & LIBRARY PURPOSE
|
|
28
|
+
--------------------------------------------------------------------------------
|
|
29
|
+
The Mumo Framework (`mumo-engine`) is an advanced mathematical computing
|
|
30
|
+
library designed to replace legacy statistical metrics (such as standard
|
|
31
|
+
deviation, raw arithmetic means, and isolated medians) with dynamic risk-corridor
|
|
32
|
+
tracking, extreme boundary isolation, consensus core squeezing, autotuned parameter
|
|
33
|
+
self-calibration, and multi-dimensional tensor risk modeling.
|
|
34
|
+
|
|
35
|
+
Traditional statistical tools suffer from critical real-world limitations:
|
|
36
|
+
- Standard deviation produces abstract squared variance units rather than
|
|
37
|
+
unit-accurate risk limits.
|
|
38
|
+
- Single outliers or sensor typos skew basic mean calculations.
|
|
39
|
+
- Single-point medians flip wildly in large streaming data.
|
|
40
|
+
- Manual tuning parameters introduce human operator bias.
|
|
41
|
+
|
|
42
|
+
The Mumo Engine solves these problems by establishing boundary envelopes, dynamic
|
|
43
|
+
consensus squeezing, calculus-based derivative velocity tracking, and automated
|
|
44
|
+
parameter self-calibration across massive datasets and GPU-accelerated matrices.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
--------------------------------------------------------------------------------
|
|
48
|
+
2. PIPELINE ARCHITECTURE DIAGRAM
|
|
49
|
+
--------------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
[ THE TOTAL SC-PGMF PIPELINE ]
|
|
52
|
+
|
|
|
53
|
+
+----------------------------+----------------------------+
|
|
54
|
+
| | |
|
|
55
|
+
v v v
|
|
56
|
+
[ Boundary Modules ] [ Core Stabilisers ] [ Calculus Tensors ]
|
|
57
|
+
* P-MPU (Ceiling Limit) * P-GMCSI (True Baseline) * Derivatives (Velocity)
|
|
58
|
+
* P-MPD (Safety Floor) * P-MBTS (Tension Weight) * Integrals (Total Strain)
|
|
59
|
+
* MBV (Risk Corridor) * MAEI (Master Engines) * Matrices (Multi-Scale)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
--------------------------------------------------------------------------------
|
|
63
|
+
3. COMPLETE FORMULA BREAKDOWN: FUNCTIONS, SYMBOLS & GAPS FILLED
|
|
64
|
+
--------------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
[SECTION A: FOUNDATIONAL MUMO MODEL (Small Sample Sizes: n, k = 1)]
|
|
67
|
+
|
|
68
|
+
1. Mumo Potential Upside (MPU)
|
|
69
|
+
* Formula: MPU = ( x_n + (sum(x_i for i=1 to n-1) / (n-1)) ) / 2
|
|
70
|
+
* Symbol Definitions:
|
|
71
|
+
- x_n : The single maximum value in the sorted dataset X.
|
|
72
|
+
- x_i : Individual baseline entries in the sorted dataset.
|
|
73
|
+
- n : Total number of data points.
|
|
74
|
+
* Function: Evaluates maximum peak performance capability relative to baseline.
|
|
75
|
+
* Gap Filled: Replaces raw maximum values (which are highly vulnerable to
|
|
76
|
+
single data entry errors or noise spikes) with a baseline-anchored peak limit.
|
|
77
|
+
|
|
78
|
+
2. Mumo Potential Downside (MPD)
|
|
79
|
+
* Formula: MPD = ( x_1 + (sum(x_i for i=2 to n) / (n-1)) ) / 2
|
|
80
|
+
* Symbol Definitions:
|
|
81
|
+
- x_1 : The single minimum value in the sorted dataset X.
|
|
82
|
+
- x_i : Individual baseline entries in the upper dataset.
|
|
83
|
+
- n : Total number of data points.
|
|
84
|
+
* Function: Evaluates system risk floor safety relative to the remaining baseline.
|
|
85
|
+
* Gap Filled: Eliminates raw minimum metrics by evaluating worst-case failure
|
|
86
|
+
risk in proportion to baseline health.
|
|
87
|
+
|
|
88
|
+
3. Mumo Average (MA)
|
|
89
|
+
* Formula: MA = ( MPU + MPD ) / 2
|
|
90
|
+
* Function: Establishes the central equilibrium point between MPU and MPD.
|
|
91
|
+
* Gap Filled: Provides an unwarped balance point for small sample counts.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
[SECTION B: GENERALIZED & PARAMETRIC BOUNDARY MODULES (Big Data & Slice k)]
|
|
95
|
+
|
|
96
|
+
4. Parametric Mumo Potential Upside (P-MPU)
|
|
97
|
+
* Formula: P-MPU = alpha * (sum(top k) / k) + (1 - alpha) * (sum(remaining) / (n - k))
|
|
98
|
+
* Symbol Definitions:
|
|
99
|
+
- k : Boundary slice size (number of top/bottom boundary elements).
|
|
100
|
+
- alpha : Weighting slider controlling boundary sensitivity (0 <= alpha <= 1).
|
|
101
|
+
- n : Total size of the data array.
|
|
102
|
+
* Function: Calculates continuous upper performance capacity across massive data streams.
|
|
103
|
+
* Gap Filled: Prevents single data typos or corrupted streaming telemetry from
|
|
104
|
+
warping performance capacity estimates.
|
|
105
|
+
|
|
106
|
+
5. Parametric Mumo Potential Downside (P-MPD)
|
|
107
|
+
* Formula: P-MPD = alpha * (sum(bottom k) / k) + (1 - alpha) * (sum(remaining) / (n - k))
|
|
108
|
+
* Function: Establishes a flexible operational safety floor across large database streams.
|
|
109
|
+
* Gap Filled: Stops transient system glitches from triggering false automated
|
|
110
|
+
emergency shutdown routines.
|
|
111
|
+
|
|
112
|
+
6. Parametric Generalized Mumo Average (P-GMA)
|
|
113
|
+
* Formula: P-GMA = beta * (P-MPU) + (1 - beta) * (P-MPD)
|
|
114
|
+
* Symbol Definitions:
|
|
115
|
+
- beta : Risk-reward balance parameter slider (0 <= beta <= 1).
|
|
116
|
+
* Function: Blends peak upside boundaries and risk floors according to operational goals.
|
|
117
|
+
* Gap Filled: Enables real-time tuning of automated trading bots or server network
|
|
118
|
+
throttling based on target risk tolerance.
|
|
119
|
+
|
|
120
|
+
7. Mumo Bounds of Variance (MBV)
|
|
121
|
+
* Formula: MBV = P-MPU - P-MPD
|
|
122
|
+
* Function: Measures the physical width of the operational risk corridor.
|
|
123
|
+
* Gap Filled: Replaces statistical standard deviation (which yields abstract
|
|
124
|
+
squared units) with a unit-accurate physical range measurement matching the input data.
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
[SECTION C: CORE STABILISERS]
|
|
128
|
+
|
|
129
|
+
8. Parametric Generalized Mumo Core-Squeeze Index (P-GMCSI)
|
|
130
|
+
* Formula: P-GMCSI = alpha * (sum(Core Cluster C_k) / k) + (1 - alpha) * (sum(Remaining Mass) / (n - k))
|
|
131
|
+
* Symbol Definitions:
|
|
132
|
+
- C_k : The contiguous central k-value slice of the sorted dataset.
|
|
133
|
+
- R_{n-k} : Every value outside C_k; this set contains n-k values.
|
|
134
|
+
- Slice position: start at index floor((n-k)/2) in the sorted dataset.
|
|
135
|
+
* Function: Squeezes middle elements to produce an uncorrupted central consensus value.
|
|
136
|
+
* Gap Filled: Eliminates median flipping in large datasets while preserving full
|
|
137
|
+
consensus awareness of outer wing mass.
|
|
138
|
+
|
|
139
|
+
For dynamic fractional sizing, set k = max(1, floor(p*n)) (capped at n-1)
|
|
140
|
+
using autotune_parameters(data, fraction=p).k. This is the framework's
|
|
141
|
+
fractional-size mode (equivalent to delta=1); omitting it leaves k under
|
|
142
|
+
caller control (equivalent to delta=0). The calculation functions
|
|
143
|
+
themselves remain deterministic and accept k as an ordinary parameter.
|
|
144
|
+
When using k for P-MBTS, MAEI, or MCHI as well, choose p < 0.5 so
|
|
145
|
+
2*k < n and P-MBTS has at least one value remaining between its two
|
|
146
|
+
boundary slices.
|
|
147
|
+
|
|
148
|
+
9. Mumo Adaptive Equilibrium Index (MAEI)
|
|
149
|
+
* Formula: MAEI = gamma * (P-GMA) + (1 - gamma) * (P-GMCSI)
|
|
150
|
+
* Symbol Definitions:
|
|
151
|
+
- gamma : Balance switch between outer threat boundary tracking and inner core stability.
|
|
152
|
+
* Function: Master Engine unifying boundary limits and core consensus into one metric.
|
|
153
|
+
* Gap Filled: Provides hardware engines with dynamic mathematical survival instincts.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
[SECTION D: AUTOTUNE ZERO-BIAS ENGINE (SC-PGMF)]
|
|
157
|
+
|
|
158
|
+
10. Autotune Self-Calibration (SC-MPU, SC-MPD, SC-GMA, SC-GMCSI, SC-MAEI)
|
|
159
|
+
* Formulas:
|
|
160
|
+
k_auto = floor(p * n)
|
|
161
|
+
alpha_auto = (x_n - x_1) / mean(x)
|
|
162
|
+
beta_auto = (x_n - mean(x)) / MBV_baseline
|
|
163
|
+
* Symbol Definitions:
|
|
164
|
+
- p : Target fractional boundary depth (e.g., 0.05 for 5%).
|
|
165
|
+
- delta, omega : Mode switches (0 = manual, 1 = fully automated autotune).
|
|
166
|
+
* Function: Auto-derives k, alpha, and beta parameters straight from data volume and variance.
|
|
167
|
+
* Gap Filled: Erases human parameter bias and eliminates manual recalibration during sudden market spikes.
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
[SECTION E: CALCULUS TENSORS & MULTI-DIMENSIONAL EXTENSIONS]
|
|
171
|
+
|
|
172
|
+
11. Risk Derivatives (Velocity & Acceleration)
|
|
173
|
+
* Formulas:
|
|
174
|
+
Velocity = d(MBV)/dt = d(P-MPU)/dt - d(P-MPD)/dt
|
|
175
|
+
Acceleration = d^2(MBV)/dt^2
|
|
176
|
+
* Function: Tracks the rate of expansion and acceleration of system risk corridors over time vectors.
|
|
177
|
+
* Gap Filled: Predicts market crashes or hardware failures before safety floors break by detecting positive acceleration.
|
|
178
|
+
|
|
179
|
+
12. Cumulative Risk Mass Integration (Total Exposure Volume)
|
|
180
|
+
* Formula: Total Exposure Volume = integral_a^b ( P-MPD(t) - f(t) ) dt
|
|
181
|
+
* Symbol Definitions:
|
|
182
|
+
- f(t) : Continuous metric spot curve over time t from a to b.
|
|
183
|
+
* Function: Integrates continuous structural strain volume over time windows.
|
|
184
|
+
* Gap Filled: Quantifies cumulative physical fatigue or financial drawdown duration rather than static point snapshots.
|
|
185
|
+
|
|
186
|
+
13. Multi-Dimensional Risk Matrix (Tensor Surface Transformation)
|
|
187
|
+
* Formula: Y = Omega * X
|
|
188
|
+
* Symbol Definitions:
|
|
189
|
+
- X : High-dimensional data matrix of multi-asset streams.
|
|
190
|
+
- Omega : Automated parameter state matrix.
|
|
191
|
+
* Function: Executes multi-asset risk surface calculations simultaneously.
|
|
192
|
+
* Gap Filled: Enables GPU hardware to compute thousands of asset risk metrics in a single clock cycle.
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
--------------------------------------------------------------------------------
|
|
196
|
+
4. INSTALLATION & QUICKSTART
|
|
197
|
+
--------------------------------------------------------------------------------
|
|
198
|
+
Install via PyPI:
|
|
199
|
+
pip install mumo-engine
|
|
200
|
+
|
|
201
|
+
Python Example:
|
|
202
|
+
from mumo_framework import MumoEngine
|
|
203
|
+
|
|
204
|
+
data = [12.0, 15.2, 11.8, 45.0, 14.1, 13.9, 10.5]
|
|
205
|
+
|
|
206
|
+
# Calculate Ceiling, Safety Floor, and Risk Corridor Width
|
|
207
|
+
p_mpu = MumoEngine.calculate_p_mpu(data, k=2, alpha=0.7)
|
|
208
|
+
p_mpd = MumoEngine.calculate_p_mpd(data, k=2, alpha=0.7)
|
|
209
|
+
mbv = MumoEngine.calculate_mbv(data, k=2, alpha=0.7)
|
|
210
|
+
|
|
211
|
+
print(f"Performance Ceiling (P-MPU): {p_mpu:.2f}")
|
|
212
|
+
print(f"Safety Floor (P-MPD): {p_mpd:.2f}")
|
|
213
|
+
print(f"Risk Corridor Width (MBV): {mbv:.2f}")
|
|
214
|
+
|
|
215
|
+
Fractional core/boundary sizing:
|
|
216
|
+
params = MumoEngine.autotune_parameters(data, fraction=0.10)
|
|
217
|
+
p_gmcsi = MumoEngine.calculate_p_gmcsi(data, k=params.k, alpha=0.7)
|
|
218
|
+
|
|
219
|
+
For combined P-MBTS, MAEI, or MCHI calculations, use a fraction below 0.5
|
|
220
|
+
and a dataset of at least three values so the returned k satisfies 2*k < n.
|
|
221
|
+
|
|
222
|
+
Interactive calculator:
|
|
223
|
+
python -m mumo_framework
|
|
224
|
+
|
|
225
|
+
The calculator accepts numbers separated by commas or spaces over as many
|
|
226
|
+
input lines as needed. Submit a blank line to finish the dataset, then enter
|
|
227
|
+
k, alpha, beta, and gamma. Its dataset size is limited by available memory.
|
|
228
|
+
P-MBTS uses the mean of the values remaining after removing the lowest k and
|
|
229
|
+
highest k entries as mu_rest_core; therefore, it requires 2*k < n.
|
|
230
|
+
|
|
231
|
+
Interactive desktop calculator:
|
|
232
|
+
mumo-framework-gui
|
|
233
|
+
|
|
234
|
+
Or, when running from the source checkout:
|
|
235
|
+
python -m mumo_gui
|
|
236
|
+
|
|
237
|
+
The desktop calculator lets you edit or paste a dataset and interactively adjust
|
|
238
|
+
k, alpha, beta, and gamma. Results update as the data or parameters change.
|
|
239
|
+
It uses Python's built-in Tkinter interface and adds no third-party dependencies.
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
--------------------------------------------------------------------------------
|
|
243
|
+
5. LICENSE
|
|
244
|
+
--------------------------------------------------------------------------------
|
|
245
|
+
This project is licensed under the MIT License. See the accompanying LICENSE
|
|
246
|
+
file for full terms.
|
|
247
|
+
|
|
248
|
+
Based on the original Mumo Mathematical Framework created by Mumo Framework Authors (Root Project).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
mumo_framework.py,sha256=5Ic9wNniA8umITis_s0irgOOY2crl0gXMx-SWtTJlp4,19589
|
|
2
|
+
mumo_gui.py,sha256=Lc19gUEQ6Y0Od7fE6XLVJ0yCGkAZSDaMqcmHAfg8Nhw,12938
|
|
3
|
+
mumo_engine-1.0.0.dist-info/licenses/LICENSE,sha256=U98_Lr-sm-XQxYvF_87awv4rBBnbTQLROaUFmOmQ-OA,1079
|
|
4
|
+
mumo_engine-1.0.0.dist-info/METADATA,sha256=MScomEZKyWZNhSnUPQjkcXFuDyGTMeE-Iwc7J6XmFC0,12111
|
|
5
|
+
mumo_engine-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
6
|
+
mumo_engine-1.0.0.dist-info/entry_points.txt,sha256=HxnKiwgwGLTM7HOf_LmoEZQu-n5vUkQzDBkreke5xvU,90
|
|
7
|
+
mumo_engine-1.0.0.dist-info/top_level.txt,sha256=kYMn7AUPAdA9TtL6p7zCLtuSmmOspilP-lG1TcTjehw,24
|
|
8
|
+
mumo_engine-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mumo Framework Authors
|
|
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.
|
mumo_framework.py
ADDED
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""Statistical boundary and risk-corridor calculations for the Mumo framework.
|
|
2
|
+
|
|
3
|
+
Based on the original Mumo Mathematical Framework created by Mumo Framework Authors (Root Project).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
import math
|
|
10
|
+
from typing import Iterable, List, Optional, Sequence, Tuple
|
|
11
|
+
|
|
12
|
+
__version__ = "1.0.0"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _finite_float(value: object, name: str) -> float:
|
|
16
|
+
if isinstance(value, (bool, str, bytes)):
|
|
17
|
+
raise TypeError("{} must be a real number".format(name))
|
|
18
|
+
try:
|
|
19
|
+
result = float(value)
|
|
20
|
+
except (TypeError, ValueError, OverflowError) as exc:
|
|
21
|
+
raise TypeError("{} must be a real number".format(name)) from exc
|
|
22
|
+
if not math.isfinite(result):
|
|
23
|
+
raise ValueError("{} must be finite".format(name))
|
|
24
|
+
return result
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _ordered_data(data: Iterable[float]) -> List[float]:
|
|
28
|
+
if isinstance(data, (str, bytes)):
|
|
29
|
+
raise TypeError("data must be an iterable of numbers")
|
|
30
|
+
try:
|
|
31
|
+
iterator = iter(data)
|
|
32
|
+
except TypeError as exc:
|
|
33
|
+
raise TypeError("data must be an iterable of numbers") from exc
|
|
34
|
+
values = [_finite_float(value, "data value") for value in iterator]
|
|
35
|
+
if len(values) < 2:
|
|
36
|
+
raise ValueError("data must contain at least two values")
|
|
37
|
+
return sorted(values)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _positive_integer(value: int, name: str) -> int:
|
|
41
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
42
|
+
raise TypeError("{} must be an integer".format(name))
|
|
43
|
+
if value <= 0:
|
|
44
|
+
raise ValueError("{} must be greater than zero".format(name))
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _slice_size(k: int, n: int) -> int:
|
|
49
|
+
value = _positive_integer(k, "k")
|
|
50
|
+
if value >= n:
|
|
51
|
+
raise ValueError("k must be smaller than the number of data values")
|
|
52
|
+
return value
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _weight(value: float, name: str) -> float:
|
|
56
|
+
result = _finite_float(value, name)
|
|
57
|
+
if not 0.0 <= result <= 1.0:
|
|
58
|
+
raise ValueError("{} must be between 0 and 1".format(name))
|
|
59
|
+
return result
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _mean(values: Sequence[float]) -> float:
|
|
63
|
+
return math.fsum(values) / len(values)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def calculate_mpu(data: Iterable[float]) -> float:
|
|
67
|
+
"""Return MPU: the average of the maximum and the remaining-data mean."""
|
|
68
|
+
values = _ordered_data(data)
|
|
69
|
+
return (values[-1] + _mean(values[:-1])) / 2.0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def calculate_mpd(data: Iterable[float]) -> float:
|
|
73
|
+
"""Return MPD: the average of the minimum and the remaining-data mean."""
|
|
74
|
+
values = _ordered_data(data)
|
|
75
|
+
return (values[0] + _mean(values[1:])) / 2.0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def calculate_ma(data: Iterable[float]) -> float:
|
|
79
|
+
"""Return the foundational Mumo average, (MPU + MPD) / 2."""
|
|
80
|
+
return (calculate_mpu(data) + calculate_mpd(data)) / 2.0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def calculate_mbts(data: Iterable[float]) -> float:
|
|
84
|
+
"""Return MBTS using the edge range and the mean of interior values."""
|
|
85
|
+
values = _ordered_data(data)
|
|
86
|
+
if len(values) < 3:
|
|
87
|
+
raise ValueError("MBTS requires at least three data values")
|
|
88
|
+
rest_mean = _mean(values[1:-1])
|
|
89
|
+
return ((values[-1] - values[0]) / 2.0 + rest_mean) / 2.0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def calculate_mpu_k(data: Iterable[float], k: int) -> float:
|
|
93
|
+
"""Return the unweighted generalized upside metric for the top k values."""
|
|
94
|
+
values = _ordered_data(data)
|
|
95
|
+
size = _slice_size(k, len(values))
|
|
96
|
+
return (_mean(values[-size:]) + _mean(values[:-size])) / 2.0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def calculate_mpd_k(data: Iterable[float], k: int) -> float:
|
|
100
|
+
"""Return the unweighted generalized downside metric for the bottom k."""
|
|
101
|
+
values = _ordered_data(data)
|
|
102
|
+
size = _slice_size(k, len(values))
|
|
103
|
+
return (_mean(values[:size]) + _mean(values[size:])) / 2.0
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def calculate_gma_k(data: Iterable[float], k: int) -> float:
|
|
107
|
+
"""Return GMA for a boundary slice of size k."""
|
|
108
|
+
return (calculate_mpu_k(data, k) + calculate_mpd_k(data, k)) / 2.0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def calculate_p_mpu(data: Iterable[float], k: int, alpha: float = 0.5) -> float:
|
|
112
|
+
"""Return parametric upside, weighting the top-k slice by alpha."""
|
|
113
|
+
values = _ordered_data(data)
|
|
114
|
+
size = _slice_size(k, len(values))
|
|
115
|
+
weight = _weight(alpha, "alpha")
|
|
116
|
+
return weight * _mean(values[-size:]) + (1.0 - weight) * _mean(values[:-size])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def calculate_p_mpd(data: Iterable[float], k: int, alpha: float = 0.5) -> float:
|
|
120
|
+
"""Return parametric downside, weighting the bottom-k slice by alpha."""
|
|
121
|
+
values = _ordered_data(data)
|
|
122
|
+
size = _slice_size(k, len(values))
|
|
123
|
+
weight = _weight(alpha, "alpha")
|
|
124
|
+
return weight * _mean(values[:size]) + (1.0 - weight) * _mean(values[size:])
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def calculate_p_gma(
|
|
128
|
+
data: Iterable[float],
|
|
129
|
+
k: int,
|
|
130
|
+
alpha: float = 0.5,
|
|
131
|
+
beta: float = 0.5,
|
|
132
|
+
) -> float:
|
|
133
|
+
"""Blend parametric upside and downside using beta."""
|
|
134
|
+
balance = _weight(beta, "beta")
|
|
135
|
+
upside = calculate_p_mpu(data, k, alpha)
|
|
136
|
+
downside = calculate_p_mpd(data, k, alpha)
|
|
137
|
+
return balance * upside + (1.0 - balance) * downside
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def calculate_mbv(data: Iterable[float], k: int, alpha: float = 0.5) -> float:
|
|
141
|
+
"""Return MBV, the unit-preserving width between parametric boundaries."""
|
|
142
|
+
return calculate_p_mpu(data, k, alpha) - calculate_p_mpd(data, k, alpha)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def calculate_p_mbts(
|
|
146
|
+
data: Iterable[float], k: int, alpha: float = 0.5
|
|
147
|
+
) -> float:
|
|
148
|
+
"""Return parametric MBTS using edge slices and the remaining-data mean.
|
|
149
|
+
|
|
150
|
+
The supplied formula's ``mu_rest_core`` is interpreted as the mean of
|
|
151
|
+
values left after removing the bottom-k and top-k boundary slices.
|
|
152
|
+
"""
|
|
153
|
+
values = _ordered_data(data)
|
|
154
|
+
size = _slice_size(k, len(values))
|
|
155
|
+
if 2 * size >= len(values):
|
|
156
|
+
raise ValueError("P-MBTS requires at least one value outside both k-slices")
|
|
157
|
+
weight = _weight(alpha, "alpha")
|
|
158
|
+
upper_mean = _mean(values[-size:])
|
|
159
|
+
lower_mean = _mean(values[:size])
|
|
160
|
+
rest_core_mean = _mean(values[size:-size])
|
|
161
|
+
return ((upper_mean - lower_mean) / 2.0 + weight * rest_core_mean) / 2.0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def calculate_p_gmcsi(
|
|
165
|
+
data: Iterable[float], k: int, alpha: float = 0.5
|
|
166
|
+
) -> float:
|
|
167
|
+
"""Return P-GMCSI using the centered k-value core cluster.
|
|
168
|
+
|
|
169
|
+
Values are sorted first. The core is the contiguous slice starting at
|
|
170
|
+
``floor((n - k) / 2)``; the remainder contains every value outside it.
|
|
171
|
+
For fractional sizing, obtain ``k`` from
|
|
172
|
+
``autotune_parameters(data, fraction=p).k``.
|
|
173
|
+
"""
|
|
174
|
+
values = _ordered_data(data)
|
|
175
|
+
size = _slice_size(k, len(values))
|
|
176
|
+
weight = _weight(alpha, "alpha")
|
|
177
|
+
start = (len(values) - size) // 2
|
|
178
|
+
core = values[start : start + size]
|
|
179
|
+
remainder = values[:start] + values[start + size :]
|
|
180
|
+
return weight * _mean(core) + (1.0 - weight) * _mean(remainder)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def calculate_maei(
|
|
184
|
+
data: Iterable[float],
|
|
185
|
+
k: int,
|
|
186
|
+
alpha: float = 0.5,
|
|
187
|
+
beta: float = 0.5,
|
|
188
|
+
gamma: float = 0.5,
|
|
189
|
+
) -> float:
|
|
190
|
+
"""Blend the generalized average and core-squeeze index using gamma."""
|
|
191
|
+
balance = _weight(gamma, "gamma")
|
|
192
|
+
generalized = calculate_p_gma(data, k, alpha, beta)
|
|
193
|
+
core = calculate_p_gmcsi(data, k, alpha)
|
|
194
|
+
return balance * generalized + (1.0 - balance) * core
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def calculate_mchi(
|
|
198
|
+
data: Iterable[float],
|
|
199
|
+
k: int,
|
|
200
|
+
alpha: float = 0.5,
|
|
201
|
+
gamma: float = 0.5,
|
|
202
|
+
) -> float:
|
|
203
|
+
"""Blend parametric MBTS and the core-squeeze index using gamma."""
|
|
204
|
+
balance = _weight(gamma, "gamma")
|
|
205
|
+
tension = calculate_p_mbts(data, k, alpha)
|
|
206
|
+
core = calculate_p_gmcsi(data, k, alpha)
|
|
207
|
+
return balance * tension + (1.0 - balance) * core
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@dataclass(frozen=True)
|
|
211
|
+
class MumoParameters:
|
|
212
|
+
"""Autotuned parameters for a dataset."""
|
|
213
|
+
|
|
214
|
+
k: int
|
|
215
|
+
alpha: float
|
|
216
|
+
beta: float
|
|
217
|
+
gamma: float = 0.5
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def autotune_parameters(
|
|
221
|
+
data: Iterable[float],
|
|
222
|
+
fraction: float = 0.05,
|
|
223
|
+
mbv_baseline: Optional[float] = None,
|
|
224
|
+
gamma: float = 0.5,
|
|
225
|
+
) -> MumoParameters:
|
|
226
|
+
"""Derive bounded k, alpha, and beta values from a dataset.
|
|
227
|
+
|
|
228
|
+
The boundary/core size is ``floor(fraction * n)``, clamped to
|
|
229
|
+
``[1, n - 1]`` so both the selected slice and its remainder are nonempty.
|
|
230
|
+
The specification's alpha and beta ratios are clamped to [0, 1]. If no
|
|
231
|
+
MBV baseline is supplied, the observed data range is used. Constant data
|
|
232
|
+
receives neutral alpha/beta settings where the corresponding ratio is
|
|
233
|
+
undefined.
|
|
234
|
+
|
|
235
|
+
When the returned ``k`` will also be used for P-MBTS, MAEI, or MCHI, it
|
|
236
|
+
must additionally satisfy ``2 * k < n``; choose a fraction below 0.5
|
|
237
|
+
for the usual fractional-sizing workflow.
|
|
238
|
+
"""
|
|
239
|
+
values = _ordered_data(data)
|
|
240
|
+
proportion = _finite_float(fraction, "fraction")
|
|
241
|
+
if not 0.0 < proportion <= 1.0:
|
|
242
|
+
raise ValueError("fraction must be greater than 0 and at most 1")
|
|
243
|
+
balance = _weight(gamma, "gamma")
|
|
244
|
+
|
|
245
|
+
size = min(len(values) - 1, max(1, math.floor(proportion * len(values))))
|
|
246
|
+
span = values[-1] - values[0]
|
|
247
|
+
average = _mean(values)
|
|
248
|
+
if span == 0.0:
|
|
249
|
+
alpha = 0.0
|
|
250
|
+
elif average == 0.0:
|
|
251
|
+
alpha = 1.0
|
|
252
|
+
else:
|
|
253
|
+
alpha = min(1.0, max(0.0, span / abs(average)))
|
|
254
|
+
|
|
255
|
+
baseline = span if mbv_baseline is None else _finite_float(
|
|
256
|
+
mbv_baseline, "mbv_baseline"
|
|
257
|
+
)
|
|
258
|
+
if baseline < 0.0:
|
|
259
|
+
raise ValueError("mbv_baseline must not be negative")
|
|
260
|
+
if baseline == 0.0:
|
|
261
|
+
if span != 0.0:
|
|
262
|
+
raise ValueError("mbv_baseline must be non-zero for non-constant data")
|
|
263
|
+
beta = 0.5
|
|
264
|
+
else:
|
|
265
|
+
beta = min(1.0, max(0.0, (values[-1] - average) / baseline))
|
|
266
|
+
return MumoParameters(k=size, alpha=alpha, beta=beta, gamma=balance)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _time_values(times: Optional[Sequence[float]], count: int) -> List[float]:
|
|
270
|
+
if times is None:
|
|
271
|
+
return [float(index) for index in range(count)]
|
|
272
|
+
if len(times) != count:
|
|
273
|
+
raise ValueError("times and values must have the same length")
|
|
274
|
+
result = [_finite_float(value, "time") for value in times]
|
|
275
|
+
if any(right <= left for left, right in zip(result, result[1:])):
|
|
276
|
+
raise ValueError("times must be strictly increasing")
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def risk_velocity(
|
|
281
|
+
mbv_values: Iterable[float], times: Optional[Sequence[float]] = None
|
|
282
|
+
) -> List[float]:
|
|
283
|
+
"""Return interval risk velocities (first differences of MBV over time)."""
|
|
284
|
+
values = [_finite_float(value, "MBV value") for value in mbv_values]
|
|
285
|
+
if len(values) < 2:
|
|
286
|
+
raise ValueError("at least two MBV values are required")
|
|
287
|
+
time_values = _time_values(times, len(values))
|
|
288
|
+
return [
|
|
289
|
+
(right_value - left_value) / (right_time - left_time)
|
|
290
|
+
for left_value, right_value, left_time, right_time in zip(
|
|
291
|
+
values, values[1:], time_values, time_values[1:]
|
|
292
|
+
)
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def risk_acceleration(
|
|
297
|
+
mbv_values: Iterable[float], times: Optional[Sequence[float]] = None
|
|
298
|
+
) -> List[float]:
|
|
299
|
+
"""Return acceleration estimates between adjacent risk-velocity intervals."""
|
|
300
|
+
values = [_finite_float(value, "MBV value") for value in mbv_values]
|
|
301
|
+
if len(values) < 3:
|
|
302
|
+
raise ValueError("at least three MBV values are required")
|
|
303
|
+
time_values = _time_values(times, len(values))
|
|
304
|
+
velocity = risk_velocity(values, time_values)
|
|
305
|
+
midpoints = [
|
|
306
|
+
(left + right) / 2.0 for left, right in zip(time_values, time_values[1:])
|
|
307
|
+
]
|
|
308
|
+
return [
|
|
309
|
+
(right_velocity - left_velocity) / (right_time - left_time)
|
|
310
|
+
for left_velocity, right_velocity, left_time, right_time in zip(
|
|
311
|
+
velocity, velocity[1:], midpoints, midpoints[1:]
|
|
312
|
+
)
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def cumulative_exposure(
|
|
317
|
+
floor_values: Iterable[float],
|
|
318
|
+
observations: Iterable[float],
|
|
319
|
+
times: Optional[Sequence[float]] = None,
|
|
320
|
+
) -> float:
|
|
321
|
+
"""Integrate floor minus observed values with the trapezoidal rule."""
|
|
322
|
+
floors = [_finite_float(value, "floor value") for value in floor_values]
|
|
323
|
+
spots = [_finite_float(value, "observation") for value in observations]
|
|
324
|
+
if len(floors) != len(spots):
|
|
325
|
+
raise ValueError("floor_values and observations must have the same length")
|
|
326
|
+
if len(floors) < 2:
|
|
327
|
+
raise ValueError("at least two points are required for integration")
|
|
328
|
+
time_values = _time_values(times, len(floors))
|
|
329
|
+
gaps = [floor - spot for floor, spot in zip(floors, spots)]
|
|
330
|
+
return math.fsum(
|
|
331
|
+
(left_gap + right_gap) * (right_time - left_time) / 2.0
|
|
332
|
+
for left_gap, right_gap, left_time, right_time in zip(
|
|
333
|
+
gaps, gaps[1:], time_values, time_values[1:]
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _matrix(values: Iterable[Iterable[float]], name: str) -> List[List[float]]:
|
|
339
|
+
if isinstance(values, (str, bytes)):
|
|
340
|
+
raise TypeError("{} must be a matrix".format(name))
|
|
341
|
+
try:
|
|
342
|
+
rows = [
|
|
343
|
+
[_finite_float(value, "{} value".format(name)) for value in row]
|
|
344
|
+
for row in values
|
|
345
|
+
]
|
|
346
|
+
except TypeError as exc:
|
|
347
|
+
raise TypeError("{} must be a matrix of numbers".format(name)) from exc
|
|
348
|
+
if not rows or not rows[0]:
|
|
349
|
+
raise ValueError("{} must not be empty".format(name))
|
|
350
|
+
width = len(rows[0])
|
|
351
|
+
if any(len(row) != width for row in rows):
|
|
352
|
+
raise ValueError("{} must be rectangular".format(name))
|
|
353
|
+
return rows
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def boundary_matrix(
|
|
357
|
+
data: Iterable[Iterable[float]], k: int, alpha: float = 0.5
|
|
358
|
+
) -> List[Tuple[float, float, float]]:
|
|
359
|
+
"""Calculate (P-MPU, P-MPD, MBV) for each row in a dataset matrix."""
|
|
360
|
+
rows = _matrix(data, "data")
|
|
361
|
+
return [
|
|
362
|
+
(
|
|
363
|
+
calculate_p_mpu(row, k, alpha),
|
|
364
|
+
calculate_p_mpd(row, k, alpha),
|
|
365
|
+
calculate_mbv(row, k, alpha),
|
|
366
|
+
)
|
|
367
|
+
for row in rows
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def matrix_multiply(
|
|
372
|
+
omega: Iterable[Iterable[float]], data: Iterable[Iterable[float]]
|
|
373
|
+
) -> List[List[float]]:
|
|
374
|
+
"""Multiply two compatible rectangular matrices (Y = Omega * X)."""
|
|
375
|
+
left = _matrix(omega, "omega")
|
|
376
|
+
right = _matrix(data, "data")
|
|
377
|
+
if len(left[0]) != len(right):
|
|
378
|
+
raise ValueError("omega columns must match data rows")
|
|
379
|
+
columns = list(zip(*right))
|
|
380
|
+
return [
|
|
381
|
+
[math.fsum(a * b for a, b in zip(row, column)) for column in columns]
|
|
382
|
+
for row in left
|
|
383
|
+
]
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _read_interactive_dataset() -> List[float]:
|
|
387
|
+
print("Enter numbers separated by spaces or commas; use multiple lines.")
|
|
388
|
+
print("Press Enter on a blank line when you have finished the dataset.")
|
|
389
|
+
values: List[float] = []
|
|
390
|
+
while True:
|
|
391
|
+
line = input("data> ").strip()
|
|
392
|
+
if not line:
|
|
393
|
+
break
|
|
394
|
+
try:
|
|
395
|
+
line_values = [
|
|
396
|
+
_finite_float(token, "data value")
|
|
397
|
+
for token in line.replace(",", " ").split()
|
|
398
|
+
]
|
|
399
|
+
except (TypeError, ValueError) as exc:
|
|
400
|
+
print("Invalid data: {}. Please re-enter that line.".format(exc))
|
|
401
|
+
continue
|
|
402
|
+
values.extend(line_values)
|
|
403
|
+
return values
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _read_interactive_float(prompt: str) -> float:
|
|
407
|
+
while True:
|
|
408
|
+
try:
|
|
409
|
+
return _finite_float(input(prompt).strip(), prompt.strip())
|
|
410
|
+
except (TypeError, ValueError) as exc:
|
|
411
|
+
print("Invalid number: {}. Please try again.".format(exc))
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _read_interactive_int(prompt: str) -> int:
|
|
415
|
+
while True:
|
|
416
|
+
raw = input(prompt).strip()
|
|
417
|
+
try:
|
|
418
|
+
return int(raw)
|
|
419
|
+
except ValueError:
|
|
420
|
+
print("Please enter a whole number.")
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _read_interactive_k(count: int) -> int:
|
|
424
|
+
maximum = (count - 1) // 2
|
|
425
|
+
while True:
|
|
426
|
+
value = _read_interactive_int(
|
|
427
|
+
"Boundary/core size k (1 to {}): ".format(maximum)
|
|
428
|
+
)
|
|
429
|
+
if 1 <= value <= maximum:
|
|
430
|
+
return value
|
|
431
|
+
print("For these formulas, k must be at least 1 and 2*k must be < n.")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _read_interactive_weight(name: str) -> float:
|
|
435
|
+
while True:
|
|
436
|
+
value = _read_interactive_float("{} (0 to 1): ".format(name))
|
|
437
|
+
try:
|
|
438
|
+
return _weight(value, name)
|
|
439
|
+
except ValueError as exc:
|
|
440
|
+
print("{} Please try again.".format(exc))
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def main() -> int:
|
|
444
|
+
"""Run an interactive prompt for calculating Mumo formulas."""
|
|
445
|
+
print("Mumo Framework interactive calculator")
|
|
446
|
+
print("Enter as many data values as needed; available memory is the limit.")
|
|
447
|
+
try:
|
|
448
|
+
while True:
|
|
449
|
+
try:
|
|
450
|
+
data = _read_interactive_dataset()
|
|
451
|
+
values = _ordered_data(data)
|
|
452
|
+
k = _read_interactive_k(len(values))
|
|
453
|
+
alpha = _read_interactive_weight("alpha")
|
|
454
|
+
beta = _read_interactive_weight("beta")
|
|
455
|
+
gamma = _read_interactive_weight("gamma")
|
|
456
|
+
|
|
457
|
+
results = [
|
|
458
|
+
("MPU", calculate_mpu(values)),
|
|
459
|
+
("MPD", calculate_mpd(values)),
|
|
460
|
+
("MA", calculate_ma(values)),
|
|
461
|
+
("MBTS", calculate_mbts(values)),
|
|
462
|
+
("MPU-k", calculate_mpu_k(values, k)),
|
|
463
|
+
("MPD-k", calculate_mpd_k(values, k)),
|
|
464
|
+
("GMA-k", calculate_gma_k(values, k)),
|
|
465
|
+
("P-MPU", calculate_p_mpu(values, k, alpha)),
|
|
466
|
+
("P-MPD", calculate_p_mpd(values, k, alpha)),
|
|
467
|
+
("P-GMA", calculate_p_gma(values, k, alpha, beta)),
|
|
468
|
+
("MBV", calculate_mbv(values, k, alpha)),
|
|
469
|
+
("P-MBTS", calculate_p_mbts(values, k, alpha)),
|
|
470
|
+
("P-GMCSI", calculate_p_gmcsi(values, k, alpha)),
|
|
471
|
+
("MAEI", calculate_maei(values, k, alpha, beta, gamma)),
|
|
472
|
+
("MCHI", calculate_mchi(values, k, alpha, gamma)),
|
|
473
|
+
]
|
|
474
|
+
print("\nResults ({} values):".format(len(values)))
|
|
475
|
+
for name, result in results:
|
|
476
|
+
print("{:10s} {:.10g}".format(name + ":", result))
|
|
477
|
+
print("Autotuned:", autotune_parameters(values))
|
|
478
|
+
except (TypeError, ValueError) as exc:
|
|
479
|
+
print("Could not calculate results: {}".format(exc))
|
|
480
|
+
|
|
481
|
+
again = input("\nCalculate another dataset? [y/N]: ").strip().lower()
|
|
482
|
+
if again not in ("y", "yes"):
|
|
483
|
+
return 0
|
|
484
|
+
except (EOFError, KeyboardInterrupt):
|
|
485
|
+
print("\nExiting Mumo calculator.")
|
|
486
|
+
return 0
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class MumoEngine:
|
|
490
|
+
"""Convenient namespace exposing the framework's calculations."""
|
|
491
|
+
|
|
492
|
+
calculate_mpu = staticmethod(calculate_mpu)
|
|
493
|
+
calculate_mpd = staticmethod(calculate_mpd)
|
|
494
|
+
calculate_ma = staticmethod(calculate_ma)
|
|
495
|
+
calculate_mbts = staticmethod(calculate_mbts)
|
|
496
|
+
calculate_mpu_k = staticmethod(calculate_mpu_k)
|
|
497
|
+
calculate_mpd_k = staticmethod(calculate_mpd_k)
|
|
498
|
+
calculate_gma_k = staticmethod(calculate_gma_k)
|
|
499
|
+
calculate_p_mpu = staticmethod(calculate_p_mpu)
|
|
500
|
+
calculate_p_mpd = staticmethod(calculate_p_mpd)
|
|
501
|
+
calculate_p_gma = staticmethod(calculate_p_gma)
|
|
502
|
+
calculate_mbv = staticmethod(calculate_mbv)
|
|
503
|
+
calculate_p_mbts = staticmethod(calculate_p_mbts)
|
|
504
|
+
calculate_p_gmcsi = staticmethod(calculate_p_gmcsi)
|
|
505
|
+
calculate_maei = staticmethod(calculate_maei)
|
|
506
|
+
calculate_mchi = staticmethod(calculate_mchi)
|
|
507
|
+
autotune_parameters = staticmethod(autotune_parameters)
|
|
508
|
+
risk_velocity = staticmethod(risk_velocity)
|
|
509
|
+
risk_acceleration = staticmethod(risk_acceleration)
|
|
510
|
+
cumulative_exposure = staticmethod(cumulative_exposure)
|
|
511
|
+
boundary_matrix = staticmethod(boundary_matrix)
|
|
512
|
+
matrix_multiply = staticmethod(matrix_multiply)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
__all__ = [
|
|
516
|
+
"MumoEngine",
|
|
517
|
+
"MumoParameters",
|
|
518
|
+
"__version__",
|
|
519
|
+
"autotune_parameters",
|
|
520
|
+
"boundary_matrix",
|
|
521
|
+
"calculate_gma_k",
|
|
522
|
+
"calculate_ma",
|
|
523
|
+
"calculate_maei",
|
|
524
|
+
"calculate_mbv",
|
|
525
|
+
"calculate_mbts",
|
|
526
|
+
"calculate_mchi",
|
|
527
|
+
"calculate_mpd",
|
|
528
|
+
"calculate_mpd_k",
|
|
529
|
+
"calculate_mpu",
|
|
530
|
+
"calculate_mpu_k",
|
|
531
|
+
"calculate_p_gma",
|
|
532
|
+
"calculate_p_gmcsi",
|
|
533
|
+
"calculate_p_mbts",
|
|
534
|
+
"calculate_p_mpd",
|
|
535
|
+
"calculate_p_mpu",
|
|
536
|
+
"cumulative_exposure",
|
|
537
|
+
"matrix_multiply",
|
|
538
|
+
"risk_acceleration",
|
|
539
|
+
"risk_velocity",
|
|
540
|
+
"main",
|
|
541
|
+
]
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
if __name__ == "__main__":
|
|
545
|
+
raise SystemExit(main())
|
mumo_gui.py
ADDED
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Interactive desktop calculator for the Mumo framework."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from tkinter import ttk
|
|
8
|
+
from typing import Dict, List
|
|
9
|
+
|
|
10
|
+
from mumo_framework import (
|
|
11
|
+
calculate_ma,
|
|
12
|
+
calculate_maei,
|
|
13
|
+
calculate_mbts,
|
|
14
|
+
calculate_mbv,
|
|
15
|
+
calculate_mchi,
|
|
16
|
+
calculate_mpd,
|
|
17
|
+
calculate_mpd_k,
|
|
18
|
+
calculate_mpu,
|
|
19
|
+
calculate_mpu_k,
|
|
20
|
+
calculate_p_gma,
|
|
21
|
+
calculate_p_gmcsi,
|
|
22
|
+
calculate_p_mbts,
|
|
23
|
+
calculate_p_mpd,
|
|
24
|
+
calculate_p_mpu,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
_SAMPLE_DATA = "10, 12.5, 14, 15.5, 18, 21, 25, 28.5, 32, 36"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _parse_dataset(text: str) -> List[float]:
|
|
32
|
+
"""Parse comma- or whitespace-separated finite numbers."""
|
|
33
|
+
tokens = text.replace(",", " ").split()
|
|
34
|
+
if not tokens:
|
|
35
|
+
raise ValueError("Enter at least three data values.")
|
|
36
|
+
|
|
37
|
+
values = []
|
|
38
|
+
for token in tokens:
|
|
39
|
+
try:
|
|
40
|
+
value = float(token)
|
|
41
|
+
except ValueError as exc:
|
|
42
|
+
raise ValueError("Could not read {!r} as a number.".format(token)) from exc
|
|
43
|
+
if not math.isfinite(value):
|
|
44
|
+
raise ValueError("All data values must be finite numbers.")
|
|
45
|
+
values.append(value)
|
|
46
|
+
if len(values) < 3:
|
|
47
|
+
raise ValueError("Enter at least three data values.")
|
|
48
|
+
return values
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _calculate_results(
|
|
52
|
+
data: List[float], k: int, alpha: float, beta: float, gamma: float
|
|
53
|
+
) -> Dict[str, float]:
|
|
54
|
+
"""Calculate the displayed metrics for the current inputs."""
|
|
55
|
+
return {
|
|
56
|
+
"MPU": calculate_mpu(data),
|
|
57
|
+
"MPD": calculate_mpd(data),
|
|
58
|
+
"MA": calculate_ma(data),
|
|
59
|
+
"MBTS": calculate_mbts(data),
|
|
60
|
+
"MPU-k": calculate_mpu_k(data, k),
|
|
61
|
+
"MPD-k": calculate_mpd_k(data, k),
|
|
62
|
+
"P-MPU": calculate_p_mpu(data, k, alpha),
|
|
63
|
+
"P-MPD": calculate_p_mpd(data, k, alpha),
|
|
64
|
+
"P-GMA": calculate_p_gma(data, k, alpha, beta),
|
|
65
|
+
"MBV": calculate_mbv(data, k, alpha),
|
|
66
|
+
"P-MBTS": calculate_p_mbts(data, k, alpha),
|
|
67
|
+
"P-GMCSI": calculate_p_gmcsi(data, k, alpha),
|
|
68
|
+
"MAEI": calculate_maei(data, k, alpha, beta, gamma),
|
|
69
|
+
"MCHI": calculate_mchi(data, k, alpha, gamma),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class MumoCalculator:
|
|
74
|
+
"""Tkinter interface with live dataset and parameter updates."""
|
|
75
|
+
|
|
76
|
+
def __init__(self, root: tk.Tk) -> None:
|
|
77
|
+
self.root = root
|
|
78
|
+
self.root.title("Mumo Framework | Interactive Calculator")
|
|
79
|
+
self.root.minsize(760, 650)
|
|
80
|
+
self.root.geometry("940x800")
|
|
81
|
+
self.root.configure(background="#f3f6fb")
|
|
82
|
+
self._pending_update: str | None = None
|
|
83
|
+
|
|
84
|
+
self.k = tk.IntVar(value=2)
|
|
85
|
+
self.alpha = tk.DoubleVar(value=0.6)
|
|
86
|
+
self.beta = tk.DoubleVar(value=0.5)
|
|
87
|
+
self.gamma = tk.DoubleVar(value=0.5)
|
|
88
|
+
self.status = tk.StringVar(value="Edit the data or adjust a parameter to recalculate.")
|
|
89
|
+
self.count = tk.StringVar(value="0 values")
|
|
90
|
+
self._metric_values: Dict[str, tk.StringVar] = {}
|
|
91
|
+
|
|
92
|
+
self._style()
|
|
93
|
+
self._build()
|
|
94
|
+
self._set_sample()
|
|
95
|
+
|
|
96
|
+
def _style(self) -> None:
|
|
97
|
+
style = ttk.Style(self.root)
|
|
98
|
+
style.configure("TFrame", background="#f3f6fb")
|
|
99
|
+
style.configure("Panel.TFrame", background="#ffffff")
|
|
100
|
+
style.configure(
|
|
101
|
+
"Title.TLabel",
|
|
102
|
+
background="#f3f6fb",
|
|
103
|
+
foreground="#14213d",
|
|
104
|
+
font=("Segoe UI", 22, "bold"),
|
|
105
|
+
)
|
|
106
|
+
style.configure(
|
|
107
|
+
"Subtitle.TLabel",
|
|
108
|
+
background="#f3f6fb",
|
|
109
|
+
foreground="#65748b",
|
|
110
|
+
font=("Segoe UI", 10),
|
|
111
|
+
)
|
|
112
|
+
style.configure(
|
|
113
|
+
"PanelTitle.TLabel",
|
|
114
|
+
background="#ffffff",
|
|
115
|
+
foreground="#14213d",
|
|
116
|
+
font=("Segoe UI", 11, "bold"),
|
|
117
|
+
)
|
|
118
|
+
style.configure(
|
|
119
|
+
"MetricName.TLabel",
|
|
120
|
+
background="#ffffff",
|
|
121
|
+
foreground="#65748b",
|
|
122
|
+
font=("Segoe UI", 9),
|
|
123
|
+
)
|
|
124
|
+
style.configure(
|
|
125
|
+
"MetricValue.TLabel",
|
|
126
|
+
background="#ffffff",
|
|
127
|
+
foreground="#14213d",
|
|
128
|
+
font=("Segoe UI", 16, "bold"),
|
|
129
|
+
)
|
|
130
|
+
style.configure(
|
|
131
|
+
"HighlightValue.TLabel",
|
|
132
|
+
background="#ffffff",
|
|
133
|
+
foreground="#3157d5",
|
|
134
|
+
font=("Segoe UI", 18, "bold"),
|
|
135
|
+
)
|
|
136
|
+
style.configure("TButton", font=("Segoe UI", 9), padding=(10, 6))
|
|
137
|
+
style.configure("TScale", background="#ffffff")
|
|
138
|
+
|
|
139
|
+
def _build(self) -> None:
|
|
140
|
+
outer = ttk.Frame(self.root, padding=(24, 20))
|
|
141
|
+
outer.pack(fill="both", expand=True)
|
|
142
|
+
ttk.Label(outer, text="Mumo Interactive Calculator", style="Title.TLabel").pack(
|
|
143
|
+
anchor="w"
|
|
144
|
+
)
|
|
145
|
+
ttk.Label(
|
|
146
|
+
outer,
|
|
147
|
+
text="Explore how your dataset and model parameters shape each index.",
|
|
148
|
+
style="Subtitle.TLabel",
|
|
149
|
+
).pack(anchor="w", pady=(4, 18))
|
|
150
|
+
|
|
151
|
+
columns = ttk.Frame(outer)
|
|
152
|
+
columns.pack(fill="x")
|
|
153
|
+
columns.columnconfigure(0, weight=3)
|
|
154
|
+
columns.columnconfigure(1, weight=2)
|
|
155
|
+
|
|
156
|
+
data_panel = ttk.Frame(columns, style="Panel.TFrame", padding=16)
|
|
157
|
+
data_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
|
|
158
|
+
ttk.Label(data_panel, text="Dataset", style="PanelTitle.TLabel").pack(
|
|
159
|
+
anchor="w"
|
|
160
|
+
)
|
|
161
|
+
ttk.Label(
|
|
162
|
+
data_panel,
|
|
163
|
+
text="Paste or type numbers, separated by commas, spaces, or new lines.",
|
|
164
|
+
style="MetricName.TLabel",
|
|
165
|
+
).pack(anchor="w", pady=(3, 10))
|
|
166
|
+
text_frame = ttk.Frame(data_panel, style="Panel.TFrame")
|
|
167
|
+
text_frame.pack(fill="both", expand=True)
|
|
168
|
+
self.dataset = tk.Text(
|
|
169
|
+
text_frame,
|
|
170
|
+
height=5,
|
|
171
|
+
wrap="word",
|
|
172
|
+
undo=True,
|
|
173
|
+
font=("Consolas", 10),
|
|
174
|
+
relief="solid",
|
|
175
|
+
borderwidth=1,
|
|
176
|
+
padx=9,
|
|
177
|
+
pady=8,
|
|
178
|
+
)
|
|
179
|
+
scrollbar = ttk.Scrollbar(
|
|
180
|
+
text_frame, orient="vertical", command=self.dataset.yview
|
|
181
|
+
)
|
|
182
|
+
self.dataset.configure(yscrollcommand=scrollbar.set)
|
|
183
|
+
self.dataset.pack(side="left", fill="both", expand=True)
|
|
184
|
+
scrollbar.pack(side="right", fill="y")
|
|
185
|
+
self.dataset.bind("<<Modified>>", self._on_dataset_change)
|
|
186
|
+
|
|
187
|
+
data_actions = ttk.Frame(data_panel, style="Panel.TFrame")
|
|
188
|
+
data_actions.pack(fill="x", pady=(9, 0))
|
|
189
|
+
ttk.Button(data_actions, text="Load sample", command=self._set_sample).pack(
|
|
190
|
+
side="left"
|
|
191
|
+
)
|
|
192
|
+
ttk.Button(data_actions, text="Clear", command=self._clear_dataset).pack(
|
|
193
|
+
side="left", padx=(7, 0)
|
|
194
|
+
)
|
|
195
|
+
ttk.Label(data_actions, textvariable=self.count, style="MetricName.TLabel").pack(
|
|
196
|
+
side="right", pady=6
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
parameters = ttk.Frame(columns, style="Panel.TFrame", padding=16)
|
|
200
|
+
parameters.grid(row=0, column=1, sticky="nsew")
|
|
201
|
+
ttk.Label(parameters, text="Parameters", style="PanelTitle.TLabel").pack(
|
|
202
|
+
anchor="w", pady=(0, 5)
|
|
203
|
+
)
|
|
204
|
+
self._add_parameter(
|
|
205
|
+
parameters, "Core / boundary size (k)", self.k, 1, 4, "{:d}",
|
|
206
|
+
integer=True,
|
|
207
|
+
)
|
|
208
|
+
self._add_parameter(parameters, "Alpha", self.alpha, 0, 1, "{:.2f}")
|
|
209
|
+
self._add_parameter(parameters, "Beta", self.beta, 0, 1, "{:.2f}")
|
|
210
|
+
self._add_parameter(parameters, "Gamma", self.gamma, 0, 1, "{:.2f}")
|
|
211
|
+
|
|
212
|
+
results_panel = ttk.Frame(outer, style="Panel.TFrame", padding=16)
|
|
213
|
+
results_panel.pack(fill="both", expand=True, pady=(16, 0))
|
|
214
|
+
ttk.Label(results_panel, text="Calculated indices", style="PanelTitle.TLabel").pack(
|
|
215
|
+
anchor="w", pady=(0, 12)
|
|
216
|
+
)
|
|
217
|
+
metrics = ttk.Frame(results_panel, style="Panel.TFrame")
|
|
218
|
+
metrics.pack(fill="both", expand=True)
|
|
219
|
+
metric_names = [
|
|
220
|
+
"MPU", "MPD", "MA", "MBTS", "MPU-k", "MPD-k", "P-MPU",
|
|
221
|
+
"P-MPD", "P-GMA", "MBV", "P-MBTS", "P-GMCSI", "MAEI", "MCHI",
|
|
222
|
+
]
|
|
223
|
+
for column in range(3):
|
|
224
|
+
metrics.columnconfigure(column, weight=1)
|
|
225
|
+
for index, name in enumerate(metric_names):
|
|
226
|
+
row, column = divmod(index, 3)
|
|
227
|
+
card = ttk.Frame(metrics, style="Panel.TFrame", padding=(8, 7))
|
|
228
|
+
card.grid(row=row, column=column, sticky="nsew", padx=4, pady=3)
|
|
229
|
+
ttk.Label(card, text=name, style="MetricName.TLabel").pack(anchor="w")
|
|
230
|
+
value = tk.StringVar(value="—")
|
|
231
|
+
self._metric_values[name] = value
|
|
232
|
+
value_style = (
|
|
233
|
+
"HighlightValue.TLabel" if name in ("MAEI", "MCHI") else "MetricValue.TLabel"
|
|
234
|
+
)
|
|
235
|
+
ttk.Label(card, textvariable=value, style=value_style).pack(anchor="w")
|
|
236
|
+
|
|
237
|
+
self.status_label = ttk.Label(
|
|
238
|
+
outer,
|
|
239
|
+
textvariable=self.status,
|
|
240
|
+
style="Subtitle.TLabel",
|
|
241
|
+
wraplength=880,
|
|
242
|
+
)
|
|
243
|
+
self.status_label.pack(anchor="w", pady=(11, 0))
|
|
244
|
+
|
|
245
|
+
def _add_parameter(
|
|
246
|
+
self,
|
|
247
|
+
parent: ttk.Frame,
|
|
248
|
+
label: str,
|
|
249
|
+
variable: tk.Variable,
|
|
250
|
+
minimum: int,
|
|
251
|
+
maximum: int,
|
|
252
|
+
number_format: str,
|
|
253
|
+
integer: bool = False,
|
|
254
|
+
) -> None:
|
|
255
|
+
row = ttk.Frame(parent, style="Panel.TFrame")
|
|
256
|
+
row.pack(fill="x", pady=5)
|
|
257
|
+
heading = ttk.Frame(row, style="Panel.TFrame")
|
|
258
|
+
heading.pack(fill="x")
|
|
259
|
+
ttk.Label(heading, text=label, style="MetricName.TLabel").pack(side="left")
|
|
260
|
+
value_label = ttk.Label(heading, style="PanelTitle.TLabel")
|
|
261
|
+
value_label.pack(side="right")
|
|
262
|
+
|
|
263
|
+
def update_label(*_args: object) -> None:
|
|
264
|
+
value_label.configure(text=number_format.format(variable.get()))
|
|
265
|
+
|
|
266
|
+
variable.trace_add("write", update_label)
|
|
267
|
+
update_label()
|
|
268
|
+
if integer:
|
|
269
|
+
scale = tk.Scale(
|
|
270
|
+
row,
|
|
271
|
+
from_=minimum,
|
|
272
|
+
to=maximum,
|
|
273
|
+
resolution=1,
|
|
274
|
+
showvalue=False,
|
|
275
|
+
variable=variable,
|
|
276
|
+
command=self._on_parameter_change,
|
|
277
|
+
orient="horizontal",
|
|
278
|
+
highlightthickness=0,
|
|
279
|
+
borderwidth=0,
|
|
280
|
+
background="#ffffff",
|
|
281
|
+
troughcolor="#e8edf5",
|
|
282
|
+
activebackground="#3157d5",
|
|
283
|
+
)
|
|
284
|
+
else:
|
|
285
|
+
scale = ttk.Scale(
|
|
286
|
+
row,
|
|
287
|
+
from_=minimum,
|
|
288
|
+
to=maximum,
|
|
289
|
+
variable=variable,
|
|
290
|
+
command=self._on_parameter_change,
|
|
291
|
+
)
|
|
292
|
+
scale.pack(fill="x", pady=(2, 0))
|
|
293
|
+
if integer:
|
|
294
|
+
self.k_scale = scale
|
|
295
|
+
|
|
296
|
+
def _on_dataset_change(self, _event: tk.Event) -> None:
|
|
297
|
+
if not self.dataset.edit_modified():
|
|
298
|
+
return
|
|
299
|
+
self.dataset.edit_modified(False)
|
|
300
|
+
if self._pending_update is not None:
|
|
301
|
+
self.root.after_cancel(self._pending_update)
|
|
302
|
+
self._pending_update = self.root.after(250, self._recalculate)
|
|
303
|
+
|
|
304
|
+
def _on_parameter_change(self, _value: str) -> None:
|
|
305
|
+
self._recalculate()
|
|
306
|
+
|
|
307
|
+
def _set_sample(self) -> None:
|
|
308
|
+
self.dataset.delete("1.0", "end")
|
|
309
|
+
self.dataset.insert("1.0", _SAMPLE_DATA)
|
|
310
|
+
self.dataset.edit_modified(False)
|
|
311
|
+
self._recalculate()
|
|
312
|
+
|
|
313
|
+
def _clear_dataset(self) -> None:
|
|
314
|
+
self.dataset.delete("1.0", "end")
|
|
315
|
+
self.dataset.edit_modified(False)
|
|
316
|
+
self._recalculate()
|
|
317
|
+
|
|
318
|
+
def _recalculate(self) -> None:
|
|
319
|
+
self._pending_update = None
|
|
320
|
+
try:
|
|
321
|
+
data = _parse_dataset(self.dataset.get("1.0", "end-1c"))
|
|
322
|
+
except ValueError as exc:
|
|
323
|
+
self.count.set("— values")
|
|
324
|
+
self.status.set(str(exc))
|
|
325
|
+
self.status_label.configure(foreground="#b42318")
|
|
326
|
+
for value in self._metric_values.values():
|
|
327
|
+
value.set("—")
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
maximum_k = (len(data) - 1) // 2
|
|
331
|
+
self.k_scale.configure(from_=1, to=maximum_k)
|
|
332
|
+
if self.k.get() < 1 or self.k.get() > maximum_k:
|
|
333
|
+
self.k.set(min(maximum_k, max(1, self.k.get())))
|
|
334
|
+
self.count.set("{} values".format(len(data)))
|
|
335
|
+
|
|
336
|
+
try:
|
|
337
|
+
results = _calculate_results(
|
|
338
|
+
data, self.k.get(), self.alpha.get(), self.beta.get(), self.gamma.get()
|
|
339
|
+
)
|
|
340
|
+
except (TypeError, ValueError) as exc:
|
|
341
|
+
self.status.set("Could not calculate indices: {}".format(exc))
|
|
342
|
+
self.status_label.configure(foreground="#b42318")
|
|
343
|
+
for value in self._metric_values.values():
|
|
344
|
+
value.set("—")
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
for name, result in results.items():
|
|
348
|
+
self._metric_values[name].set("{:.6g}".format(result))
|
|
349
|
+
self.status.set(
|
|
350
|
+
"Sorted values are used for boundary calculations. "
|
|
351
|
+
"P-MBTS uses the mean after removing the lowest k and highest k values."
|
|
352
|
+
)
|
|
353
|
+
self.status_label.configure(foreground="#65748b")
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def main() -> int:
|
|
357
|
+
"""Launch the interactive desktop calculator."""
|
|
358
|
+
root = tk.Tk()
|
|
359
|
+
MumoCalculator(root)
|
|
360
|
+
root.mainloop()
|
|
361
|
+
return 0
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
raise SystemExit(main())
|