pydasa 0.4.7__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.
- pydasa/__init__.py +103 -0
- pydasa/_version.py +6 -0
- pydasa/analysis/__init__.py +0 -0
- pydasa/analysis/scenario.py +584 -0
- pydasa/analysis/simulation.py +1158 -0
- pydasa/context/__init__.py +0 -0
- pydasa/context/conversion.py +11 -0
- pydasa/context/system.py +17 -0
- pydasa/context/units.py +15 -0
- pydasa/core/__init__.py +15 -0
- pydasa/core/basic.py +287 -0
- pydasa/core/cfg/default.json +136 -0
- pydasa/core/constants.py +27 -0
- pydasa/core/io.py +102 -0
- pydasa/core/setup.py +269 -0
- pydasa/dimensional/__init__.py +0 -0
- pydasa/dimensional/buckingham.py +728 -0
- pydasa/dimensional/fundamental.py +146 -0
- pydasa/dimensional/model.py +1077 -0
- pydasa/dimensional/vaschy.py +633 -0
- pydasa/elements/__init__.py +19 -0
- pydasa/elements/parameter.py +218 -0
- pydasa/elements/specs/__init__.py +22 -0
- pydasa/elements/specs/conceptual.py +161 -0
- pydasa/elements/specs/numerical.py +469 -0
- pydasa/elements/specs/statistical.py +229 -0
- pydasa/elements/specs/symbolic.py +394 -0
- pydasa/serialization/__init__.py +27 -0
- pydasa/serialization/parser.py +133 -0
- pydasa/structs/__init__.py +0 -0
- pydasa/structs/lists/__init__.py +0 -0
- pydasa/structs/lists/arlt.py +578 -0
- pydasa/structs/lists/dllt.py +18 -0
- pydasa/structs/lists/ndlt.py +262 -0
- pydasa/structs/lists/sllt.py +746 -0
- pydasa/structs/tables/__init__.py +0 -0
- pydasa/structs/tables/htme.py +182 -0
- pydasa/structs/tables/scht.py +774 -0
- pydasa/structs/tools/__init__.py +0 -0
- pydasa/structs/tools/hashing.py +53 -0
- pydasa/structs/tools/math.py +149 -0
- pydasa/structs/tools/memory.py +54 -0
- pydasa/structs/types/__init__.py +0 -0
- pydasa/structs/types/functions.py +131 -0
- pydasa/structs/types/generics.py +54 -0
- pydasa/validations/__init__.py +0 -0
- pydasa/validations/decorators.py +510 -0
- pydasa/validations/error.py +100 -0
- pydasa/validations/patterns.py +32 -0
- pydasa/workflows/__init__.py +1 -0
- pydasa/workflows/influence.py +497 -0
- pydasa/workflows/phenomena.py +529 -0
- pydasa/workflows/practical.py +765 -0
- pydasa-0.4.7.dist-info/METADATA +320 -0
- pydasa-0.4.7.dist-info/RECORD +58 -0
- pydasa-0.4.7.dist-info/WHEEL +5 -0
- pydasa-0.4.7.dist-info/licenses/LICENSE +674 -0
- pydasa-0.4.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1158 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Module simulation.py
|
|
4
|
+
===========================================
|
|
5
|
+
|
|
6
|
+
Module for Monte Carlo Simulation execution and analysis in *PyDASA*.
|
|
7
|
+
|
|
8
|
+
This module provides the MonteCarlo class for performing Monte Carlo simulations on dimensionless coefficients derived from dimensional analysis.
|
|
9
|
+
|
|
10
|
+
Classes:
|
|
11
|
+
|
|
12
|
+
**MonteCarlo**: Performs Monte Carlo simulations on dimensionless coefficients.
|
|
13
|
+
|
|
14
|
+
*IMPORTANT:* Based on the theory from:
|
|
15
|
+
|
|
16
|
+
# H.Gorter, *Dimensionalanalyse: Eine Theoririe der physikalischen Dimensionen mit Anwendungen*
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from typing import Optional, List, Dict, Any, Callable, Tuple, Union
|
|
22
|
+
|
|
23
|
+
# python third-party modules
|
|
24
|
+
import numpy as np
|
|
25
|
+
from numpy.typing import NDArray
|
|
26
|
+
from sympy import lambdify
|
|
27
|
+
# from sympy import Expr, Symbol
|
|
28
|
+
from scipy import stats
|
|
29
|
+
import sympy as sp
|
|
30
|
+
|
|
31
|
+
# Import validation base classes
|
|
32
|
+
from pydasa.core.basic import Foundation
|
|
33
|
+
|
|
34
|
+
# Import validation decorators
|
|
35
|
+
from pydasa.validations.decorators import validate_range, validate_type, validate_custom
|
|
36
|
+
|
|
37
|
+
# Import related classes
|
|
38
|
+
from pydasa.dimensional.buckingham import Coefficient
|
|
39
|
+
from pydasa.elements.parameter import Variable
|
|
40
|
+
|
|
41
|
+
# Import utils
|
|
42
|
+
from pydasa.serialization.parser import parse_latex, create_latex_mapping
|
|
43
|
+
|
|
44
|
+
# Import configuration
|
|
45
|
+
from pydasa.serialization.parser import latex_to_python
|
|
46
|
+
|
|
47
|
+
# # Type aliases
|
|
48
|
+
# SymbolDict = Dict[str, sp.Symbol]
|
|
49
|
+
# # FIX: Allow Basic or Expr since subs() returns Basic
|
|
50
|
+
# SymExpr = Union[sp.Expr, sp.Basic]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class MonteCarlo(Foundation):
|
|
55
|
+
"""**MonteCarlo** class for stochastic analysis in *PyDASA*.
|
|
56
|
+
|
|
57
|
+
Performs Monte Carlo simulations on dimensionless coefficients to analyze the coefficient's distribution and sensitivity to input parameter
|
|
58
|
+
variations.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
Foundation: Foundation class for validation of symbols and frameworks.
|
|
62
|
+
|
|
63
|
+
Attributes:
|
|
64
|
+
# Core Identification
|
|
65
|
+
name (str): User-friendly name of the Monte Carlo simulation.
|
|
66
|
+
description (str): Brief summary of the simulation.
|
|
67
|
+
_idx (int): Index/precedence of the simulation.
|
|
68
|
+
_sym (str): Symbol representation (LaTeX or alphanumeric).
|
|
69
|
+
_alias (str): Python-compatible alias for use in code.
|
|
70
|
+
_fwk (str): Frameworks context (PHYSICAL, COMPUTATION, SOFTWARE, CUSTOM).
|
|
71
|
+
_cat (str): Category of analysis (SYM, NUM, HYB).
|
|
72
|
+
|
|
73
|
+
# Coefficient and Expression Management
|
|
74
|
+
_coefficient (Optional[Coefficient]): Coefficient for the simulation.
|
|
75
|
+
_pi_expr (str): LaTeX expression to analyze.
|
|
76
|
+
_sym_func (Callable): Sympy function of the simulation.
|
|
77
|
+
_exe_func (Callable): Executable function for numerical evaluation.
|
|
78
|
+
|
|
79
|
+
# Variable Management
|
|
80
|
+
_variables (Dict[str, Variable]): Variable symbols in the expression.
|
|
81
|
+
_symbols (Dict[str, Any]): Python symbols for the variables.
|
|
82
|
+
_aliases (Dict[str, Any]): Variable aliases for use in code.
|
|
83
|
+
_latex_to_py (Dict[str, str]): Mapping from LaTeX to Python variable names.
|
|
84
|
+
_py_to_latex (Dict[str, str]): Mapping from Python to LaTeX variable names.
|
|
85
|
+
|
|
86
|
+
# Simulation Configuration
|
|
87
|
+
_experiments (int): Number of simulation experiments to run. Default is -1.
|
|
88
|
+
_distributions (Dict[str, Dict[str, Any]]): Variable sampling distributions.
|
|
89
|
+
_simul_cache (Dict[str, NDArray[np.float64]]): Working sampled values cache.
|
|
90
|
+
|
|
91
|
+
# Results and Inputs
|
|
92
|
+
inputs (Optional[np.ndarray]): Variable simulated inputs.
|
|
93
|
+
_results (Optional[np.ndarray]): Raw simulation results.
|
|
94
|
+
|
|
95
|
+
# Statistics
|
|
96
|
+
_mean (float): Mean value of simulation results.
|
|
97
|
+
_median (float): Median value of simulation results.
|
|
98
|
+
_std_dev (float): Standard deviation of simulation results.
|
|
99
|
+
_variance (float): Variance of simulation results.
|
|
100
|
+
_min (float): Minimum value in simulation results.
|
|
101
|
+
_max (float): Maximum value in simulation results.
|
|
102
|
+
_count (int): Number of valid simulation results.
|
|
103
|
+
_statistics (Optional[Dict[str, float]]): Statistical summary.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
# ========================================================================
|
|
107
|
+
# Core Identification
|
|
108
|
+
# ========================================================================
|
|
109
|
+
|
|
110
|
+
# :attr: name
|
|
111
|
+
_name: str = ""
|
|
112
|
+
"""User-friendly name of the Monte Carlo simulation."""
|
|
113
|
+
|
|
114
|
+
# :attr: description
|
|
115
|
+
description: str = ""
|
|
116
|
+
"""Brief summary of the simulation."""
|
|
117
|
+
|
|
118
|
+
# :attr: _idx
|
|
119
|
+
_idx: int = -1
|
|
120
|
+
"""Index/precedence of the simulation."""
|
|
121
|
+
|
|
122
|
+
# :attr: _sym
|
|
123
|
+
_sym: str = ""
|
|
124
|
+
"""Symbol representation (LaTeX or alphanumeric)."""
|
|
125
|
+
|
|
126
|
+
# :attr: _alias
|
|
127
|
+
_alias: str = ""
|
|
128
|
+
"""Python-compatible alias for use in code."""
|
|
129
|
+
|
|
130
|
+
# :attr: _fwk
|
|
131
|
+
_fwk: str = "PHYSICAL"
|
|
132
|
+
"""Frameworks context (PHYSICAL, COMPUTATION, SOFTWARE, CUSTOM)."""
|
|
133
|
+
|
|
134
|
+
# :attr: _cat
|
|
135
|
+
_cat: str = "NUM"
|
|
136
|
+
"""Category of analysis (SYM, NUM, HYB)."""
|
|
137
|
+
|
|
138
|
+
# ========================================================================
|
|
139
|
+
# Coefficient and Expression Management
|
|
140
|
+
# ========================================================================
|
|
141
|
+
|
|
142
|
+
# :attr: _coefficient
|
|
143
|
+
_coefficient: Coefficient = field(default_factory=Coefficient)
|
|
144
|
+
"""Coefficient for the simulation."""
|
|
145
|
+
|
|
146
|
+
# :attr: _pi_expr
|
|
147
|
+
_pi_expr: Optional[str] = None
|
|
148
|
+
"""LaTeX expression to analyze."""
|
|
149
|
+
|
|
150
|
+
# :attr: _sym_func
|
|
151
|
+
_sym_func: Optional[Union[sp.Expr, sp.Basic]] = None
|
|
152
|
+
"""Sympy expression object for the coefficient (Mul, Add, Pow, Symbol, etc.)."""
|
|
153
|
+
|
|
154
|
+
# :attr: _exe_func
|
|
155
|
+
_exe_func: Optional[Callable[..., Union[float, np.ndarray]]] = None
|
|
156
|
+
"""Compiled executable function for evaluation of the coefficient."""
|
|
157
|
+
|
|
158
|
+
# ========================================================================
|
|
159
|
+
# Variable Management
|
|
160
|
+
# ========================================================================
|
|
161
|
+
|
|
162
|
+
# :attr: _variables
|
|
163
|
+
_variables: Dict[str, Variable] = field(default_factory=dict)
|
|
164
|
+
"""Dictionary of variables in the expression."""
|
|
165
|
+
|
|
166
|
+
# :attr: _symbols
|
|
167
|
+
_symbols: Dict[str, sp.Symbol] = field(default_factory=dict)
|
|
168
|
+
"""Map from variable names (strings) to sympy Symbols."""
|
|
169
|
+
|
|
170
|
+
# :attr: _aliases
|
|
171
|
+
_aliases: Dict[str, sp.Symbol] = field(default_factory=dict)
|
|
172
|
+
"""Map from Variable aliases to sympy Symbols."""
|
|
173
|
+
|
|
174
|
+
# :attr: _latex_to_py
|
|
175
|
+
_latex_to_py: Dict[str, str] = field(default_factory=dict)
|
|
176
|
+
"""Map from LaTeX symbols to Python-compatible names."""
|
|
177
|
+
|
|
178
|
+
# :attr: _py_to_latex
|
|
179
|
+
_py_to_latex: Dict[str, str] = field(default_factory=dict)
|
|
180
|
+
"""Map from Python-compatible names to LaTeX symbols."""
|
|
181
|
+
|
|
182
|
+
# :attr: _var_symbols
|
|
183
|
+
_var_symbols: List[str] = field(default_factory=list)
|
|
184
|
+
"""List of variable names extracted from expression."""
|
|
185
|
+
# ========================================================================
|
|
186
|
+
# Simulation Configuration
|
|
187
|
+
# ========================================================================
|
|
188
|
+
|
|
189
|
+
# :attr: _experiments
|
|
190
|
+
_experiments: int = -1
|
|
191
|
+
"""Number of simulation iterations to run."""
|
|
192
|
+
|
|
193
|
+
# :attr: _distributions
|
|
194
|
+
_distributions: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
195
|
+
"""Variable sampling distributions and specifications that includes:
|
|
196
|
+
- 'dtype': Distribution type name.
|
|
197
|
+
- 'params': Distribution parameters (mean, std_dev, etc.).
|
|
198
|
+
- 'func': Function for sampling, usually in Lambda format.
|
|
199
|
+
- 'depends': List of variables this variable depends on.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
# :attr: _dependencies
|
|
203
|
+
_dependencies: Dict[str, List[str]] = field(default_factory=dict, init=False)
|
|
204
|
+
"""Variable dependencies for simulations."""
|
|
205
|
+
|
|
206
|
+
# :attr: _simul_cache
|
|
207
|
+
_simul_cache: Dict[str, NDArray[np.float64]] = field(default_factory=dict)
|
|
208
|
+
"""Working sampled values during each simulation iteration. Memory cache."""
|
|
209
|
+
|
|
210
|
+
# ========================================================================
|
|
211
|
+
# Results and Inputs
|
|
212
|
+
# ========================================================================
|
|
213
|
+
|
|
214
|
+
# :attr: inputs
|
|
215
|
+
inputs: NDArray[np.float64] = field(
|
|
216
|
+
default_factory=lambda: np.array([], dtype=np.float64))
|
|
217
|
+
"""Sample value range for the simulation."""
|
|
218
|
+
|
|
219
|
+
# :attr: _results
|
|
220
|
+
_results: NDArray[np.float64] = field(
|
|
221
|
+
default_factory=lambda: np.array([], dtype=np.float64))
|
|
222
|
+
"""Raw simulation results."""
|
|
223
|
+
|
|
224
|
+
# ========================================================================
|
|
225
|
+
# Statistics
|
|
226
|
+
# ========================================================================
|
|
227
|
+
|
|
228
|
+
# :attr: _mean
|
|
229
|
+
_mean: float = np.nan
|
|
230
|
+
"""Mean value of simulation results."""
|
|
231
|
+
|
|
232
|
+
# :attr: _median
|
|
233
|
+
_median: float = np.nan
|
|
234
|
+
"""Median value of simulation results."""
|
|
235
|
+
|
|
236
|
+
# :attr: _std_dev
|
|
237
|
+
_std_dev: float = np.nan
|
|
238
|
+
"""Standard deviation of simulation results."""
|
|
239
|
+
|
|
240
|
+
# :attr: _variance
|
|
241
|
+
_variance: float = np.nan
|
|
242
|
+
"""Variance of simulation results."""
|
|
243
|
+
|
|
244
|
+
# :attr: _min
|
|
245
|
+
_min: float = np.nan
|
|
246
|
+
"""Minimum value in simulation results."""
|
|
247
|
+
|
|
248
|
+
# :attr: _max
|
|
249
|
+
_max: float = np.nan
|
|
250
|
+
"""Maximum value in simulation results."""
|
|
251
|
+
|
|
252
|
+
# :attr: _count
|
|
253
|
+
_count: int = 0
|
|
254
|
+
"""Number of valid simulation results."""
|
|
255
|
+
|
|
256
|
+
# :attr: _statistics
|
|
257
|
+
_statistics: Optional[Dict[str, float]] = None
|
|
258
|
+
"""Statistical summary of the Monte Carlo simulation results."""
|
|
259
|
+
|
|
260
|
+
# ========================================================================
|
|
261
|
+
# Initialization
|
|
262
|
+
# ========================================================================
|
|
263
|
+
|
|
264
|
+
def _validate_dist(self, value: Dict[str, Dict[str, Any]], field_name: str) -> None:
|
|
265
|
+
"""*_validate_dist()* Custom validator to ensure all distributions have callable 'func'.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
value: The distributions dictionary to validate.
|
|
269
|
+
field_name: Name of the field being validated.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If distributions don't have callable 'func' functions.
|
|
273
|
+
"""
|
|
274
|
+
if not all(callable(v["func"]) for v in value.values()):
|
|
275
|
+
inv = [k for k, v in value.items() if not callable(v["func"])]
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"All distributions must have callable 'func' functions. "
|
|
278
|
+
f"Invalid entries: {inv}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
def __post_init__(self) -> None:
|
|
282
|
+
"""*__post_init__()* Initializes the Monte Carlo simulation."""
|
|
283
|
+
# Initialize from base class
|
|
284
|
+
super().__post_init__()
|
|
285
|
+
|
|
286
|
+
# Validate coefficient
|
|
287
|
+
if not self._coefficient.pi_expr:
|
|
288
|
+
raise ValueError("Coefficient must have a valid expression")
|
|
289
|
+
|
|
290
|
+
# Derive expression from coefficient
|
|
291
|
+
self._pi_expr = self._coefficient.pi_expr
|
|
292
|
+
|
|
293
|
+
# Set default symbol if not specified
|
|
294
|
+
if not self._sym:
|
|
295
|
+
self._sym = f"MC_\\Pi_{{{self._idx}}}" if self._idx >= 0 else "MC_\\Pi_{}"
|
|
296
|
+
|
|
297
|
+
# Set default Python alias if not specified
|
|
298
|
+
if not self._alias:
|
|
299
|
+
self._alias = latex_to_python(self._sym)
|
|
300
|
+
|
|
301
|
+
# Set name and description if not already set
|
|
302
|
+
if not self._name:
|
|
303
|
+
self._name = f"{self._sym} Monte Carlo"
|
|
304
|
+
|
|
305
|
+
if not self.description:
|
|
306
|
+
self.description = f"Monte Carlo simulation for {self._sym}"
|
|
307
|
+
|
|
308
|
+
if self._pi_expr:
|
|
309
|
+
# Parse the expression
|
|
310
|
+
self._parse_expression(self._pi_expr)
|
|
311
|
+
|
|
312
|
+
# Preallocate full array space with NaN only if experiments > 0
|
|
313
|
+
n_sym = len(self._symbols)
|
|
314
|
+
if n_sym > 0 and self._experiments > 0:
|
|
315
|
+
# Only allocate if we have variables and valid experiment count
|
|
316
|
+
if self.inputs.size == 0: # Check size, not None
|
|
317
|
+
self.inputs = np.full((self._experiments, n_sym),
|
|
318
|
+
np.nan,
|
|
319
|
+
dtype=np.float64)
|
|
320
|
+
if self._results.size == 0: # Check size, not None
|
|
321
|
+
self._results = np.full((self._experiments, 1),
|
|
322
|
+
np.nan,
|
|
323
|
+
dtype=np.float64)
|
|
324
|
+
|
|
325
|
+
# Only initialize cache if not already provided and experiments > 0
|
|
326
|
+
if not self._simul_cache and self._experiments > 0:
|
|
327
|
+
# Create local cache only if no external cache provided
|
|
328
|
+
for var in self._variables.keys():
|
|
329
|
+
self._simul_cache[var] = np.full((self._experiments, 1),
|
|
330
|
+
np.nan,
|
|
331
|
+
dtype=np.float64)
|
|
332
|
+
|
|
333
|
+
# Statistics initialized to NaN (not calculated yet)
|
|
334
|
+
self._mean = np.nan
|
|
335
|
+
self._median = np.nan
|
|
336
|
+
self._std_dev = np.nan
|
|
337
|
+
self._variance = np.nan
|
|
338
|
+
self._min = np.nan
|
|
339
|
+
self._max = np.nan
|
|
340
|
+
|
|
341
|
+
# Zero makes sense here
|
|
342
|
+
self._count = 0
|
|
343
|
+
|
|
344
|
+
# ========================================================================
|
|
345
|
+
# Foundation and Configuration
|
|
346
|
+
# ========================================================================
|
|
347
|
+
|
|
348
|
+
def _validate_readiness(self) -> None:
|
|
349
|
+
"""*_validate_readiness()* Checks if the simulation can be performed.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
ValueError: If the simulation is not ready due to missing variables, executable function, distributions, or invalid number of iterations.
|
|
353
|
+
"""
|
|
354
|
+
if not self._variables:
|
|
355
|
+
raise ValueError("No variables found in the expression.")
|
|
356
|
+
if not self._sym_func:
|
|
357
|
+
raise ValueError("No expression has been defined for analysis.")
|
|
358
|
+
if not self._distributions:
|
|
359
|
+
_vars = self._variables
|
|
360
|
+
missing = [v for v in _vars if v not in self._distributions]
|
|
361
|
+
if missing:
|
|
362
|
+
_msg = f"Missing distributions for variables: {missing}"
|
|
363
|
+
raise ValueError(_msg)
|
|
364
|
+
if self._experiments < 1:
|
|
365
|
+
_msg = f"Invalid number of iterations: {self._experiments}"
|
|
366
|
+
raise ValueError(_msg)
|
|
367
|
+
|
|
368
|
+
def set_coefficient(self, coef: Coefficient) -> None:
|
|
369
|
+
"""*set_coefficient()* Configure analysis from a coefficient.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
coef (Coefficient): Dimensionless coefficient to analyze.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
ValueError: If the coefficient doesn't have a valid expression.
|
|
376
|
+
"""
|
|
377
|
+
if not coef.pi_expr:
|
|
378
|
+
raise ValueError("Coefficient does not have a valid expression.")
|
|
379
|
+
|
|
380
|
+
# Save coefficient
|
|
381
|
+
self._coefficient = coef
|
|
382
|
+
|
|
383
|
+
# Set expression
|
|
384
|
+
self._pi_expr = coef.pi_expr
|
|
385
|
+
|
|
386
|
+
# Parse coefficient expression
|
|
387
|
+
if coef._pi_expr:
|
|
388
|
+
self._parse_expression(self._pi_expr)
|
|
389
|
+
|
|
390
|
+
# Set name and description if not already set
|
|
391
|
+
if not self._name:
|
|
392
|
+
self._name = f"{coef.name} Monte Carlo Experiments"
|
|
393
|
+
if not self.description:
|
|
394
|
+
self.description = f"Monte Carlo simulation for {coef.name}"
|
|
395
|
+
|
|
396
|
+
def _parse_expression(self, expr: str) -> None:
|
|
397
|
+
"""*_parse_expression()* Parse the LaTeX expression into a sympy function.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
expr (str): LaTeX expression to parse.
|
|
401
|
+
|
|
402
|
+
Raises:
|
|
403
|
+
ValueError: If the expression cannot be parsed.
|
|
404
|
+
"""
|
|
405
|
+
try:
|
|
406
|
+
# Parse the expression
|
|
407
|
+
self._sym_func = parse_latex(expr)
|
|
408
|
+
|
|
409
|
+
if self._sym_func is None:
|
|
410
|
+
raise ValueError("Parsing returned None")
|
|
411
|
+
|
|
412
|
+
# Store the sympy expression
|
|
413
|
+
self._sym_func = self._sym_func
|
|
414
|
+
|
|
415
|
+
# Create symbol mapping
|
|
416
|
+
maps = create_latex_mapping(expr)
|
|
417
|
+
|
|
418
|
+
symbols_raw: Dict[Any, sp.Symbol] = maps[0]
|
|
419
|
+
aliases_raw: Dict[str, sp.Symbol] = maps[1]
|
|
420
|
+
latex_to_py: Dict[str, str] = maps[2]
|
|
421
|
+
py_to_latex: Dict[str, str] = maps[3]
|
|
422
|
+
|
|
423
|
+
# Convert Symbol keys to strings
|
|
424
|
+
self._symbols = {
|
|
425
|
+
str(k): v for k, v in symbols_raw.items()
|
|
426
|
+
}
|
|
427
|
+
self._aliases = aliases_raw
|
|
428
|
+
self._latex_to_py = latex_to_py
|
|
429
|
+
self._py_to_latex = py_to_latex
|
|
430
|
+
|
|
431
|
+
# Substitute LaTeX symbols with Python symbols
|
|
432
|
+
for latex_sym_key, py_sym in symbols_raw.items():
|
|
433
|
+
if self._sym_func is None:
|
|
434
|
+
break
|
|
435
|
+
|
|
436
|
+
# Handle both string and Symbol keys
|
|
437
|
+
if isinstance(latex_sym_key, sp.Symbol):
|
|
438
|
+
# subs() returns Basic, which is fine
|
|
439
|
+
self._sym_func = self._sym_func.subs(latex_sym_key, py_sym)
|
|
440
|
+
else:
|
|
441
|
+
# Try to find the symbol by name
|
|
442
|
+
latex_symbol = sp.Symbol(str(latex_sym_key))
|
|
443
|
+
self._sym_func = self._sym_func.subs(latex_symbol, py_sym)
|
|
444
|
+
|
|
445
|
+
# Get Python variable names as strings
|
|
446
|
+
if self._sym_func is not None and hasattr(self._sym_func, 'free_symbols'):
|
|
447
|
+
free_symbols = self._sym_func.free_symbols
|
|
448
|
+
self._var_symbols = sorted([str(s) for s in free_symbols])
|
|
449
|
+
else:
|
|
450
|
+
raise ValueError("Expression has no free symbols")
|
|
451
|
+
|
|
452
|
+
except Exception as e:
|
|
453
|
+
_msg = f"Failed to parse expression: {str(e)}"
|
|
454
|
+
raise ValueError(_msg)
|
|
455
|
+
|
|
456
|
+
# ========================================================================
|
|
457
|
+
# Simulation Execution
|
|
458
|
+
# ========================================================================
|
|
459
|
+
|
|
460
|
+
def _generate_sample(self,
|
|
461
|
+
var: Variable,
|
|
462
|
+
memory: Dict[str, float]) -> float:
|
|
463
|
+
"""*_generate_sample()* Generate a sample for a given variable.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
var (Variable): The variable to generate a sample for.
|
|
467
|
+
memory (Dict[str, float]): The current iteration values.
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
float: The generated sample.
|
|
471
|
+
"""
|
|
472
|
+
# Initialize sample
|
|
473
|
+
data: float = -1.0
|
|
474
|
+
|
|
475
|
+
# relevant data type, HOTFIX
|
|
476
|
+
_type = (list, tuple, np.ndarray)
|
|
477
|
+
|
|
478
|
+
# Get dependency values from memory
|
|
479
|
+
chace_deps = []
|
|
480
|
+
for dep in var.depends:
|
|
481
|
+
if dep in memory:
|
|
482
|
+
dep_val = memory[dep]
|
|
483
|
+
# If dependency is a list/tuple/array, take the last value
|
|
484
|
+
if isinstance(dep_val, (list, tuple, np.ndarray)):
|
|
485
|
+
dep_val = dep_val[-1]
|
|
486
|
+
chace_deps.append(dep_val)
|
|
487
|
+
|
|
488
|
+
# print(f"chace_deps: {chace_deps}")
|
|
489
|
+
|
|
490
|
+
# if the distribution function is defined
|
|
491
|
+
if var._dist_func is not None:
|
|
492
|
+
# If the variable is independent
|
|
493
|
+
if not var.depends:
|
|
494
|
+
data = var.sample()
|
|
495
|
+
|
|
496
|
+
# If the variable has dependencies
|
|
497
|
+
elif len(var.depends) == len(chace_deps):
|
|
498
|
+
raw_data = var.sample(*chace_deps)
|
|
499
|
+
# print(f"raw_data: {raw_data}")
|
|
500
|
+
|
|
501
|
+
# Handle array-like results
|
|
502
|
+
if isinstance(raw_data, _type):
|
|
503
|
+
# get the last number
|
|
504
|
+
data = raw_data[-1]
|
|
505
|
+
|
|
506
|
+
# adjust the memory accordingly to the rest of the list
|
|
507
|
+
for dep in var.depends:
|
|
508
|
+
if dep in memory:
|
|
509
|
+
memory[dep] = raw_data[var.depends.index(dep)]
|
|
510
|
+
# otherwise, its a number
|
|
511
|
+
else:
|
|
512
|
+
data = raw_data
|
|
513
|
+
|
|
514
|
+
# print(f"dependencies keys {var.depends}")
|
|
515
|
+
# print(f"memory: {memory}")
|
|
516
|
+
|
|
517
|
+
# Store sample in memory
|
|
518
|
+
memory[var.sym] = float(data)
|
|
519
|
+
|
|
520
|
+
# return sampled data
|
|
521
|
+
return data
|
|
522
|
+
|
|
523
|
+
def run(self, iters: Optional[int] = None) -> None:
|
|
524
|
+
"""*run()* Execute the Monte Carlo simulation.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
iters (int, optional): Number of iterations to run. If None, uses _experiments.
|
|
528
|
+
|
|
529
|
+
Raises:
|
|
530
|
+
ValueError: If simulation is not ready or encounters errors during execution.
|
|
531
|
+
"""
|
|
532
|
+
# Validate simulation readiness
|
|
533
|
+
self._validate_readiness()
|
|
534
|
+
|
|
535
|
+
# Set iterations if necessary
|
|
536
|
+
if iters is not None:
|
|
537
|
+
self._experiments = iters
|
|
538
|
+
|
|
539
|
+
# Clear previous results, inputs, and intermediate values
|
|
540
|
+
self._reset_memory()
|
|
541
|
+
|
|
542
|
+
# Create lambdify function using Python symbols
|
|
543
|
+
aliases = [self._aliases[v] for v in self._var_symbols]
|
|
544
|
+
self._exe_func = lambdify(aliases, self._sym_func, "numpy")
|
|
545
|
+
|
|
546
|
+
if self._exe_func is None:
|
|
547
|
+
raise ValueError("Failed to create executable function")
|
|
548
|
+
|
|
549
|
+
# Run experiment loop
|
|
550
|
+
for _iter in range(self._experiments):
|
|
551
|
+
try:
|
|
552
|
+
# Dict to store sample memory for the iteration
|
|
553
|
+
memory: Dict[str, float] = {}
|
|
554
|
+
|
|
555
|
+
# run through all variables
|
|
556
|
+
for var in self._variables.values():
|
|
557
|
+
# Check for cached value
|
|
558
|
+
cached_val = self._get_cached_value(var.sym, _iter)
|
|
559
|
+
|
|
560
|
+
# if no cached value, generate new sample
|
|
561
|
+
if cached_val is None or np.isnan(cached_val):
|
|
562
|
+
# Generate sample for the variable
|
|
563
|
+
val = self._generate_sample(var, memory)
|
|
564
|
+
# Store the sample in the iteration values
|
|
565
|
+
memory[var.sym] = val
|
|
566
|
+
self._set_cached_value(var.sym, _iter, val)
|
|
567
|
+
|
|
568
|
+
# otherwise use cached value
|
|
569
|
+
else:
|
|
570
|
+
# Use cached value
|
|
571
|
+
memory[var.sym] = cached_val
|
|
572
|
+
|
|
573
|
+
# Prepare sorted/ordered values from memory for evaluation
|
|
574
|
+
sorted_vals = [memory[var] for var in self._latex_to_py]
|
|
575
|
+
|
|
576
|
+
# FIXME hotfix for queue functions
|
|
577
|
+
_type = (list, tuple, np.ndarray)
|
|
578
|
+
# Handle adjusted values
|
|
579
|
+
if any(isinstance(v, _type) for v in sorted_vals):
|
|
580
|
+
sorted_vals = [
|
|
581
|
+
v[-1] if isinstance(v, _type) else v for v in sorted_vals]
|
|
582
|
+
|
|
583
|
+
# Evaluate the coefficient
|
|
584
|
+
result = float(self._exe_func(*sorted_vals))
|
|
585
|
+
|
|
586
|
+
# Handle array results
|
|
587
|
+
if isinstance(result, _type):
|
|
588
|
+
result = result[-1]
|
|
589
|
+
sorted_vals = [v[-1] for v in result]
|
|
590
|
+
|
|
591
|
+
# Save simulation inputs and results
|
|
592
|
+
self.inputs[_iter, :] = sorted_vals
|
|
593
|
+
self._results[_iter] = result
|
|
594
|
+
|
|
595
|
+
except Exception as e:
|
|
596
|
+
_msg = f"Error during simulation run {_iter}: {str(e)}"
|
|
597
|
+
raise ValueError(_msg)
|
|
598
|
+
|
|
599
|
+
# Calculate statistics
|
|
600
|
+
self._calculate_statistics()
|
|
601
|
+
|
|
602
|
+
# ========================================================================
|
|
603
|
+
# Memory and Statistics Management
|
|
604
|
+
# ========================================================================
|
|
605
|
+
|
|
606
|
+
def _reset_memory(self) -> None:
|
|
607
|
+
"""*_reset_memory()* Reset results and inputs arrays."""
|
|
608
|
+
# reseting full array space with NaN
|
|
609
|
+
n_sym = len(self._symbols)
|
|
610
|
+
self.inputs = np.full((self._experiments, n_sym), np.nan)
|
|
611
|
+
self._results = np.full((self._experiments, 1), np.nan)
|
|
612
|
+
|
|
613
|
+
# # reset intermediate values
|
|
614
|
+
# for var in self._variables.keys():
|
|
615
|
+
# self._simul_cache[var] = np.full((self._experiments, 1),
|
|
616
|
+
# np.nan,
|
|
617
|
+
# dtype=np.float64)
|
|
618
|
+
|
|
619
|
+
def _reset_statistics(self) -> None:
|
|
620
|
+
"""*_reset_statistics()* Reset all statistical attributes to default values."""
|
|
621
|
+
# reset statistics to NaN or zero
|
|
622
|
+
self._mean = np.nan
|
|
623
|
+
self._median = np.nan
|
|
624
|
+
self._std_dev = np.nan
|
|
625
|
+
self._variance = np.nan
|
|
626
|
+
self._min = np.nan
|
|
627
|
+
self._max = np.nan
|
|
628
|
+
self._count = 0
|
|
629
|
+
|
|
630
|
+
def _calculate_statistics(self) -> None:
|
|
631
|
+
"""*_calculate_statistics()* Calculate statistical properties of simulation results."""
|
|
632
|
+
# Check for empty array (size == 0), not None
|
|
633
|
+
if self._results.size == 0:
|
|
634
|
+
raise ValueError("No results available. Run simulation first.")
|
|
635
|
+
|
|
636
|
+
else:
|
|
637
|
+
self._mean = float(np.mean(self._results))
|
|
638
|
+
self._median = float(np.median(self._results))
|
|
639
|
+
self._std_dev = float(np.std(self._results))
|
|
640
|
+
self._variance = float(np.var(self._results))
|
|
641
|
+
self._min = float(np.min(self._results))
|
|
642
|
+
self._max = float(np.max(self._results))
|
|
643
|
+
self._count = len(self._results)
|
|
644
|
+
|
|
645
|
+
def get_confidence_interval(self,
|
|
646
|
+
conf: float = 0.95) -> Tuple[float, float]:
|
|
647
|
+
"""*get_confidence_interval()* Calculate the confidence interval.
|
|
648
|
+
|
|
649
|
+
Args:
|
|
650
|
+
conf (float, optional): Confidence level for the interval. Defaults to 0.95.
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
ValueError: If no results are available or if the confidence level is invalid.
|
|
654
|
+
|
|
655
|
+
Returns:
|
|
656
|
+
Tuple[float, float]: Lower and upper bounds of the confidence interval.
|
|
657
|
+
"""
|
|
658
|
+
if self._results.size == 0:
|
|
659
|
+
_msg = "No results available. Run the simulation first."
|
|
660
|
+
raise ValueError(_msg)
|
|
661
|
+
|
|
662
|
+
if not 0 < conf < 1:
|
|
663
|
+
_msg = f"Confidence must be between 0 and 1. Got: {conf}"
|
|
664
|
+
raise ValueError(_msg)
|
|
665
|
+
|
|
666
|
+
# Calculate the margin of error using the t-distribution
|
|
667
|
+
alpha = stats.t.ppf((1 + conf) / 2, self._count - 1)
|
|
668
|
+
margin = alpha * self._std_dev / np.sqrt(self._count)
|
|
669
|
+
ans = (self._mean - margin, self._mean + margin)
|
|
670
|
+
return ans
|
|
671
|
+
|
|
672
|
+
# ========================================================================
|
|
673
|
+
# simulation cache management
|
|
674
|
+
# ========================================================================
|
|
675
|
+
|
|
676
|
+
def _validate_cache_locations(self,
|
|
677
|
+
var_syms: Union[str, List[str]],
|
|
678
|
+
idx: int) -> bool:
|
|
679
|
+
"""*_validate_cache_locations()* Check if cache locations are valid for variable(s) at the iteration.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
var_syms (Union[str, List[str]]): Variable symbol(s) to check.
|
|
683
|
+
idx (int): Iteration index to check.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
bool: True if all cache locations are valid (including NaN placeholders), False otherwise.
|
|
687
|
+
"""
|
|
688
|
+
# Convert single string to list for uniform handling
|
|
689
|
+
syms = [var_syms] if isinstance(var_syms, str) else var_syms
|
|
690
|
+
|
|
691
|
+
# Start with assumption that cache is invalid
|
|
692
|
+
valid = False
|
|
693
|
+
|
|
694
|
+
# Check each symbol
|
|
695
|
+
for var_sym in syms:
|
|
696
|
+
# Reset validity check for each variable
|
|
697
|
+
var_valid = False
|
|
698
|
+
|
|
699
|
+
# Get cache array for the variable
|
|
700
|
+
cache_array = self._simul_cache.get(var_sym, None)
|
|
701
|
+
|
|
702
|
+
# Check if cache exists and location is valid
|
|
703
|
+
if cache_array is not None:
|
|
704
|
+
# Check if index is within bounds
|
|
705
|
+
if idx < cache_array.shape[0] and idx >= 0:
|
|
706
|
+
# Location exists - valid regardless of whether value is NaN
|
|
707
|
+
# (NaN is a valid placeholder for uncomputed values)
|
|
708
|
+
var_valid = True
|
|
709
|
+
|
|
710
|
+
# If any variable is invalid, entire check fails
|
|
711
|
+
if not var_valid:
|
|
712
|
+
return False
|
|
713
|
+
|
|
714
|
+
# All variables passed validation
|
|
715
|
+
valid = True
|
|
716
|
+
return valid
|
|
717
|
+
|
|
718
|
+
def _get_cached_value(self, var_sym: str, idx: int) -> Optional[float]:
|
|
719
|
+
"""*_get_cached_value()* Retrieve cached value for variable at the iteration.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
var_sym (str): Variable symbol.
|
|
723
|
+
idx (int): Iteration index.
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
Optional[float]: Cached value if valid, None otherwise.
|
|
727
|
+
"""
|
|
728
|
+
# Initialize return value
|
|
729
|
+
cache_data = None
|
|
730
|
+
|
|
731
|
+
# Check if cache location is valid
|
|
732
|
+
if self._validate_cache_locations(var_sym, idx):
|
|
733
|
+
# Retrieve cached data
|
|
734
|
+
cache_data = self._simul_cache[var_sym][idx, 0]
|
|
735
|
+
# if value is not NaN (valid location, but no data yet)
|
|
736
|
+
if not np.isnan(cache_data):
|
|
737
|
+
# cast to float the computed value
|
|
738
|
+
cache_data = float(cache_data)
|
|
739
|
+
# return valid cache location
|
|
740
|
+
return cache_data
|
|
741
|
+
|
|
742
|
+
def _set_cached_value(self,
|
|
743
|
+
var_sym: str,
|
|
744
|
+
idx: int,
|
|
745
|
+
val: Union[float, Dict]) -> None:
|
|
746
|
+
"""*_set_cached_value()* Store value in cache for variable at the iteration.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
var_sym (str): Variable symbol.
|
|
750
|
+
idx (int): Iteration index.
|
|
751
|
+
val (Union[float, Dict]): Value to cache. It can be a normal number (float) or a memory cache correction (dict).
|
|
752
|
+
|
|
753
|
+
Raises:
|
|
754
|
+
ValueError: If cache location is invalid.
|
|
755
|
+
"""
|
|
756
|
+
# Normalize input to dictionary format
|
|
757
|
+
cache_updates = val if isinstance(val, dict) else {var_sym: val}
|
|
758
|
+
|
|
759
|
+
# Validate all cache locations
|
|
760
|
+
if not self._validate_cache_locations(list(cache_updates.keys()), idx):
|
|
761
|
+
invalid_vars = list(cache_updates.keys())
|
|
762
|
+
_msg = f"Invalid cache location at index {idx}. "
|
|
763
|
+
_msg += f"For variables: {invalid_vars}"
|
|
764
|
+
raise ValueError(_msg)
|
|
765
|
+
|
|
766
|
+
# Store all values
|
|
767
|
+
for k, v in cache_updates.items():
|
|
768
|
+
self._simul_cache[k][idx, 0] = v
|
|
769
|
+
|
|
770
|
+
# ========================================================================
|
|
771
|
+
# Results Extraction
|
|
772
|
+
# ========================================================================
|
|
773
|
+
|
|
774
|
+
def extract_results(self) -> Dict[str, NDArray[np.float64]]:
|
|
775
|
+
"""*extract_results()* Extract simulation results.
|
|
776
|
+
|
|
777
|
+
Returns:
|
|
778
|
+
Dict[str, NDArray[np.float64]]: Dictionary containing simulation results.
|
|
779
|
+
"""
|
|
780
|
+
export: Dict[str, NDArray[np.float64]] = {}
|
|
781
|
+
|
|
782
|
+
# Extract all values for each variable (column)
|
|
783
|
+
for i, var in enumerate(self._py_to_latex.values()):
|
|
784
|
+
# Get the entire column for this variable (all simulation runs)
|
|
785
|
+
column = self.inputs[:, i]
|
|
786
|
+
|
|
787
|
+
# Use a meaningful key that includes variable name and coefficient
|
|
788
|
+
key = f"{var}@{self._coefficient.sym}"
|
|
789
|
+
export[key] = column
|
|
790
|
+
|
|
791
|
+
# Add the coefficient results
|
|
792
|
+
export[self._coefficient.sym] = self._results.flatten()
|
|
793
|
+
return export
|
|
794
|
+
|
|
795
|
+
# ========================================================================
|
|
796
|
+
# Properties
|
|
797
|
+
# ========================================================================
|
|
798
|
+
|
|
799
|
+
@property
|
|
800
|
+
def variables(self) -> Dict[str, Variable]:
|
|
801
|
+
"""*variables* Get the variables involved in the simulation.
|
|
802
|
+
|
|
803
|
+
Returns:
|
|
804
|
+
Dict[str, Variable]: Dictionary of variable symbols and Variable objects.
|
|
805
|
+
"""
|
|
806
|
+
return self._variables.copy()
|
|
807
|
+
|
|
808
|
+
@property
|
|
809
|
+
def coefficient(self) -> Optional[Coefficient]:
|
|
810
|
+
"""*coefficient* Get the coefficient associated with the simulation.
|
|
811
|
+
|
|
812
|
+
Returns:
|
|
813
|
+
Optional[Coefficient]: The associated Coefficient object, or None.
|
|
814
|
+
"""
|
|
815
|
+
return self._coefficient
|
|
816
|
+
|
|
817
|
+
@property
|
|
818
|
+
def results(self) -> NDArray[np.float64]:
|
|
819
|
+
"""*results* Raw simulation results.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
NDArray[np.float64]: Copy of the simulation results.
|
|
823
|
+
|
|
824
|
+
Raises:
|
|
825
|
+
ValueError: If no results are available.
|
|
826
|
+
"""
|
|
827
|
+
if self._results.size == 0:
|
|
828
|
+
raise ValueError("No results available. Run the simulation first.")
|
|
829
|
+
return self._results.copy()
|
|
830
|
+
|
|
831
|
+
@property
|
|
832
|
+
def statistics(self) -> Dict[str, float]:
|
|
833
|
+
"""*statistics* Get the statistical analysis of simulation results.
|
|
834
|
+
|
|
835
|
+
Raises:
|
|
836
|
+
ValueError: If no results are available.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
Dict[str, float]: Dictionary containing statistical properties.
|
|
840
|
+
"""
|
|
841
|
+
if self._results.size == 0:
|
|
842
|
+
_msg = "No statistics available. Run the simulation first."
|
|
843
|
+
raise ValueError(_msg)
|
|
844
|
+
|
|
845
|
+
# Build statistics dictionary from individual attributes
|
|
846
|
+
self._statistics = {
|
|
847
|
+
"mean": self._mean,
|
|
848
|
+
"median": self._median,
|
|
849
|
+
"std_dev": self._std_dev,
|
|
850
|
+
"variance": self._variance,
|
|
851
|
+
"min": self._min,
|
|
852
|
+
"max": self._max,
|
|
853
|
+
"count": self._count
|
|
854
|
+
}
|
|
855
|
+
return self._statistics
|
|
856
|
+
|
|
857
|
+
@property
|
|
858
|
+
def experiments(self) -> int:
|
|
859
|
+
"""*experiments* Number of simulation experiments.
|
|
860
|
+
|
|
861
|
+
Returns:
|
|
862
|
+
int: Current number of experiments.
|
|
863
|
+
"""
|
|
864
|
+
return self._experiments
|
|
865
|
+
|
|
866
|
+
@experiments.setter
|
|
867
|
+
@validate_range(min_value=1)
|
|
868
|
+
def experiments(self, val: int) -> None:
|
|
869
|
+
"""*experiments* Set the number of simulation runs.
|
|
870
|
+
|
|
871
|
+
Args:
|
|
872
|
+
val (int): Number of experiments to run the simulation.
|
|
873
|
+
|
|
874
|
+
Raises:
|
|
875
|
+
ValueError: If the number of experiments is not positive.
|
|
876
|
+
"""
|
|
877
|
+
self._experiments = val
|
|
878
|
+
|
|
879
|
+
@property
|
|
880
|
+
def distributions(self) -> Dict[str, Dict[str, Any]]:
|
|
881
|
+
"""*distributions* Get the variable distributions.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
Dict[str, Dict[str, Any]]: Current variable distributions.
|
|
885
|
+
"""
|
|
886
|
+
return self._distributions.copy()
|
|
887
|
+
|
|
888
|
+
@distributions.setter
|
|
889
|
+
@validate_custom(lambda self, val: self._validate_dist(val,
|
|
890
|
+
"distributions"))
|
|
891
|
+
def distributions(self, val: Dict[str, Dict[str, Any]]) -> None:
|
|
892
|
+
"""*distributions* Set the variable distributions.
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
val (Dict[str, Dict[str, Any]]): New variable distributions.
|
|
896
|
+
|
|
897
|
+
Raises:
|
|
898
|
+
ValueError: If the distributions are invalid.
|
|
899
|
+
"""
|
|
900
|
+
self._distributions = val
|
|
901
|
+
|
|
902
|
+
@property
|
|
903
|
+
def dependencies(self) -> Dict[str, List[str]]:
|
|
904
|
+
"""*dependencies* Get variable dependencies.
|
|
905
|
+
|
|
906
|
+
Returns:
|
|
907
|
+
Dict[str, List[str]]: Dictionary of variable dependencies.
|
|
908
|
+
"""
|
|
909
|
+
return self._dependencies
|
|
910
|
+
|
|
911
|
+
@dependencies.setter
|
|
912
|
+
@validate_type(dict)
|
|
913
|
+
def dependencies(self, val: Dict[str, List[str]]) -> None:
|
|
914
|
+
"""*dependencies* Set variable dependencies.
|
|
915
|
+
|
|
916
|
+
Args:
|
|
917
|
+
val (Dict[str, List[str]]): New variable dependencies.
|
|
918
|
+
"""
|
|
919
|
+
self._dependencies = val
|
|
920
|
+
|
|
921
|
+
# Individual statistics properties
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def mean(self) -> float:
|
|
925
|
+
"""*mean* Mean value of simulation results.
|
|
926
|
+
|
|
927
|
+
Returns:
|
|
928
|
+
float: Mean value.
|
|
929
|
+
|
|
930
|
+
Raises:
|
|
931
|
+
ValueError: If no results are available.
|
|
932
|
+
"""
|
|
933
|
+
if self._results.size == 0:
|
|
934
|
+
_msg = "No statistics available. Run the simulation first."
|
|
935
|
+
raise ValueError(_msg)
|
|
936
|
+
return self._mean
|
|
937
|
+
|
|
938
|
+
@property
|
|
939
|
+
def median(self) -> float:
|
|
940
|
+
"""*median* Median value of simulation results.
|
|
941
|
+
|
|
942
|
+
Returns:
|
|
943
|
+
float: Median value.
|
|
944
|
+
|
|
945
|
+
Raises:
|
|
946
|
+
ValueError: If no results are available.
|
|
947
|
+
"""
|
|
948
|
+
if self._results.size == 0:
|
|
949
|
+
_msg = "No statistics available. Run the simulation first."
|
|
950
|
+
raise ValueError(_msg)
|
|
951
|
+
return self._median
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def std_dev(self) -> float:
|
|
955
|
+
"""*std_dev* Standard deviation of simulation results.
|
|
956
|
+
|
|
957
|
+
Returns:
|
|
958
|
+
float: Standard deviation.
|
|
959
|
+
|
|
960
|
+
Raises:
|
|
961
|
+
ValueError: If no results are available.
|
|
962
|
+
"""
|
|
963
|
+
if self._results.size == 0:
|
|
964
|
+
_msg = "No statistics available. Run the simulation first."
|
|
965
|
+
raise ValueError(_msg)
|
|
966
|
+
return self._std_dev
|
|
967
|
+
|
|
968
|
+
@property
|
|
969
|
+
def variance(self) -> float:
|
|
970
|
+
"""*variance* Variance of simulation results.
|
|
971
|
+
|
|
972
|
+
Returns:
|
|
973
|
+
float: Variance value.
|
|
974
|
+
|
|
975
|
+
Raises:
|
|
976
|
+
ValueError: If no results are available.
|
|
977
|
+
"""
|
|
978
|
+
if self._results.size == 0:
|
|
979
|
+
_msg = "No statistics available. Run the simulation first."
|
|
980
|
+
raise ValueError(_msg)
|
|
981
|
+
return self._variance
|
|
982
|
+
|
|
983
|
+
@property
|
|
984
|
+
def min_value(self) -> float:
|
|
985
|
+
"""*min_value* Minimum value in simulation results.
|
|
986
|
+
|
|
987
|
+
Returns:
|
|
988
|
+
float: Minimum value.
|
|
989
|
+
|
|
990
|
+
Raises:
|
|
991
|
+
ValueError: If no results are available.
|
|
992
|
+
"""
|
|
993
|
+
if self._results.size == 0:
|
|
994
|
+
_msg = "No statistics available. Run the simulation first."
|
|
995
|
+
raise ValueError(_msg)
|
|
996
|
+
return self._min
|
|
997
|
+
|
|
998
|
+
@property
|
|
999
|
+
def max_value(self) -> float:
|
|
1000
|
+
"""*max_value* Maximum value in simulation results.
|
|
1001
|
+
|
|
1002
|
+
Returns:
|
|
1003
|
+
float: Maximum value.
|
|
1004
|
+
|
|
1005
|
+
Raises:
|
|
1006
|
+
ValueError: If no results are available.
|
|
1007
|
+
"""
|
|
1008
|
+
if self._results.size == 0:
|
|
1009
|
+
_msg = "No statistics available. Run the simulation first."
|
|
1010
|
+
raise ValueError(_msg)
|
|
1011
|
+
return self._max
|
|
1012
|
+
|
|
1013
|
+
@property
|
|
1014
|
+
def count(self) -> int:
|
|
1015
|
+
"""*count* Number of valid simulation results.
|
|
1016
|
+
|
|
1017
|
+
Returns:
|
|
1018
|
+
int: Result count.
|
|
1019
|
+
|
|
1020
|
+
Raises:
|
|
1021
|
+
ValueError: If no results are available.
|
|
1022
|
+
"""
|
|
1023
|
+
if self._results.size == 0:
|
|
1024
|
+
_msg = "No statistics available. Run the simulation first."
|
|
1025
|
+
raise ValueError(_msg)
|
|
1026
|
+
return self._count
|
|
1027
|
+
|
|
1028
|
+
@property
|
|
1029
|
+
def summary(self) -> Dict[str, float]:
|
|
1030
|
+
"""*summary* Get the statistical analysis of simulation results.
|
|
1031
|
+
|
|
1032
|
+
Raises:
|
|
1033
|
+
ValueError: If no results are available.
|
|
1034
|
+
|
|
1035
|
+
Returns:
|
|
1036
|
+
Dict[str, float]: Dictionary containing statistical properties.
|
|
1037
|
+
"""
|
|
1038
|
+
if self._results.size == 0:
|
|
1039
|
+
_msg = "No statistics available. Run the simulation first."
|
|
1040
|
+
raise ValueError(_msg)
|
|
1041
|
+
|
|
1042
|
+
# Build summary dictionary from individual attributes
|
|
1043
|
+
self._summary = {
|
|
1044
|
+
"mean": self._mean,
|
|
1045
|
+
"median": self._median,
|
|
1046
|
+
"std_dev": self._std_dev,
|
|
1047
|
+
"variance": self._variance,
|
|
1048
|
+
"min": self._min,
|
|
1049
|
+
"max": self._max,
|
|
1050
|
+
"count": self._count
|
|
1051
|
+
}
|
|
1052
|
+
return self._summary
|
|
1053
|
+
|
|
1054
|
+
# ========================================================================
|
|
1055
|
+
# Utility Methods
|
|
1056
|
+
# ========================================================================
|
|
1057
|
+
|
|
1058
|
+
def clear(self) -> None:
|
|
1059
|
+
"""*clear()* Reset all attributes to default values."""
|
|
1060
|
+
# Reset base class attributes
|
|
1061
|
+
self._idx = -1
|
|
1062
|
+
self._sym = "MC_\\Pi_{}"
|
|
1063
|
+
self._alias = ""
|
|
1064
|
+
self._fwk = "PHYSICAL"
|
|
1065
|
+
self._name = ""
|
|
1066
|
+
self.description = ""
|
|
1067
|
+
|
|
1068
|
+
# Reset simulation attributes
|
|
1069
|
+
self._pi_expr = None
|
|
1070
|
+
self._sym_func = None
|
|
1071
|
+
self._exe_func = None
|
|
1072
|
+
self._variables = {}
|
|
1073
|
+
self._latex_to_py = {}
|
|
1074
|
+
self._py_to_latex = {}
|
|
1075
|
+
self._experiments = -1
|
|
1076
|
+
self._distributions = {}
|
|
1077
|
+
|
|
1078
|
+
# reset results, inputs and intermediate values
|
|
1079
|
+
self._reset_memory()
|
|
1080
|
+
|
|
1081
|
+
# Reset statistics
|
|
1082
|
+
self._reset_statistics()
|
|
1083
|
+
|
|
1084
|
+
# ========================================================================
|
|
1085
|
+
# Serialization
|
|
1086
|
+
# ========================================================================
|
|
1087
|
+
|
|
1088
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1089
|
+
"""*to_dict()* Convert simulation to dictionary representation."""
|
|
1090
|
+
return {
|
|
1091
|
+
# Foundation class attributes
|
|
1092
|
+
"idx": self._idx,
|
|
1093
|
+
"sym": self._sym,
|
|
1094
|
+
"alias": self._alias,
|
|
1095
|
+
"fwk": self._fwk,
|
|
1096
|
+
"name": self._name,
|
|
1097
|
+
"description": self.description,
|
|
1098
|
+
# Simulation attributes
|
|
1099
|
+
"pi_expr": self._pi_expr,
|
|
1100
|
+
"variables": self._variables,
|
|
1101
|
+
"iterations": self._experiments,
|
|
1102
|
+
# Results
|
|
1103
|
+
"mean": self._mean,
|
|
1104
|
+
"median": self._median,
|
|
1105
|
+
"std_dev": self._std_dev,
|
|
1106
|
+
"variance": self._variance,
|
|
1107
|
+
"min": self._min,
|
|
1108
|
+
"max": self._max,
|
|
1109
|
+
"count": self._count,
|
|
1110
|
+
"inputs": self.inputs.tolist() if self.inputs.size > 0 else None,
|
|
1111
|
+
"results": self._results.tolist() if self._results.size > 0 else None,
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
@classmethod
|
|
1115
|
+
def from_dict(cls, data: Dict[str, Any]) -> "MonteCarlo":
|
|
1116
|
+
"""*from_dict()* Create simulation from dictionary representation.
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
data (Dict[str, Any]): Dictionary representation.
|
|
1120
|
+
|
|
1121
|
+
Returns:
|
|
1122
|
+
MonteCarlo: New simulation instance.
|
|
1123
|
+
"""
|
|
1124
|
+
# Create basic instance
|
|
1125
|
+
instance = cls(
|
|
1126
|
+
_name=data.get("name", ""),
|
|
1127
|
+
description=data.get("description", ""),
|
|
1128
|
+
_idx=data.get("idx", -1),
|
|
1129
|
+
_sym=data.get("sym", "MC_\\Pi_{}"),
|
|
1130
|
+
_fwk=data.get("fwk", "PHYSICAL"),
|
|
1131
|
+
_alias=data.get("alias", ""),
|
|
1132
|
+
_cat=data.get("cat", "NUM"),
|
|
1133
|
+
_pi_expr=data.get("pi_expr", None),
|
|
1134
|
+
_experiments=data.get("iterations", -1),
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
# The to_dict() method stores them at the top level
|
|
1138
|
+
instance._mean = data.get("mean", np.nan)
|
|
1139
|
+
instance._median = data.get("median", np.nan)
|
|
1140
|
+
instance._std_dev = data.get("std_dev", np.nan)
|
|
1141
|
+
instance._variance = data.get("variance", np.nan)
|
|
1142
|
+
instance._min = data.get("min", np.nan)
|
|
1143
|
+
instance._max = data.get("max", np.nan)
|
|
1144
|
+
instance._count = data.get("count", 0)
|
|
1145
|
+
|
|
1146
|
+
# Optionally set inputs and results if available
|
|
1147
|
+
if "inputs" in data and data["inputs"] is not None:
|
|
1148
|
+
instance.inputs = np.array(data["inputs"], dtype=np.float64)
|
|
1149
|
+
|
|
1150
|
+
if "results" in data and data["results"] is not None:
|
|
1151
|
+
instance._results = np.array(data["results"], dtype=np.float64)
|
|
1152
|
+
|
|
1153
|
+
# Optionally set variables if available
|
|
1154
|
+
if "variables" in data and data["variables"]:
|
|
1155
|
+
for sym, specs in data["variables"].items():
|
|
1156
|
+
instance._variables[sym] = Variable(**specs)
|
|
1157
|
+
|
|
1158
|
+
return instance
|