mocaco 0.1.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.
- mocaco/__init__.py +10 -0
- mocaco/api.py +109 -0
- mocaco/api.pyi +40 -0
- mocaco/methods/__init__.py +7 -0
- mocaco/methods/clt_absolute.py +133 -0
- mocaco/protocols.py +104 -0
- mocaco/registry.py +126 -0
- mocaco/result.py +222 -0
- mocaco/wrappers.py +170 -0
- mocaco-0.1.0.dist-info/METADATA +105 -0
- mocaco-0.1.0.dist-info/RECORD +13 -0
- mocaco-0.1.0.dist-info/WHEEL +4 -0
- mocaco-0.1.0.dist-info/licenses/LICENSE +165 -0
mocaco/__init__.py
ADDED
mocaco/api.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Main module to define the API behaviour and usage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from . import methods as _methods # noqa: F401
|
|
8
|
+
from .protocols import SampleFrame
|
|
9
|
+
from .registry import registry
|
|
10
|
+
from .wrappers import EvalFrequencyWrapper
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Callable
|
|
14
|
+
|
|
15
|
+
import polars as pl
|
|
16
|
+
|
|
17
|
+
from .result import ConvergenceResult
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def samples(
|
|
21
|
+
df: pl.DataFrame,
|
|
22
|
+
*,
|
|
23
|
+
target_col: str,
|
|
24
|
+
it_col: str | None = None,
|
|
25
|
+
) -> SampleFrame:
|
|
26
|
+
"""
|
|
27
|
+
Create the normalized and frozen data representation used by convergence criteria.
|
|
28
|
+
|
|
29
|
+
This prevents input errors and ensure the input data is read-only and is
|
|
30
|
+
never modified accidentally.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
df
|
|
35
|
+
Source Polars DataFrame.
|
|
36
|
+
it_col
|
|
37
|
+
Column containing the iteration/order of the samples. If the user omit it, it
|
|
38
|
+
is automatically completed.
|
|
39
|
+
target_col
|
|
40
|
+
Column containing the quantity to analyze.
|
|
41
|
+
"""
|
|
42
|
+
return SampleFrame(
|
|
43
|
+
df=df,
|
|
44
|
+
target_col=target_col,
|
|
45
|
+
it_col=it_col,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Convergence:
|
|
50
|
+
"""
|
|
51
|
+
Callable namespace exposing registered convergence criteria as attributes.
|
|
52
|
+
|
|
53
|
+
Use as ``convergence(samples, method="...", **kwargs)`` for generic calls,
|
|
54
|
+
or ``convergence.clt_absolute(samples, threshold=...)`` for typed calls.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __call__(
|
|
58
|
+
self,
|
|
59
|
+
samples: SampleFrame,
|
|
60
|
+
*,
|
|
61
|
+
method: str,
|
|
62
|
+
**kwargs: Any,
|
|
63
|
+
) -> ConvergenceResult:
|
|
64
|
+
"""Invoke a registered convergence criterion generically by name."""
|
|
65
|
+
criterion = registry.get(method)
|
|
66
|
+
eval_frequency = kwargs.pop("eval_frequency", None)
|
|
67
|
+
params = criterion.params_type(**kwargs)
|
|
68
|
+
|
|
69
|
+
if eval_frequency is not None:
|
|
70
|
+
if not getattr(criterion, "supports_eval_frequency", False):
|
|
71
|
+
raise TypeError(f"Criterion '{criterion.name}' does not support eval_frequency")
|
|
72
|
+
wrapper = EvalFrequencyWrapper(criterion, eval_frequency=eval_frequency)
|
|
73
|
+
return wrapper.run(samples=samples, params=params)
|
|
74
|
+
|
|
75
|
+
return criterion.run(
|
|
76
|
+
samples=samples,
|
|
77
|
+
params=params,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def __getattr__(self, name: str) -> Callable[..., ConvergenceResult]:
|
|
81
|
+
"""Resolve registered criteria as callable attributes."""
|
|
82
|
+
criterion = registry.get(name)
|
|
83
|
+
|
|
84
|
+
def _method(samples: SampleFrame, **kwargs: Any) -> ConvergenceResult:
|
|
85
|
+
eval_frequency = kwargs.pop("eval_frequency", None)
|
|
86
|
+
params = criterion.params_type(**kwargs)
|
|
87
|
+
|
|
88
|
+
if eval_frequency is not None:
|
|
89
|
+
if not getattr(criterion, "supports_eval_frequency", False):
|
|
90
|
+
raise TypeError(f"Criterion '{criterion.name}' does not support eval_frequency")
|
|
91
|
+
wrapper = EvalFrequencyWrapper(criterion, eval_frequency=eval_frequency)
|
|
92
|
+
return wrapper.run(samples=samples, params=params)
|
|
93
|
+
|
|
94
|
+
return criterion.run(samples=samples, params=params)
|
|
95
|
+
|
|
96
|
+
return _method
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
convergence = Convergence()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def methods() -> tuple[str, ...]:
|
|
103
|
+
"""Return the names of all registered convergence criteria."""
|
|
104
|
+
return registry.names()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def describe(name: str) -> None:
|
|
108
|
+
"""Return metadata and parameter information for a criterion."""
|
|
109
|
+
return registry.describe(name)
|
mocaco/api.pyi
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import polars as pl
|
|
6
|
+
|
|
7
|
+
from .protocols import SampleFrame
|
|
8
|
+
from .result import ConvergenceResult
|
|
9
|
+
|
|
10
|
+
def samples(df: pl.DataFrame, *, target_col: str, it_col: str | None = None) -> SampleFrame: ...
|
|
11
|
+
def methods() -> tuple[str, ...]: ...
|
|
12
|
+
def describe(name: str) -> None: ...
|
|
13
|
+
|
|
14
|
+
class Convergence:
|
|
15
|
+
def __call__(
|
|
16
|
+
self, samples: SampleFrame, *, method: str, **kwargs: Any
|
|
17
|
+
) -> ConvergenceResult: ...
|
|
18
|
+
def clt_absolute(
|
|
19
|
+
self,
|
|
20
|
+
samples: SampleFrame,
|
|
21
|
+
*,
|
|
22
|
+
threshold: float,
|
|
23
|
+
confidence_level: float = 0.95,
|
|
24
|
+
eval_frequency: int = 1,
|
|
25
|
+
) -> ConvergenceResult:
|
|
26
|
+
"""
|
|
27
|
+
Central Limit Theorem based convergence criterion using an absolute Monte Carlo error threshold.
|
|
28
|
+
|
|
29
|
+
Parameters
|
|
30
|
+
----------
|
|
31
|
+
threshold : float
|
|
32
|
+
Maximum accepted absolute Monte Carlo error.
|
|
33
|
+
confidence_level : float, default 0.95
|
|
34
|
+
Confidence level used for the CLT margin of error.
|
|
35
|
+
eval_frequency : int, default 1
|
|
36
|
+
Number of rows between successive evaluations.
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
convergence: Convergence
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Central Limit Theorem based convergence criterion using an absolute Monte Carlo error threshold."""
|
|
2
|
+
|
|
3
|
+
# Mandatory basic modules
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
# Basic modules used to build the criterion (According contributor needs)
|
|
9
|
+
import polars as pl
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
from scipy.stats import norm
|
|
12
|
+
|
|
13
|
+
# Modules needed from mocaco
|
|
14
|
+
from mocaco.registry import registry
|
|
15
|
+
from mocaco.result import ConvergenceResult
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from mocaco.protocols import SampleFrame
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 1. Criterion input parameters
|
|
22
|
+
class CLTAbsoluteParams(BaseModel):
|
|
23
|
+
threshold: float = Field(
|
|
24
|
+
...,
|
|
25
|
+
gt=0,
|
|
26
|
+
description="Maximum accepted absolute Monte Carlo error.",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
confidence_level: float = Field(
|
|
30
|
+
0.95,
|
|
31
|
+
gt=0,
|
|
32
|
+
lt=1,
|
|
33
|
+
description="Confidence level used for the CLT margin of error.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# 2. Criterion logic (calculous)
|
|
38
|
+
@registry.register()
|
|
39
|
+
class CLTAbsoluteCriterion:
|
|
40
|
+
name = "clt_absolute"
|
|
41
|
+
|
|
42
|
+
supports_eval_frequency = True
|
|
43
|
+
|
|
44
|
+
description = (
|
|
45
|
+
"Central Limit Theorem based convergence criterion "
|
|
46
|
+
"using an absolute Monte Carlo error threshold."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
params_type = CLTAbsoluteParams
|
|
50
|
+
|
|
51
|
+
assumptions = (
|
|
52
|
+
"Samples are independent and identically distributed (i.i.d.).",
|
|
53
|
+
"The target quantity has finite variance.",
|
|
54
|
+
"The sample size is sufficiently large for the CLT to provide a normal approximation. The deafult stb_window = 30 aims to support this assumption.",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
limitations = (
|
|
58
|
+
"This is a probabilistic stopping criterion, not a proof of convergence.",
|
|
59
|
+
"For small sample sizes, the normal approximation may be inaccurate.",
|
|
60
|
+
"Does not detect systematic bias or non-stationarity in the sampling process.",
|
|
61
|
+
"The stability window introduces a lag in detecting convergence.",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
result_interpretation = (
|
|
65
|
+
"is_converged is True when the CLT-based absolute error has stayed below "
|
|
66
|
+
"the threshold for at least stb_window consecutive iterations. "
|
|
67
|
+
"estimate is the cumulative mean at the final sample. "
|
|
68
|
+
"error is the CLT-based absolute error (z_score * SEM) at the final sample. "
|
|
69
|
+
"n is the total number of samples evaluated."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
example_usage = """\
|
|
73
|
+
import polars as pl
|
|
74
|
+
import mocaco as mcc
|
|
75
|
+
|
|
76
|
+
df = pl.DataFrame({"it": range(100), "value": [1.0] * 100})
|
|
77
|
+
samples = mcc.samples(df, it_col="it", target_col="value")
|
|
78
|
+
result = mcc.convergence.clt_absolute(samples, threshold=0.1)
|
|
79
|
+
print(result.is_converged)"""
|
|
80
|
+
|
|
81
|
+
references = ("https://en.wikipedia.org/wiki/Central_limit_theorem",)
|
|
82
|
+
|
|
83
|
+
def run(
|
|
84
|
+
self,
|
|
85
|
+
samples: SampleFrame,
|
|
86
|
+
params: CLTAbsoluteParams,
|
|
87
|
+
) -> ConvergenceResult:
|
|
88
|
+
|
|
89
|
+
target = pl.col(samples.target_col).cast(pl.Float64)
|
|
90
|
+
|
|
91
|
+
agg_df = samples.df.select(
|
|
92
|
+
[pl.len().alias("n"), target.mean().alias("mean"), target.std().alias("std")]
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
final = agg_df.row(0, named=True)
|
|
96
|
+
n = final["n"]
|
|
97
|
+
mean = final["mean"]
|
|
98
|
+
std = final["std"] if final["std"] is not None else 0.0
|
|
99
|
+
|
|
100
|
+
sem = std / (n**0.5) if n > 0 else 0.0
|
|
101
|
+
z_score = float(norm.ppf(1.0 - (1.0 - params.confidence_level) / 2.0))
|
|
102
|
+
abs_error = sem * z_score
|
|
103
|
+
|
|
104
|
+
is_converged = abs_error <= params.threshold
|
|
105
|
+
|
|
106
|
+
final_df = pl.DataFrame(
|
|
107
|
+
{
|
|
108
|
+
"n": [n],
|
|
109
|
+
"mean": [mean],
|
|
110
|
+
"std": [std],
|
|
111
|
+
"sem": [sem],
|
|
112
|
+
"abs_error": [abs_error],
|
|
113
|
+
"is_converged": [is_converged],
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return ConvergenceResult(
|
|
118
|
+
method=self.name,
|
|
119
|
+
n=int(n),
|
|
120
|
+
n_col="n",
|
|
121
|
+
estimate=float(mean) if mean is not None else None,
|
|
122
|
+
estimate_col="mean",
|
|
123
|
+
error=float(abs_error),
|
|
124
|
+
error_col="abs_error",
|
|
125
|
+
is_converged=is_converged,
|
|
126
|
+
is_converged_col="is_converged",
|
|
127
|
+
data=final_df,
|
|
128
|
+
diagnostics={
|
|
129
|
+
"threshold": params.threshold,
|
|
130
|
+
"confidence_level": params.confidence_level,
|
|
131
|
+
"z_score": z_score,
|
|
132
|
+
},
|
|
133
|
+
)
|
mocaco/protocols.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Protocol for method addition and its input data format."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Protocol
|
|
8
|
+
|
|
9
|
+
import polars as pl
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from .result import ConvergenceResult
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class SampleFrame:
|
|
21
|
+
"""
|
|
22
|
+
Normalized representation of Monte Carlo samples.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
df
|
|
27
|
+
Source Polars DataFrame.
|
|
28
|
+
it_col
|
|
29
|
+
Column containing the iteration/order of the samples.
|
|
30
|
+
target_col
|
|
31
|
+
Column containing the quantity to analyze.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
df: pl.DataFrame
|
|
35
|
+
target_col: str
|
|
36
|
+
it_col: str | None = None
|
|
37
|
+
|
|
38
|
+
def __post_init__(self) -> None:
|
|
39
|
+
"""Validate columns, assign default iteration indices if missing, and handle edge cases."""
|
|
40
|
+
# 1. Resolve and normalize it_col name
|
|
41
|
+
if self.it_col is None:
|
|
42
|
+
it_col_name = "it"
|
|
43
|
+
new_df = self.df.with_columns(pl.arange(1, len(self.df) + 1).alias(it_col_name))
|
|
44
|
+
object.__setattr__(self, "df", new_df)
|
|
45
|
+
object.__setattr__(self, "it_col", it_col_name)
|
|
46
|
+
logger.info("it_col was None — auto-created column 'it' with sequential indices.")
|
|
47
|
+
else:
|
|
48
|
+
it_col_name = self.it_col
|
|
49
|
+
|
|
50
|
+
# 2. Validate structural integrity (it_col_name is guaranteed to be str here)
|
|
51
|
+
missing = [col for col in (it_col_name, self.target_col) if col not in self.df.columns]
|
|
52
|
+
if missing:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"Columns not found in DataFrame: {missing}. Available columns: {self.df.columns}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if self.df.is_empty():
|
|
58
|
+
raise ValueError("The sample DataFrame cannot be empty.")
|
|
59
|
+
|
|
60
|
+
# 3. Validate Data Types (it_col and target_col must be numeric)
|
|
61
|
+
it_dtype = self.df.get_column(it_col_name).dtype
|
|
62
|
+
if not it_dtype.is_numeric():
|
|
63
|
+
raise TypeError(
|
|
64
|
+
f"Iteration column '{it_col_name}' must be numeric (int or float), got {it_dtype}."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
target = self.df.get_column(self.target_col)
|
|
68
|
+
if not target.dtype.is_numeric():
|
|
69
|
+
raise TypeError(
|
|
70
|
+
f"Target column '{self.target_col}' must be numeric (int or float), got {target.dtype}."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 4. Input validation and edge case handling (Null, NaN, Inf)
|
|
74
|
+
if target.is_null().any():
|
|
75
|
+
raise ValueError(f"Target column '{self.target_col}' contains Null (missing) values.")
|
|
76
|
+
|
|
77
|
+
if target.dtype in (pl.Float32, pl.Float64):
|
|
78
|
+
if target.is_nan().any():
|
|
79
|
+
raise ValueError(f"Target column '{self.target_col}' contains NaN values.")
|
|
80
|
+
if target.is_infinite().any():
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Target column '{self.target_col}' contains Infinite (Inf) values."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Criterion(Protocol):
|
|
87
|
+
"""Structural contract implemented by every convergence criterion."""
|
|
88
|
+
|
|
89
|
+
name: str
|
|
90
|
+
supports_eval_frequency: bool
|
|
91
|
+
description: str
|
|
92
|
+
params_type: type[Any]
|
|
93
|
+
|
|
94
|
+
assumptions: ClassVar[Sequence[str]]
|
|
95
|
+
limitations: ClassVar[Sequence[str]]
|
|
96
|
+
result_interpretation: str
|
|
97
|
+
example_usage: str
|
|
98
|
+
references: ClassVar[Sequence[str]]
|
|
99
|
+
|
|
100
|
+
def run(
|
|
101
|
+
self,
|
|
102
|
+
samples: SampleFrame,
|
|
103
|
+
params: Any,
|
|
104
|
+
) -> ConvergenceResult: ...
|
mocaco/registry.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Registry singleton of mocaco convergence methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, TypeVar, overload
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
|
|
14
|
+
from .protocols import Criterion
|
|
15
|
+
|
|
16
|
+
C = TypeVar("C", bound=type)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CriterionRegistry:
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._criteria: dict[str, Criterion] = {}
|
|
24
|
+
|
|
25
|
+
# Overload 1: Decorator usage (no criterion provided)
|
|
26
|
+
@overload
|
|
27
|
+
def register(self, criterion: None = None, *, overwrite: bool = False) -> Callable[[C], C]: ...
|
|
28
|
+
|
|
29
|
+
# Overload 2: Programmatic usage (criterion instance provided)
|
|
30
|
+
@overload
|
|
31
|
+
def register(self, criterion: Criterion, *, overwrite: bool = False) -> Criterion: ...
|
|
32
|
+
|
|
33
|
+
def register(
|
|
34
|
+
self,
|
|
35
|
+
criterion: Criterion | None = None,
|
|
36
|
+
*,
|
|
37
|
+
overwrite: bool = False,
|
|
38
|
+
) -> Any:
|
|
39
|
+
# 1. Programmatic use: registry.register(MyCriterion())
|
|
40
|
+
if criterion is not None:
|
|
41
|
+
name = criterion.name
|
|
42
|
+
|
|
43
|
+
if not name:
|
|
44
|
+
raise ValueError("A criterion must define a non-empty name.")
|
|
45
|
+
|
|
46
|
+
if name in self._criteria and not overwrite:
|
|
47
|
+
raise ValueError(f"Criterion {name!r} is already registered.")
|
|
48
|
+
|
|
49
|
+
self._criteria[name] = criterion
|
|
50
|
+
return criterion
|
|
51
|
+
|
|
52
|
+
# 2. Decorator use: @registry.register()
|
|
53
|
+
def wrapper(cls: C) -> C:
|
|
54
|
+
instance = cls()
|
|
55
|
+
self.register(instance, overwrite=overwrite)
|
|
56
|
+
return cls
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
def get(self, name: str) -> Criterion:
|
|
61
|
+
try:
|
|
62
|
+
return self._criteria[name]
|
|
63
|
+
except KeyError as exc:
|
|
64
|
+
available = ", ".join(self.names())
|
|
65
|
+
raise ValueError(
|
|
66
|
+
f"Unknown convergence criterion: {name!r}. Available criteria: {available}"
|
|
67
|
+
) from exc
|
|
68
|
+
|
|
69
|
+
def names(self) -> tuple[str, ...]:
|
|
70
|
+
return tuple(self._criteria.keys())
|
|
71
|
+
|
|
72
|
+
def describe(self, name: str) -> None:
|
|
73
|
+
# 1. Fetch the criterion from the registry
|
|
74
|
+
try:
|
|
75
|
+
criterion = registry.get(name)
|
|
76
|
+
except ValueError:
|
|
77
|
+
console.print(f"[bold red]Error:[/bold red] Method '{name}' not found in registry.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# 2. Print the Method Name and Main Description in a Panel
|
|
81
|
+
console.print()
|
|
82
|
+
console.print(
|
|
83
|
+
Panel(
|
|
84
|
+
f"{criterion.description}",
|
|
85
|
+
title=f"Method: [bold cyan]{name}[/bold cyan]",
|
|
86
|
+
title_align="left",
|
|
87
|
+
border_style="cyan",
|
|
88
|
+
expand=False,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# 3. Create a Table for the Parameters
|
|
93
|
+
table = Table(
|
|
94
|
+
title="Parameters",
|
|
95
|
+
title_style="bold magenta",
|
|
96
|
+
title_justify="left",
|
|
97
|
+
show_header=True,
|
|
98
|
+
header_style="bold black on white",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
table.add_column("Name", style="bold cyan", no_wrap=True)
|
|
102
|
+
table.add_column("Type", style="green")
|
|
103
|
+
table.add_column("Required", justify="center")
|
|
104
|
+
table.add_column("Default", style="yellow")
|
|
105
|
+
table.add_column("Description", style="dim")
|
|
106
|
+
|
|
107
|
+
# 4. Introspect the Pydantic V2 model fields
|
|
108
|
+
params_type = criterion.params_type
|
|
109
|
+
|
|
110
|
+
for field_name, field_info in params_type.model_fields.items():
|
|
111
|
+
# Get type name cleanly
|
|
112
|
+
field_type = getattr(field_info.annotation, "__name__", str(field_info.annotation))
|
|
113
|
+
|
|
114
|
+
is_required = field_info.is_required()
|
|
115
|
+
req_icon = "[green]✔[/green]" if is_required else "[red]✘[/red]"
|
|
116
|
+
default_val = "-" if is_required else repr(field_info.default)
|
|
117
|
+
desc = field_info.description or "No description provided."
|
|
118
|
+
|
|
119
|
+
table.add_row(field_name, field_type, req_icon, default_val, desc)
|
|
120
|
+
|
|
121
|
+
# 5. Render the table
|
|
122
|
+
console.print(table)
|
|
123
|
+
console.print()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
registry = CriterionRegistry()
|
mocaco/result.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Module to format Result output from convergence methods."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import polars as pl
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
import pandas as pd
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ResultPlotter:
|
|
19
|
+
"""Helper class to provide plotting capabilities for ConvergenceResult."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, result: ConvergenceResult) -> None:
|
|
22
|
+
self.result = result
|
|
23
|
+
|
|
24
|
+
def evolution(
|
|
25
|
+
self,
|
|
26
|
+
*,
|
|
27
|
+
show: bool = False,
|
|
28
|
+
) -> Any:
|
|
29
|
+
"""
|
|
30
|
+
Plot the evolution of the estimate, the error band, and the convergence point.
|
|
31
|
+
|
|
32
|
+
Requires `matplotlib` to be installed.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
show
|
|
37
|
+
If True, automatically displays the plot with `plt.show()` and cleans up resources.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
matplotlib.axes.Axes
|
|
42
|
+
The matplotlib Axes object containing the plot.
|
|
43
|
+
"""
|
|
44
|
+
df = self.result.data
|
|
45
|
+
|
|
46
|
+
# Ensure required columns exist
|
|
47
|
+
missing = [
|
|
48
|
+
col
|
|
49
|
+
for col in (
|
|
50
|
+
self.result.estimate_col,
|
|
51
|
+
self.result.error_col,
|
|
52
|
+
self.result.n_col,
|
|
53
|
+
self.result.is_converged_col,
|
|
54
|
+
)
|
|
55
|
+
if col not in self.result.data.columns
|
|
56
|
+
]
|
|
57
|
+
if missing:
|
|
58
|
+
raise ValueError(f"Columns not found in diagnostic data for plotting: {missing}")
|
|
59
|
+
|
|
60
|
+
# Extract arrays
|
|
61
|
+
n_vals = df.get_column(self.result.n_col).to_numpy()
|
|
62
|
+
estimate_vals = df.get_column(self.result.estimate_col).to_numpy()
|
|
63
|
+
error_vals = df.get_column(self.result.error_col).to_numpy()
|
|
64
|
+
|
|
65
|
+
fig, ax = plt.subplots(figsize=(8, 5))
|
|
66
|
+
|
|
67
|
+
# Plot estimate line
|
|
68
|
+
ax.plot(n_vals, estimate_vals, label="Estimate", color="#1f77b4", linewidth=2)
|
|
69
|
+
|
|
70
|
+
# Plot error band
|
|
71
|
+
ax.fill_between(
|
|
72
|
+
n_vals,
|
|
73
|
+
estimate_vals - error_vals,
|
|
74
|
+
estimate_vals + error_vals,
|
|
75
|
+
color="#1f77b4",
|
|
76
|
+
alpha=0.2,
|
|
77
|
+
label="Error Band",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Plot vertical line for convergence (first True occurrence)
|
|
81
|
+
converged_df = df.filter(pl.col(self.result.is_converged_col))
|
|
82
|
+
if not converged_df.is_empty():
|
|
83
|
+
first_converged_n = converged_df.get_column(self.result.n_col)[0]
|
|
84
|
+
ax.axvline(
|
|
85
|
+
x=first_converged_n,
|
|
86
|
+
color="red",
|
|
87
|
+
linestyle="--",
|
|
88
|
+
label=f"Converged at n={first_converged_n}",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
ax.set_xlabel("Number of samples (n)")
|
|
92
|
+
ax.set_ylabel("Estimate")
|
|
93
|
+
ax.set_title(f"Convergence Evolution ({self.result.method})")
|
|
94
|
+
ax.legend()
|
|
95
|
+
ax.grid(True, linestyle=":", alpha=0.7)
|
|
96
|
+
|
|
97
|
+
if show:
|
|
98
|
+
plt.show()
|
|
99
|
+
plt.close(fig)
|
|
100
|
+
|
|
101
|
+
return ax
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(frozen=True)
|
|
105
|
+
class ConvergenceResult:
|
|
106
|
+
"""
|
|
107
|
+
Common result returned by convergence criteria.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
method
|
|
112
|
+
Name of the convergence criterion.
|
|
113
|
+
n
|
|
114
|
+
Number of samples used in the final estimate.
|
|
115
|
+
n_col
|
|
116
|
+
Name of the n column set on the method data output
|
|
117
|
+
estimate
|
|
118
|
+
Final estimate of the target quantity.
|
|
119
|
+
estimate_col
|
|
120
|
+
Name of the estimate column set on the method data output
|
|
121
|
+
error
|
|
122
|
+
Final estimated error.
|
|
123
|
+
error_col
|
|
124
|
+
Name of the error column set on the method data output
|
|
125
|
+
is_converged
|
|
126
|
+
Whether the final evaluated state satisfies the criterion.
|
|
127
|
+
is_converged_col
|
|
128
|
+
Name of the is_converged column set on the method data output
|
|
129
|
+
data
|
|
130
|
+
Full diagnostic DataFrame generated by the method/wrapper.
|
|
131
|
+
diagnostics
|
|
132
|
+
Method-specific diagnostic information.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
# Common outputs to all criterions
|
|
136
|
+
method: str
|
|
137
|
+
n: int
|
|
138
|
+
n_col: str
|
|
139
|
+
estimate: float | None
|
|
140
|
+
estimate_col: str
|
|
141
|
+
error: float | None
|
|
142
|
+
error_col: str
|
|
143
|
+
is_converged: bool | None
|
|
144
|
+
is_converged_col: str
|
|
145
|
+
|
|
146
|
+
# Build by Wrapper or by the criterion default
|
|
147
|
+
data: pl.DataFrame
|
|
148
|
+
|
|
149
|
+
# Method-specific outputs
|
|
150
|
+
diagnostics: dict[str, Any] = field(default_factory=dict)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def plot(self) -> ResultPlotter:
|
|
154
|
+
"""Access the plotting API for this result."""
|
|
155
|
+
return ResultPlotter(self)
|
|
156
|
+
|
|
157
|
+
def to_polars(self) -> pl.DataFrame:
|
|
158
|
+
"""Return the complete diagnostic DataFrame as a Polars DataFrame."""
|
|
159
|
+
return self.data
|
|
160
|
+
|
|
161
|
+
def to_pandas(self) -> pd.DataFrame:
|
|
162
|
+
"""Return the complete diagnostic DataFrame as a Pandas DataFrame."""
|
|
163
|
+
return self.data.to_pandas()
|
|
164
|
+
|
|
165
|
+
def summary(self) -> None:
|
|
166
|
+
"""Print a rich formatted summary of the convergence result to the console."""
|
|
167
|
+
console = Console()
|
|
168
|
+
|
|
169
|
+
# Define status colors and icons
|
|
170
|
+
status_color = "green" if self.is_converged else "yellow"
|
|
171
|
+
status_icon = "✔" if self.is_converged else "⚠"
|
|
172
|
+
status_text = "Converged" if self.is_converged else "Not Converged"
|
|
173
|
+
|
|
174
|
+
# Create the inner layout table
|
|
175
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
176
|
+
table.add_row("[bold]Method:[/bold]", f"[cyan]{self.method}[/cyan]")
|
|
177
|
+
table.add_row(
|
|
178
|
+
"[bold]Status:[/bold]", f"[{status_color}]{status_icon} {status_text}[/{status_color}]"
|
|
179
|
+
)
|
|
180
|
+
table.add_row("[bold]Samples (n):[/bold]", str(self.n))
|
|
181
|
+
est_str = f"{self.estimate:.6g}" if self.estimate is not None else "-"
|
|
182
|
+
table.add_row("[bold]Estimate:[/bold]", est_str)
|
|
183
|
+
err_str = f"{self.error:.6g}" if self.error is not None else "-"
|
|
184
|
+
table.add_row("[bold]Error:[/bold]", err_str)
|
|
185
|
+
|
|
186
|
+
# Append diagnostics dynamically if they exist
|
|
187
|
+
if self.diagnostics:
|
|
188
|
+
table.add_row("", "") # Spacer
|
|
189
|
+
table.add_row("[bold italic]Diagnostics:[/bold italic]", "")
|
|
190
|
+
for key, value in self.diagnostics.items():
|
|
191
|
+
# Format floats cleanly, otherwise just cast to str
|
|
192
|
+
val_str = f"{value:.6g}" if isinstance(value, float) else str(value)
|
|
193
|
+
table.add_row(f" {key}:", val_str)
|
|
194
|
+
|
|
195
|
+
# Wrap in a stylized panel
|
|
196
|
+
panel = Panel(
|
|
197
|
+
table,
|
|
198
|
+
title="[bold]Convergence Summary[/bold]",
|
|
199
|
+
title_align="left",
|
|
200
|
+
border_style=status_color,
|
|
201
|
+
expand=False,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
console.print()
|
|
205
|
+
console.print(panel)
|
|
206
|
+
console.print()
|
|
207
|
+
|
|
208
|
+
def __str__(self) -> str:
|
|
209
|
+
"""Return the string representation of the underlying Polars DataFrame."""
|
|
210
|
+
return str(self.data)
|
|
211
|
+
|
|
212
|
+
def __repr__(self) -> str:
|
|
213
|
+
"""Return a compact string representation of the result summary."""
|
|
214
|
+
return (
|
|
215
|
+
f"ConvergenceResult("
|
|
216
|
+
f"method={self.method!r}, "
|
|
217
|
+
f"n={self.n!r}"
|
|
218
|
+
f"estimate={self.estimate!r}, "
|
|
219
|
+
f"error={self.error!r}, "
|
|
220
|
+
f"is_converged={self.is_converged!r}, "
|
|
221
|
+
f")"
|
|
222
|
+
)
|
mocaco/wrappers.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Criterion wrappers to expand evaluation behaviors."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
from .protocols import SampleFrame
|
|
10
|
+
from .result import ConvergenceResult
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
from .protocols import Criterion
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CriterionWrapper:
|
|
19
|
+
"""
|
|
20
|
+
Base class for criterion wrappers.
|
|
21
|
+
|
|
22
|
+
A wrapper intercepts calls to a criterion's ``run()`` method to add
|
|
23
|
+
cross-cutting behaviour (e.g. periodic evaluation, caching, logging)
|
|
24
|
+
without modifying the criterion itself.
|
|
25
|
+
|
|
26
|
+
All metadata attributes are delegated to the inner criterion so that
|
|
27
|
+
the wrapper satisfies the same ``Criterion`` protocol.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, criterion: Criterion) -> None:
|
|
31
|
+
self._criterion = criterion
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return self._criterion.name
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def description(self) -> str:
|
|
39
|
+
return self._criterion.description
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def params_type(self) -> type[Any]:
|
|
43
|
+
return self._criterion.params_type
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def assumptions(self) -> Sequence[str]:
|
|
47
|
+
return self._criterion.assumptions
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def limitations(self) -> Sequence[str]:
|
|
51
|
+
return self._criterion.limitations
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def result_interpretation(self) -> str:
|
|
55
|
+
return self._criterion.result_interpretation
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def example_usage(self) -> str:
|
|
59
|
+
return self._criterion.example_usage
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def references(self) -> Sequence[str]:
|
|
63
|
+
return self._criterion.references
|
|
64
|
+
|
|
65
|
+
def run(
|
|
66
|
+
self,
|
|
67
|
+
samples: SampleFrame,
|
|
68
|
+
params: Any,
|
|
69
|
+
) -> ConvergenceResult:
|
|
70
|
+
return self._criterion.run(samples, params)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class EvalFrequencyWrapper(CriterionWrapper):
|
|
74
|
+
"""
|
|
75
|
+
Wrapper that evaluates a criterion at regular iteration intervals.
|
|
76
|
+
|
|
77
|
+
Instead of running the criterion once on the full dataset, this wrapper
|
|
78
|
+
runs it on cumulative slices of the data at every ``eval_frequency``
|
|
79
|
+
rows. The diagnostic DataFrames from each evaluation are concatenated
|
|
80
|
+
into a single DataFrame, and the final ``ConvergenceResult`` carries the
|
|
81
|
+
last run's summary fields combined with the concatenated history.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
criterion
|
|
86
|
+
The inner criterion to wrap.
|
|
87
|
+
eval_frequency
|
|
88
|
+
Number of rows between successive evaluations. Must be a
|
|
89
|
+
positive integer.
|
|
90
|
+
|
|
91
|
+
Examples
|
|
92
|
+
--------
|
|
93
|
+
>>> wrapper = EvalFrequencyWrapper(criterion, eval_frequency=10)
|
|
94
|
+
>>> result = wrapper.run(samples, params)
|
|
95
|
+
>>> result.data # diagnostic history across all evaluations
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, criterion: Criterion, eval_frequency: int) -> None:
|
|
99
|
+
super().__init__(criterion)
|
|
100
|
+
if eval_frequency <= 0:
|
|
101
|
+
raise ValueError(f"eval_frequency must be a positive integer, got {eval_frequency}")
|
|
102
|
+
self._eval_frequency = eval_frequency
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def eval_frequency(self) -> int:
|
|
106
|
+
return self._eval_frequency
|
|
107
|
+
|
|
108
|
+
def run(
|
|
109
|
+
self,
|
|
110
|
+
samples: SampleFrame,
|
|
111
|
+
params: Any,
|
|
112
|
+
) -> ConvergenceResult:
|
|
113
|
+
total_rows = len(samples.df)
|
|
114
|
+
|
|
115
|
+
if self._eval_frequency >= total_rows:
|
|
116
|
+
result = self._criterion.run(samples, params)
|
|
117
|
+
return ConvergenceResult(
|
|
118
|
+
method=result.method,
|
|
119
|
+
n=result.n,
|
|
120
|
+
n_col=result.n_col,
|
|
121
|
+
estimate=result.estimate,
|
|
122
|
+
estimate_col=result.estimate_col,
|
|
123
|
+
error=result.error,
|
|
124
|
+
error_col=result.error_col,
|
|
125
|
+
is_converged=result.is_converged,
|
|
126
|
+
is_converged_col=result.is_converged_col,
|
|
127
|
+
data=result.data,
|
|
128
|
+
diagnostics={
|
|
129
|
+
**result.diagnostics,
|
|
130
|
+
"eval_frequency": self._eval_frequency,
|
|
131
|
+
"n_evaluations": 1,
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
eval_points = list(range(self._eval_frequency, total_rows + 1, self._eval_frequency))
|
|
136
|
+
if eval_points[-1] != total_rows:
|
|
137
|
+
eval_points.append(total_rows)
|
|
138
|
+
|
|
139
|
+
results: list[ConvergenceResult] = []
|
|
140
|
+
for point in eval_points:
|
|
141
|
+
sliced_df = samples.df.head(point)
|
|
142
|
+
sliced_samples = SampleFrame(
|
|
143
|
+
df=sliced_df,
|
|
144
|
+
target_col=samples.target_col,
|
|
145
|
+
it_col=samples.it_col,
|
|
146
|
+
)
|
|
147
|
+
result = self._criterion.run(sliced_samples, params)
|
|
148
|
+
results.append(result)
|
|
149
|
+
|
|
150
|
+
data_frames = [r.data for r in results]
|
|
151
|
+
combined_data = pl.concat(data_frames, how="align")
|
|
152
|
+
|
|
153
|
+
final = results[-1]
|
|
154
|
+
return ConvergenceResult(
|
|
155
|
+
method=final.method,
|
|
156
|
+
n=final.n,
|
|
157
|
+
n_col=final.n_col,
|
|
158
|
+
estimate=final.estimate,
|
|
159
|
+
estimate_col=final.estimate_col,
|
|
160
|
+
error=final.error,
|
|
161
|
+
error_col=final.error_col,
|
|
162
|
+
is_converged=final.is_converged,
|
|
163
|
+
is_converged_col=final.is_converged_col,
|
|
164
|
+
data=combined_data,
|
|
165
|
+
diagnostics={
|
|
166
|
+
**final.diagnostics,
|
|
167
|
+
"eval_frequency": self._eval_frequency,
|
|
168
|
+
"n_evaluations": len(results),
|
|
169
|
+
},
|
|
170
|
+
)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mocaco
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convergence analysis for Monte Carlo series — works with Polars DataFrames
|
|
5
|
+
Author-email: Álvaro Esteban Muñoz <em.alvaro.l@gmail.com>
|
|
6
|
+
License: LGPL-3.0-or-later
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Requires-Dist: matplotlib>=3.11.2
|
|
10
|
+
Requires-Dist: numpy>=2.5.3
|
|
11
|
+
Requires-Dist: pandas>=3.0.6
|
|
12
|
+
Requires-Dist: polars>=1.44.2
|
|
13
|
+
Requires-Dist: pyarrow>=25.0.1
|
|
14
|
+
Requires-Dist: pydantic>=2.13.5
|
|
15
|
+
Requires-Dist: rich>=15.0.0
|
|
16
|
+
Requires-Dist: scipy>=1.18.1
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# mocaco
|
|
20
|
+
|
|
21
|
+
[](https://github.com/emalvarol/mocaco/actions/workflows/ci.yml)
|
|
22
|
+

|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
Convergence analysis for Monte Carlo series using Polars DataFrames.
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="assets/Logo_v1.png" alt="mocaco logo" width="400"/>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
The library provides a simple API for applying different convergence and
|
|
32
|
+
stopping criteria to Monte Carlo simulation results.
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import polars as pl
|
|
38
|
+
import mocaco as mcc
|
|
39
|
+
|
|
40
|
+
# Prepare your Monte Carlo samples
|
|
41
|
+
df = pl.DataFrame({"it": range(1000), "value": your_samples})
|
|
42
|
+
|
|
43
|
+
# Create a normalized sample frame
|
|
44
|
+
samples = mcc.samples(df, it_col="it", target_col="value")
|
|
45
|
+
|
|
46
|
+
# Run a convergence criterion
|
|
47
|
+
result = mcc.convergence.clt_absolute(samples, threshold=0.01)
|
|
48
|
+
|
|
49
|
+
# Inspect results
|
|
50
|
+
print(result.is_converged) # True/False
|
|
51
|
+
print(result.estimate) # Final cumulative mean
|
|
52
|
+
print(result.error) # Final CLT-based absolute error
|
|
53
|
+
print(result.n) # Number of samples
|
|
54
|
+
# and more...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## API Overview
|
|
58
|
+
|
|
59
|
+
| Function | Description |
|
|
60
|
+
|----------|-------------|
|
|
61
|
+
| `mcc.samples(df, it_col, target_col)` | Create a normalized sample frame from a Polars DataFrame |
|
|
62
|
+
| `mcc.convergence(samples, method="...")` | Generic invocation of any registered criterion |
|
|
63
|
+
| `mcc.convergence.clt_absolute(samples, ...)` | Typed invocation of a specific criterion |
|
|
64
|
+
| `mcc.methods()` | List all registered convergence criteria |
|
|
65
|
+
| `mcc.describe("clt_absolute")` | Get metadata and parameter info for a criterion |
|
|
66
|
+
|
|
67
|
+
## Available Methods
|
|
68
|
+
|
|
69
|
+
- [`clt_absolute`](docs/methods/clt_absolute.md) — CLT-based absolute error threshold
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
|
|
73
|
+
Full documentation is available in the `docs/` directory or can be built with:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
mkdocs serve
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Installation (Developers)
|
|
80
|
+
|
|
81
|
+
### Environment
|
|
82
|
+
|
|
83
|
+
Create the virtual environment and install the project in editable mode with `uv`:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
uv venv
|
|
87
|
+
uv sync
|
|
88
|
+
uv pip install -e .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If it is needed to remove an old environment:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
Remove-Item -Recurse -Force .venv
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### ruff
|
|
98
|
+
```bash
|
|
99
|
+
uv run ruff check --fix . && uv run ruff format .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### mypy
|
|
103
|
+
```bash
|
|
104
|
+
uv run mypy src/
|
|
105
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
mocaco/__init__.py,sha256=tWfOIMSLYMu9gEYcOqpgn9z2MBvi48vSHzyg4oNhDCA,182
|
|
2
|
+
mocaco/api.py,sha256=LEQmSmDVupbUY6A4eKn6AiaPqCRusZ9bLGk8GGf019E,3389
|
|
3
|
+
mocaco/api.pyi,sha256=gTxbV3lDQWn-aObe5CWWn4cQuzigJGAjsUMylgeSiPE,1165
|
|
4
|
+
mocaco/protocols.py,sha256=aSYD-ij3sXFZGvbn7YbSmK3XjTohujHgAkh8RsckgE0,3431
|
|
5
|
+
mocaco/registry.py,sha256=7BzJVuhfihAi5wdihQouR4_mXiF8MOp-oQ2y2C5DLh4,4036
|
|
6
|
+
mocaco/result.py,sha256=qdZ9Mu4OR-W-JZiQqRin2S4jRED6gZji5hRu4i_UIaQ,7068
|
|
7
|
+
mocaco/wrappers.py,sha256=G97_DmlcPlAryxWAjn1DmwyEIS7BN9CtX-6lfjhTIbE,5262
|
|
8
|
+
mocaco/methods/__init__.py,sha256=3Npj5Yu67T8vlTcfxNqwGbtSx2V3mmG0RSHoLFGQCz8,216
|
|
9
|
+
mocaco/methods/clt_absolute.py,sha256=pkcEj8UhwFxdKVyjitXACmF9VVWGZ3K4Blrzy1-jxzE,4195
|
|
10
|
+
mocaco-0.1.0.dist-info/METADATA,sha256=XqCjH_H7bXpOSWwyzvWXvESS4aibiO7OFvjYiu_T1uo,2841
|
|
11
|
+
mocaco-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
12
|
+
mocaco-0.1.0.dist-info/licenses/LICENSE,sha256=pWgb-bBdsU2Gd2kwAXxketnm5W_2u8_fIeWEgojfrxs,7651
|
|
13
|
+
mocaco-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
This version of the GNU Lesser General Public License incorporates
|
|
10
|
+
the terms and conditions of version 3 of the GNU General Public
|
|
11
|
+
License, supplemented by the additional permissions listed below.
|
|
12
|
+
|
|
13
|
+
0. Additional Definitions.
|
|
14
|
+
|
|
15
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
16
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
|
17
|
+
General Public License.
|
|
18
|
+
|
|
19
|
+
"The Library" refers to a covered work governed by this License,
|
|
20
|
+
other than an Application or a Combined Work as defined below.
|
|
21
|
+
|
|
22
|
+
An "Application" is any work that makes use of an interface provided
|
|
23
|
+
by the Library, but which is not otherwise based on the Library.
|
|
24
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
|
25
|
+
of using an interface provided by the Library.
|
|
26
|
+
|
|
27
|
+
A "Combined Work" is a work produced by combining or linking an
|
|
28
|
+
Application with the Library. The particular version of the Library
|
|
29
|
+
with which the Combined Work was made is also called the "Linked
|
|
30
|
+
Version".
|
|
31
|
+
|
|
32
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
|
33
|
+
Corresponding Source for the Combined Work, excluding any source code
|
|
34
|
+
for portions of the Combined Work that, considered in isolation, are
|
|
35
|
+
based on the Application, and not on the Linked Version.
|
|
36
|
+
|
|
37
|
+
The "Corresponding Application Code" for a Combined Work means the
|
|
38
|
+
object code and/or source code for the Application, including any data
|
|
39
|
+
and utility programs needed for reproducing the Combined Work from the
|
|
40
|
+
Application, but excluding the System Libraries of the Combined Work.
|
|
41
|
+
|
|
42
|
+
1. Exception to Section 3 of the GNU GPL.
|
|
43
|
+
|
|
44
|
+
You may convey a covered work under sections 3 and 4 of this License
|
|
45
|
+
without being bound by section 3 of the GNU GPL.
|
|
46
|
+
|
|
47
|
+
2. Conveying Modified Versions.
|
|
48
|
+
|
|
49
|
+
If you modify a copy of the Library, and, in your modifications, a
|
|
50
|
+
facility refers to a function or data to be supplied by an Application
|
|
51
|
+
that uses the facility (other than as an argument passed when the
|
|
52
|
+
facility is invoked), then you may convey a copy of the modified
|
|
53
|
+
version:
|
|
54
|
+
|
|
55
|
+
a) under this License, provided that you make a good faith effort to
|
|
56
|
+
ensure that, in the event an Application does not supply the
|
|
57
|
+
function or data, the facility still operates, and performs
|
|
58
|
+
whatever part of its purpose remains meaningful, or
|
|
59
|
+
|
|
60
|
+
b) under the GNU GPL, with none of the additional permissions of
|
|
61
|
+
this License applicable to that copy.
|
|
62
|
+
|
|
63
|
+
3. Object Code Incorporating Material from Library Header Files.
|
|
64
|
+
|
|
65
|
+
The object code form of an Application may incorporate material from
|
|
66
|
+
a header file that is part of the Library. You may convey such object
|
|
67
|
+
code under terms of your choice, provided that, if the incorporated
|
|
68
|
+
material is not limited to numerical parameters, data structure
|
|
69
|
+
layouts and accessors, or small macros, inline functions and templates
|
|
70
|
+
(ten or fewer lines in length), you do both of the following:
|
|
71
|
+
|
|
72
|
+
a) Give prominent notice with each copy of the object code that the
|
|
73
|
+
Library is used in it and that the Library and its use are
|
|
74
|
+
covered by this License.
|
|
75
|
+
|
|
76
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
|
77
|
+
document.
|
|
78
|
+
|
|
79
|
+
4. Combined Works.
|
|
80
|
+
|
|
81
|
+
You may convey a Combined Work under terms of your choice that,
|
|
82
|
+
taken together, effectively do not restrict modification of the
|
|
83
|
+
portions of the Library contained in the Combined Work and reverse
|
|
84
|
+
engineering for debugging such modifications, if you also do each of
|
|
85
|
+
the following:
|
|
86
|
+
|
|
87
|
+
a) Give prominent notice with each copy of the Combined Work that
|
|
88
|
+
the Library is used in it and that the Library and its use are
|
|
89
|
+
covered by this License.
|
|
90
|
+
|
|
91
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
|
92
|
+
document.
|
|
93
|
+
|
|
94
|
+
c) For a Combined Work that displays copyright notices during
|
|
95
|
+
execution, include the copyright notice for the Library among
|
|
96
|
+
these notices, as well as a reference directing the user to the
|
|
97
|
+
copies of the GNU GPL and this license document.
|
|
98
|
+
|
|
99
|
+
d) Do one of the following:
|
|
100
|
+
|
|
101
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
|
102
|
+
License, and the Corresponding Application Code in a form
|
|
103
|
+
suitable for, and under terms that permit, the user to
|
|
104
|
+
recombine or relink the Application with a modified version of
|
|
105
|
+
the Linked Version to produce a modified Combined Work, in the
|
|
106
|
+
manner specified by section 6 of the GNU GPL for conveying
|
|
107
|
+
Corresponding Source.
|
|
108
|
+
|
|
109
|
+
1) Use a suitable shared library mechanism for linking with the
|
|
110
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
|
111
|
+
a copy of the Library already present on the user's computer
|
|
112
|
+
system, and (b) will operate properly with a modified version
|
|
113
|
+
of the Library that is interface-compatible with the Linked
|
|
114
|
+
Version.
|
|
115
|
+
|
|
116
|
+
e) Provide Installation Information, but only if you would otherwise
|
|
117
|
+
be required to provide such information under section 6 of the
|
|
118
|
+
GNU GPL, and only to the extent that such information is
|
|
119
|
+
necessary to install and execute a modified version of the
|
|
120
|
+
Combined Work produced by recombining or relinking the
|
|
121
|
+
Application with a modified version of the Linked Version. (If
|
|
122
|
+
you use option 4d0, the Installation Information must accompany
|
|
123
|
+
the Minimal Corresponding Source and Corresponding Application
|
|
124
|
+
Code. If you use option 4d1, you must provide the Installation
|
|
125
|
+
Information in the manner specified by section 6 of the GNU GPL
|
|
126
|
+
for conveying Corresponding Source.)
|
|
127
|
+
|
|
128
|
+
5. Combined Libraries.
|
|
129
|
+
|
|
130
|
+
You may place library facilities that are a work based on the
|
|
131
|
+
Library side by side in a single library together with other library
|
|
132
|
+
facilities that are not Applications and are not covered by this
|
|
133
|
+
License, and convey such a combined library under terms of your
|
|
134
|
+
choice, if you do both of the following:
|
|
135
|
+
|
|
136
|
+
a) Accompany the combined library with a copy of the same work based
|
|
137
|
+
on the Library, uncombined with any other library facilities,
|
|
138
|
+
conveyed under the terms of this License.
|
|
139
|
+
|
|
140
|
+
b) Give prominent notice with the combined library that part of it
|
|
141
|
+
is a work based on the Library, and explaining where to find the
|
|
142
|
+
accompanying uncombined form of the same work.
|
|
143
|
+
|
|
144
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
|
145
|
+
|
|
146
|
+
The Free Software Foundation may publish revised and/or new versions
|
|
147
|
+
of the GNU Lesser General Public License from time to time. Such new
|
|
148
|
+
versions will be similar in spirit to the present version, but may
|
|
149
|
+
differ in detail to address new problems or concerns.
|
|
150
|
+
|
|
151
|
+
Each version is given a distinguishing version number. If the
|
|
152
|
+
Library as you received it specifies that a certain numbered version
|
|
153
|
+
of the GNU Lesser General Public License "or any later version"
|
|
154
|
+
applies to it, you have the option of following the terms and
|
|
155
|
+
conditions either of that published version or of any later version
|
|
156
|
+
published by the Free Software Foundation. If the Library as you
|
|
157
|
+
received it does not specify a version number of the GNU Lesser
|
|
158
|
+
General Public License, you may choose any version of the GNU Lesser
|
|
159
|
+
General Public License ever published by the Free Software Foundation.
|
|
160
|
+
|
|
161
|
+
If the Library as you received it specifies that a proxy can decide
|
|
162
|
+
whether future versions of the GNU Lesser General Public License shall
|
|
163
|
+
apply, that proxy's public statement of acceptance of any version is
|
|
164
|
+
permanent authorization for you to choose that version for the
|
|
165
|
+
Library.
|