pyAgrum-nightly 2.3.1.9.dev202512261765915415__cp310-abi3-macosx_10_15_x86_64.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.
- pyagrum/__init__.py +165 -0
- pyagrum/_pyagrum.so +0 -0
- pyagrum/bnmixture/BNMInference.py +268 -0
- pyagrum/bnmixture/BNMLearning.py +376 -0
- pyagrum/bnmixture/BNMixture.py +464 -0
- pyagrum/bnmixture/__init__.py +60 -0
- pyagrum/bnmixture/notebook.py +1058 -0
- pyagrum/causal/_CausalFormula.py +280 -0
- pyagrum/causal/_CausalModel.py +436 -0
- pyagrum/causal/__init__.py +81 -0
- pyagrum/causal/_causalImpact.py +356 -0
- pyagrum/causal/_dSeparation.py +598 -0
- pyagrum/causal/_doAST.py +761 -0
- pyagrum/causal/_doCalculus.py +361 -0
- pyagrum/causal/_doorCriteria.py +374 -0
- pyagrum/causal/_exceptions.py +95 -0
- pyagrum/causal/_types.py +61 -0
- pyagrum/causal/causalEffectEstimation/_CausalEffectEstimation.py +1175 -0
- pyagrum/causal/causalEffectEstimation/_IVEstimators.py +718 -0
- pyagrum/causal/causalEffectEstimation/_RCTEstimators.py +132 -0
- pyagrum/causal/causalEffectEstimation/__init__.py +46 -0
- pyagrum/causal/causalEffectEstimation/_backdoorEstimators.py +774 -0
- pyagrum/causal/causalEffectEstimation/_causalBNEstimator.py +324 -0
- pyagrum/causal/causalEffectEstimation/_frontdoorEstimators.py +396 -0
- pyagrum/causal/causalEffectEstimation/_learners.py +118 -0
- pyagrum/causal/causalEffectEstimation/_utils.py +466 -0
- pyagrum/causal/notebook.py +172 -0
- pyagrum/clg/CLG.py +658 -0
- pyagrum/clg/GaussianVariable.py +111 -0
- pyagrum/clg/SEM.py +312 -0
- pyagrum/clg/__init__.py +63 -0
- pyagrum/clg/canonicalForm.py +408 -0
- pyagrum/clg/constants.py +54 -0
- pyagrum/clg/forwardSampling.py +202 -0
- pyagrum/clg/learning.py +776 -0
- pyagrum/clg/notebook.py +480 -0
- pyagrum/clg/variableElimination.py +271 -0
- pyagrum/common.py +60 -0
- pyagrum/config.py +319 -0
- pyagrum/ctbn/CIM.py +513 -0
- pyagrum/ctbn/CTBN.py +573 -0
- pyagrum/ctbn/CTBNGenerator.py +216 -0
- pyagrum/ctbn/CTBNInference.py +459 -0
- pyagrum/ctbn/CTBNLearner.py +161 -0
- pyagrum/ctbn/SamplesStats.py +671 -0
- pyagrum/ctbn/StatsIndepTest.py +355 -0
- pyagrum/ctbn/__init__.py +79 -0
- pyagrum/ctbn/constants.py +54 -0
- pyagrum/ctbn/notebook.py +264 -0
- pyagrum/defaults.ini +199 -0
- pyagrum/deprecated.py +95 -0
- pyagrum/explain/_ComputationCausal.py +75 -0
- pyagrum/explain/_ComputationConditional.py +48 -0
- pyagrum/explain/_ComputationMarginal.py +48 -0
- pyagrum/explain/_CustomShapleyCache.py +110 -0
- pyagrum/explain/_Explainer.py +176 -0
- pyagrum/explain/_Explanation.py +70 -0
- pyagrum/explain/_FIFOCache.py +54 -0
- pyagrum/explain/_ShallCausalValues.py +204 -0
- pyagrum/explain/_ShallConditionalValues.py +155 -0
- pyagrum/explain/_ShallMarginalValues.py +155 -0
- pyagrum/explain/_ShallValues.py +296 -0
- pyagrum/explain/_ShapCausalValues.py +208 -0
- pyagrum/explain/_ShapConditionalValues.py +126 -0
- pyagrum/explain/_ShapMarginalValues.py +191 -0
- pyagrum/explain/_ShapleyValues.py +298 -0
- pyagrum/explain/__init__.py +81 -0
- pyagrum/explain/_explGeneralizedMarkovBlanket.py +152 -0
- pyagrum/explain/_explIndependenceListForPairs.py +146 -0
- pyagrum/explain/_explInformationGraph.py +264 -0
- pyagrum/explain/notebook/__init__.py +54 -0
- pyagrum/explain/notebook/_bar.py +142 -0
- pyagrum/explain/notebook/_beeswarm.py +174 -0
- pyagrum/explain/notebook/_showShapValues.py +97 -0
- pyagrum/explain/notebook/_waterfall.py +220 -0
- pyagrum/explain/shapley.py +225 -0
- pyagrum/lib/__init__.py +46 -0
- pyagrum/lib/_colors.py +390 -0
- pyagrum/lib/bn2graph.py +299 -0
- pyagrum/lib/bn2roc.py +1026 -0
- pyagrum/lib/bn2scores.py +217 -0
- pyagrum/lib/bn_vs_bn.py +605 -0
- pyagrum/lib/cn2graph.py +305 -0
- pyagrum/lib/discreteTypeProcessor.py +1102 -0
- pyagrum/lib/discretizer.py +58 -0
- pyagrum/lib/dynamicBN.py +390 -0
- pyagrum/lib/explain.py +57 -0
- pyagrum/lib/export.py +84 -0
- pyagrum/lib/id2graph.py +258 -0
- pyagrum/lib/image.py +387 -0
- pyagrum/lib/ipython.py +307 -0
- pyagrum/lib/mrf2graph.py +471 -0
- pyagrum/lib/notebook.py +1821 -0
- pyagrum/lib/proba_histogram.py +552 -0
- pyagrum/lib/utils.py +138 -0
- pyagrum/pyagrum.py +31495 -0
- pyagrum/skbn/_MBCalcul.py +242 -0
- pyagrum/skbn/__init__.py +49 -0
- pyagrum/skbn/_learningMethods.py +282 -0
- pyagrum/skbn/_utils.py +297 -0
- pyagrum/skbn/bnclassifier.py +1014 -0
- pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/LICENSE.md +12 -0
- pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/LICENSES/LGPL-3.0-or-later.txt +304 -0
- pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/LICENSES/MIT.txt +18 -0
- pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/METADATA +145 -0
- pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/RECORD +107 -0
- pyagrum_nightly-2.3.1.9.dev202512261765915415.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
############################################################################
|
|
2
|
+
# This file is part of the aGrUM/pyAgrum library. #
|
|
3
|
+
# #
|
|
4
|
+
# Copyright (c) 2005-2025 by #
|
|
5
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
6
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
7
|
+
# #
|
|
8
|
+
# The aGrUM/pyAgrum library is free software; you can redistribute it #
|
|
9
|
+
# and/or modify it under the terms of either : #
|
|
10
|
+
# #
|
|
11
|
+
# - the GNU Lesser General Public License as published by #
|
|
12
|
+
# the Free Software Foundation, either version 3 of the License, #
|
|
13
|
+
# or (at your option) any later version, #
|
|
14
|
+
# - the MIT license (MIT), #
|
|
15
|
+
# - or both in dual license, as here. #
|
|
16
|
+
# #
|
|
17
|
+
# (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) #
|
|
18
|
+
# #
|
|
19
|
+
# This aGrUM/pyAgrum library is distributed in the hope that it will be #
|
|
20
|
+
# useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #
|
|
21
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS #
|
|
22
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
|
|
23
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
|
|
24
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
|
|
25
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
|
|
26
|
+
# OTHER DEALINGS IN THE SOFTWARE. #
|
|
27
|
+
# #
|
|
28
|
+
# See LICENCES for more details. #
|
|
29
|
+
# #
|
|
30
|
+
# SPDX-FileCopyrightText: Copyright 2005-2025 #
|
|
31
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
32
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
33
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later OR MIT #
|
|
34
|
+
# #
|
|
35
|
+
# Contact : info_at_agrum_dot_org #
|
|
36
|
+
# homepage : http://agrum.gitlab.io #
|
|
37
|
+
# gitlab : https://gitlab.com/agrumery/agrum #
|
|
38
|
+
# #
|
|
39
|
+
############################################################################
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
This module implements the inference algorithms for CLG.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
import math
|
|
46
|
+
|
|
47
|
+
import numpy as np
|
|
48
|
+
from pyagrum import JunctionTreeGenerator
|
|
49
|
+
from .canonicalForm import CanonicalForm
|
|
50
|
+
from .GaussianVariable import GaussianVariable
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CLGVariableElimination:
|
|
54
|
+
def __init__(self, clg):
|
|
55
|
+
self._clg = clg
|
|
56
|
+
self._evidence = {}
|
|
57
|
+
self._cf_dict = self._constructCanonicalForms()
|
|
58
|
+
|
|
59
|
+
def updateEvidence(self, evidence):
|
|
60
|
+
"""
|
|
61
|
+
Update evidences.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
evidence : dict
|
|
66
|
+
A dictionary of evidence.
|
|
67
|
+
"""
|
|
68
|
+
self._evidence.update(evidence)
|
|
69
|
+
|
|
70
|
+
def hasEvidence(self, variable):
|
|
71
|
+
"""
|
|
72
|
+
Check if a variable has an evidence.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
variable : str
|
|
77
|
+
The variable name.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
bool
|
|
82
|
+
True if the variable has an evidence, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
return variable in self._evidence
|
|
85
|
+
|
|
86
|
+
def eraseEvidence(self, variable):
|
|
87
|
+
"""
|
|
88
|
+
Remove the evidence corresponding to the variable name.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
variable : str
|
|
93
|
+
The variable name.
|
|
94
|
+
"""
|
|
95
|
+
del self._evidence[variable]
|
|
96
|
+
|
|
97
|
+
def eraseAllEvidence(self):
|
|
98
|
+
"""
|
|
99
|
+
Remove all the evidences.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
variable : str
|
|
104
|
+
The variable name.
|
|
105
|
+
"""
|
|
106
|
+
self._evidence = {}
|
|
107
|
+
|
|
108
|
+
def nbrEvidence(self):
|
|
109
|
+
"""
|
|
110
|
+
Returns the number of evidence.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
int
|
|
115
|
+
The number of evidence.
|
|
116
|
+
"""
|
|
117
|
+
return len(self._evidence)
|
|
118
|
+
|
|
119
|
+
def canonicalPosterior(self, variables, normalized=True):
|
|
120
|
+
"""
|
|
121
|
+
Returns the posterior density as a canonical form.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
variables : list
|
|
126
|
+
The list of target variable names.
|
|
127
|
+
normalized : bool, optional
|
|
128
|
+
Optional parameter to normalize the returned canonical form.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
CanonicalForm
|
|
133
|
+
The posterior canonical form.
|
|
134
|
+
"""
|
|
135
|
+
if type(variables) is not list:
|
|
136
|
+
variables = [variables]
|
|
137
|
+
|
|
138
|
+
for l in variables:
|
|
139
|
+
if l in self._evidence:
|
|
140
|
+
raise ValueError(f"The variable {l} is observed.")
|
|
141
|
+
|
|
142
|
+
# Converting list of name to list of ids
|
|
143
|
+
variables = [self._clg._name2id[v] for v in variables]
|
|
144
|
+
|
|
145
|
+
# Finding the elimination order
|
|
146
|
+
jtg = JunctionTreeGenerator()
|
|
147
|
+
elimination_order = jtg.eliminationOrder(self._clg._graph)
|
|
148
|
+
|
|
149
|
+
# Removing posterior variables from elimination order
|
|
150
|
+
elimination_order_removed = []
|
|
151
|
+
elimination_order_kept = []
|
|
152
|
+
for var in elimination_order:
|
|
153
|
+
if var not in variables:
|
|
154
|
+
elimination_order_removed.append(var)
|
|
155
|
+
else:
|
|
156
|
+
elimination_order_kept.append(var)
|
|
157
|
+
|
|
158
|
+
cf_list = self._sum_product_ve(elimination_order_removed, list(self._cf_dict.values()), self._evidence)
|
|
159
|
+
posterior = np.prod(cf_list)
|
|
160
|
+
if normalized:
|
|
161
|
+
normalization_cf = self._sum_product_ve(elimination_order_kept, cf_list, {})
|
|
162
|
+
posterior = posterior / np.prod(normalization_cf)
|
|
163
|
+
return posterior
|
|
164
|
+
|
|
165
|
+
def posterior(self, variable: str) -> GaussianVariable:
|
|
166
|
+
"""
|
|
167
|
+
Returns the posterior density as a Gaussian variable.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
variable : str
|
|
172
|
+
The target variable name.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
GaussianVariable
|
|
177
|
+
The posterior Gaussian variable.
|
|
178
|
+
"""
|
|
179
|
+
if variable in self._evidence:
|
|
180
|
+
return GaussianVariable(variable, self._evidence[variable], 0)
|
|
181
|
+
|
|
182
|
+
posterior_cf = self.canonicalPosterior([variable])
|
|
183
|
+
_, t_mu, t_var = posterior_cf.toGaussian()
|
|
184
|
+
|
|
185
|
+
return GaussianVariable(variable, t_mu[0][0], math.sqrt(t_var[0][0]))
|
|
186
|
+
|
|
187
|
+
def _constructCanonicalForms(self):
|
|
188
|
+
"""
|
|
189
|
+
Construct the canonical forms associated with the CLG.
|
|
190
|
+
"""
|
|
191
|
+
cf_dict = {}
|
|
192
|
+
for node in self._clg._graph.nodes():
|
|
193
|
+
var = self._clg._id2var[node]
|
|
194
|
+
parents = list(self._clg.parents(node))
|
|
195
|
+
if len(parents) == 0:
|
|
196
|
+
cf = CanonicalForm.fromCLG(node, [], var.mu(), var.sigma(), [])
|
|
197
|
+
else:
|
|
198
|
+
B = []
|
|
199
|
+
for parent in parents:
|
|
200
|
+
B.append(self._clg._arc2coef[(parent, node)])
|
|
201
|
+
|
|
202
|
+
cf = CanonicalForm.fromCLG(node, parents, var.mu(), var.sigma(), B)
|
|
203
|
+
cf_dict[node] = cf
|
|
204
|
+
return cf_dict
|
|
205
|
+
|
|
206
|
+
def _sum_product_ve(self, elimination_order, cf_list, evidence):
|
|
207
|
+
"""
|
|
208
|
+
The variable elimination algorithm for CLG.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
elimination_order : list
|
|
213
|
+
The elimination order.
|
|
214
|
+
cf_list : list
|
|
215
|
+
A list of canonical forms.
|
|
216
|
+
evidence : dict
|
|
217
|
+
A dictionary of evidences.
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
list
|
|
222
|
+
A list of canonical forms with variables in elimination_order eliminated.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
# Converting from dict[str, float] to dict[NodeId, float]
|
|
226
|
+
evidence = {self._clg._name2id[name]: evidence[name] for name in evidence.keys()}
|
|
227
|
+
|
|
228
|
+
# Reducing the canonical forms containing observed variables
|
|
229
|
+
if len(evidence) != 0:
|
|
230
|
+
for i, cf in enumerate(cf_list):
|
|
231
|
+
cf_list[i] = cf.reduce(evidence)
|
|
232
|
+
|
|
233
|
+
if len(elimination_order) != 0:
|
|
234
|
+
for variable in elimination_order:
|
|
235
|
+
cf_list = self._sum_product_eliminate_var(cf_list, variable)
|
|
236
|
+
|
|
237
|
+
return cf_list
|
|
238
|
+
|
|
239
|
+
def _sum_product_eliminate_var(self, cf_list, variable):
|
|
240
|
+
"""
|
|
241
|
+
Remove a variable from a set of canonical forms.
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
cf_list : list
|
|
246
|
+
A list of canonical form.
|
|
247
|
+
variable : int
|
|
248
|
+
The variable id to eliminate.
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
list
|
|
253
|
+
The list of canonical form with the variable removed.
|
|
254
|
+
"""
|
|
255
|
+
contain_var_cfs = [] # CF containing the variable
|
|
256
|
+
id_to_remove = []
|
|
257
|
+
for i, cf in enumerate(cf_list):
|
|
258
|
+
if variable in cf:
|
|
259
|
+
contain_var_cfs.append(cf)
|
|
260
|
+
id_to_remove.append(i)
|
|
261
|
+
|
|
262
|
+
if len(contain_var_cfs) == 0:
|
|
263
|
+
return cf_list
|
|
264
|
+
|
|
265
|
+
for i in reversed(id_to_remove):
|
|
266
|
+
cf_list.pop(i)
|
|
267
|
+
cf_product = np.prod(contain_var_cfs)
|
|
268
|
+
cf_marg = cf_product.marginalize([variable])
|
|
269
|
+
cf_list.append(cf_marg)
|
|
270
|
+
|
|
271
|
+
return cf_list
|
pyagrum/common.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
__version__ = '2.3.1.9'
|
|
2
|
+
__license__ = __doc__
|
|
3
|
+
__project_url__ = 'https://agrum.org'
|
|
4
|
+
__project_name__ = 'pyAgrum'
|
|
5
|
+
__project_description__ = __doc__
|
|
6
|
+
__project__ = __doc__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def about():
|
|
10
|
+
"""
|
|
11
|
+
about() for pyAgrum
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
print(f"pyAgrum {__version__}")
|
|
15
|
+
print("(c) 2015-2024 Pierre-Henri Wuillemin, Christophe Gonzales")
|
|
16
|
+
print("""
|
|
17
|
+
This is free software; see the source code for copying conditions.
|
|
18
|
+
There is ABSOLUTELY NO WARRANTY; not even for MERCHANTABILITY or
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE. For details, see 'pyagrum.warranty'.
|
|
20
|
+
""")
|
|
21
|
+
############################################################################
|
|
22
|
+
# This file is part of the aGrUM/pyAgrum library. #
|
|
23
|
+
# #
|
|
24
|
+
# Copyright (c) 2005-2025 by #
|
|
25
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
26
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
27
|
+
# #
|
|
28
|
+
# The aGrUM/pyAgrum library is free software; you can redistribute it #
|
|
29
|
+
# and/or modify it under the terms of either : #
|
|
30
|
+
# #
|
|
31
|
+
# - the GNU Lesser General Public License as published by #
|
|
32
|
+
# the Free Software Foundation, either version 3 of the License, #
|
|
33
|
+
# or (at your option) any later version, #
|
|
34
|
+
# - the MIT license (MIT), #
|
|
35
|
+
# - or both in dual license, as here. #
|
|
36
|
+
# #
|
|
37
|
+
# (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) #
|
|
38
|
+
# #
|
|
39
|
+
# This aGrUM/pyAgrum library is distributed in the hope that it will be #
|
|
40
|
+
# useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #
|
|
41
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS #
|
|
42
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
|
|
43
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
|
|
44
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
|
|
45
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
|
|
46
|
+
# OTHER DEALINGS IN THE SOFTWARE. #
|
|
47
|
+
# #
|
|
48
|
+
# See LICENCES for more details. #
|
|
49
|
+
# #
|
|
50
|
+
# SPDX-FileCopyrightText: Copyright 2005-2025 #
|
|
51
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
52
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
53
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later OR MIT #
|
|
54
|
+
# #
|
|
55
|
+
# Contact : info_at_agrum_dot_org #
|
|
56
|
+
# homepage : http://agrum.gitlab.io #
|
|
57
|
+
# gitlab : https://gitlab.com/agrumery/agrum #
|
|
58
|
+
# #
|
|
59
|
+
############################################################################
|
|
60
|
+
|
pyagrum/config.py
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
############################################################################
|
|
2
|
+
# This file is part of the aGrUM/pyAgrum library. #
|
|
3
|
+
# #
|
|
4
|
+
# Copyright (c) 2005-2025 by #
|
|
5
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
6
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
7
|
+
# #
|
|
8
|
+
# The aGrUM/pyAgrum library is free software; you can redistribute it #
|
|
9
|
+
# and/or modify it under the terms of either : #
|
|
10
|
+
# #
|
|
11
|
+
# - the GNU Lesser General Public License as published by #
|
|
12
|
+
# the Free Software Foundation, either version 3 of the License, #
|
|
13
|
+
# or (at your option) any later version, #
|
|
14
|
+
# - the MIT license (MIT), #
|
|
15
|
+
# - or both in dual license, as here. #
|
|
16
|
+
# #
|
|
17
|
+
# (see https://agrum.gitlab.io/articles/dual-licenses-lgplv3mit.html) #
|
|
18
|
+
# #
|
|
19
|
+
# This aGrUM/pyAgrum library is distributed in the hope that it will be #
|
|
20
|
+
# useful, but WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, #
|
|
21
|
+
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES MERCHANTABILITY or FITNESS #
|
|
22
|
+
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
|
|
23
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
|
|
24
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, #
|
|
25
|
+
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
|
|
26
|
+
# OTHER DEALINGS IN THE SOFTWARE. #
|
|
27
|
+
# #
|
|
28
|
+
# See LICENCES for more details. #
|
|
29
|
+
# #
|
|
30
|
+
# SPDX-FileCopyrightText: Copyright 2005-2025 #
|
|
31
|
+
# - Pierre-Henri WUILLEMIN(_at_LIP6) #
|
|
32
|
+
# - Christophe GONZALES(_at_AMU) #
|
|
33
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later OR MIT #
|
|
34
|
+
# #
|
|
35
|
+
# Contact : info_at_agrum_dot_org #
|
|
36
|
+
# homepage : http://agrum.gitlab.io #
|
|
37
|
+
# gitlab : https://gitlab.com/agrumery/agrum #
|
|
38
|
+
# #
|
|
39
|
+
############################################################################
|
|
40
|
+
|
|
41
|
+
"""
|
|
42
|
+
configuration tool for pyAgrum
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
from configparser import ConfigParser
|
|
46
|
+
import os
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GumSingleton(type):
|
|
50
|
+
_instances = {}
|
|
51
|
+
|
|
52
|
+
def __call__(cls, *args, **kwargs):
|
|
53
|
+
if cls not in cls._instances:
|
|
54
|
+
cls._instances[cls] = super(GumSingleton, cls).__call__(*args, **kwargs)
|
|
55
|
+
return cls._instances[cls]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PyAgrumConfiguration(metaclass=GumSingleton):
|
|
59
|
+
"""PyAgrumConfiguration is a the pyAgrum configuration singleton. The configuration is build
|
|
60
|
+
as a classical ConfigParser with read-only structure. Then a value is adressable using a double key: ``[section,key]``.
|
|
61
|
+
|
|
62
|
+
See `this notebook <https://lip6.fr/Pierre-Henri.Wuillemin/aGrUM/docs/last/notebooks/configForPyAgrum.ipynb.html>`_.
|
|
63
|
+
|
|
64
|
+
Examples
|
|
65
|
+
--------
|
|
66
|
+
>>> import pyagrum as gum
|
|
67
|
+
>>> gum.config["dynamicBN", "default_graph_size"] = 10
|
|
68
|
+
>>> gum.config["dynamicBN", "default_graph_size"]
|
|
69
|
+
"10"
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def _check_int(self, s):
|
|
73
|
+
if s[0] in ("-", "+"):
|
|
74
|
+
return s[1:].isdigit()
|
|
75
|
+
return s.isdigit()
|
|
76
|
+
|
|
77
|
+
def _check_float(self, s):
|
|
78
|
+
t = s.split(".")
|
|
79
|
+
if len(t) == 1:
|
|
80
|
+
return self._check_int(t[0])
|
|
81
|
+
elif len(t) == 2:
|
|
82
|
+
return self._check_int(t[0]) and t[1].isdigit()
|
|
83
|
+
else:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
def _check_bool(self, s):
|
|
87
|
+
return self._check_bool_true(s) or self._check_bool_false(s)
|
|
88
|
+
|
|
89
|
+
def _check_bool_true(self, s):
|
|
90
|
+
return s.upper() in ["TRUE", "1", "ON", "YES"]
|
|
91
|
+
|
|
92
|
+
def _check_bool_false(self, s):
|
|
93
|
+
return s.upper() in ["FALSE", "0", "OFF", "NO"]
|
|
94
|
+
|
|
95
|
+
class _Casterization:
|
|
96
|
+
def __init__(self, container):
|
|
97
|
+
self.container = container
|
|
98
|
+
|
|
99
|
+
class _CastAsInt(_Casterization):
|
|
100
|
+
def __getitem__(self, x):
|
|
101
|
+
return int(self.container[x])
|
|
102
|
+
|
|
103
|
+
def __setitem__(self, x, v):
|
|
104
|
+
s = str(v)
|
|
105
|
+
if self.container._check_int(s):
|
|
106
|
+
self.container[x] = s
|
|
107
|
+
else:
|
|
108
|
+
raise ValueError(f"'{s}' must contain an int.")
|
|
109
|
+
|
|
110
|
+
class _CastAsFloat(_Casterization):
|
|
111
|
+
def __getitem__(self, x):
|
|
112
|
+
return float(self.container[x])
|
|
113
|
+
|
|
114
|
+
def __setitem__(self, x, v):
|
|
115
|
+
s = str(v)
|
|
116
|
+
if self.container._check_float(s):
|
|
117
|
+
self.container[x] = s
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"'{s}' must contain a float.")
|
|
120
|
+
|
|
121
|
+
class _CastAsBool(_Casterization):
|
|
122
|
+
def __getitem__(self, x):
|
|
123
|
+
return self.container._check_bool_true(self.container[x])
|
|
124
|
+
|
|
125
|
+
def __setitem__(self, x, v):
|
|
126
|
+
s = str(v)
|
|
127
|
+
if self.container._check_bool(s):
|
|
128
|
+
self.container[x] = s
|
|
129
|
+
else:
|
|
130
|
+
raise ValueError(f"'{s}' must contain a boolean (False/True, 0/1, Off/On).")
|
|
131
|
+
|
|
132
|
+
def __init__(self):
|
|
133
|
+
self.__parser = ConfigParser(allow_no_value=False)
|
|
134
|
+
|
|
135
|
+
defaultsfn = os.path.dirname(__file__) + "/defaults.ini"
|
|
136
|
+
self.__parser.read(defaultsfn)
|
|
137
|
+
self.__defaults = self.__str__()
|
|
138
|
+
self.__hooks = []
|
|
139
|
+
self.__stacks = []
|
|
140
|
+
|
|
141
|
+
self.asInt = self._CastAsInt(self)
|
|
142
|
+
self.asFloat = self._CastAsFloat(self)
|
|
143
|
+
self.asBool = self._CastAsBool(self)
|
|
144
|
+
|
|
145
|
+
def add_hook(self, fn):
|
|
146
|
+
self.__hooks.append(fn)
|
|
147
|
+
|
|
148
|
+
def run_hooks(self):
|
|
149
|
+
for fn in self.__hooks:
|
|
150
|
+
fn()
|
|
151
|
+
|
|
152
|
+
def set(self, section, option, value, no_hook=False):
|
|
153
|
+
"""set a property in a section. Preferably use ``__getitem__`` and ``__setitem__``.
|
|
154
|
+
|
|
155
|
+
Examples
|
|
156
|
+
--------
|
|
157
|
+
>>> gum.config["dynamicBN", "default_graph_size"] = 10
|
|
158
|
+
>>> gum.config["dynamicBN", "default_graph_size"]
|
|
159
|
+
"10"
|
|
160
|
+
|
|
161
|
+
Arguments:
|
|
162
|
+
section {str} -- the section name (has to exist in defaults)
|
|
163
|
+
option {str} -- the option/property name (has to exist in defaults)
|
|
164
|
+
value {str} -- the value (will be store as string)
|
|
165
|
+
no_hook {bool} -- (optional) should this call trigger the hooks ?
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
SyntaxError: if the secion name or the property name does not exist
|
|
169
|
+
"""
|
|
170
|
+
if section in self.__parser.sections():
|
|
171
|
+
if option in self.__parser[section]:
|
|
172
|
+
self.__parser.set(section, option, str(value))
|
|
173
|
+
if not no_hook:
|
|
174
|
+
self.run_hooks()
|
|
175
|
+
else:
|
|
176
|
+
raise SyntaxError(f"Key '{section},{option}' unknown in pyAgrum configuration.")
|
|
177
|
+
else:
|
|
178
|
+
raise SyntaxError(f"Section '{section}' unknown in pyAgrum configuration.")
|
|
179
|
+
|
|
180
|
+
def get(self, section, option):
|
|
181
|
+
"""Give the value associated to section.option. Preferably use ``__getitem__`` and ``__setitem__``.
|
|
182
|
+
|
|
183
|
+
Examples
|
|
184
|
+
--------
|
|
185
|
+
>>> gum.config["dynamicBN", "default_graph_size"] = 10
|
|
186
|
+
>>> gum.config["dynamicBN", "default_graph_size"]
|
|
187
|
+
"10"
|
|
188
|
+
|
|
189
|
+
Arguments:
|
|
190
|
+
section {str} -- the section
|
|
191
|
+
option {str} -- the property
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
str -- the value (as string)
|
|
195
|
+
"""
|
|
196
|
+
return self.__parser.get(section, option)
|
|
197
|
+
|
|
198
|
+
def __diff(self):
|
|
199
|
+
mine = self.__parser
|
|
200
|
+
c = ConfigParser()
|
|
201
|
+
c.read_string(self.__defaults)
|
|
202
|
+
|
|
203
|
+
def aff_sec(section):
|
|
204
|
+
return (
|
|
205
|
+
"["
|
|
206
|
+
+ section
|
|
207
|
+
+ "]\n"
|
|
208
|
+
+ "\n".join(
|
|
209
|
+
[
|
|
210
|
+
f" {key} = {mine[section][key]}"
|
|
211
|
+
for key in mine[section].keys()
|
|
212
|
+
if mine.get(section, key) != c.get(section, key)
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return "\n".join([sec for sec in [aff_sec(section) for section in mine.sections()] if "=" in sec])
|
|
218
|
+
|
|
219
|
+
def save(self):
|
|
220
|
+
"""Save the diff with the defaults in ``pyagrum.ini`` in the current directory"""
|
|
221
|
+
with open("pyagrum.ini", "w") as configfile:
|
|
222
|
+
print(self.__diff(), file=configfile)
|
|
223
|
+
|
|
224
|
+
def reset(self):
|
|
225
|
+
"""back to defaults"""
|
|
226
|
+
self.__parser.read_string(self.__defaults)
|
|
227
|
+
self.run_hooks()
|
|
228
|
+
|
|
229
|
+
def load(self):
|
|
230
|
+
"""load pyagrum.ini in the current directory, and change the properties if needed
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
FileNotFoundError: if there is no pyagrum.ini in the current directory
|
|
234
|
+
"""
|
|
235
|
+
if os.path.isfile("pyagrum.ini"):
|
|
236
|
+
# to force to use the protected set() method
|
|
237
|
+
c = ConfigParser()
|
|
238
|
+
c.read("pyagrum.ini")
|
|
239
|
+
error_found = False
|
|
240
|
+
for section in c.sections():
|
|
241
|
+
if section not in self.__parser.sections():
|
|
242
|
+
error_found = True
|
|
243
|
+
print(f"[pyagrum.ini] Section '{section}' does not exist.")
|
|
244
|
+
for option in c[section]:
|
|
245
|
+
try:
|
|
246
|
+
self.set(section, option, c[section][option], no_hook=True)
|
|
247
|
+
except SyntaxError:
|
|
248
|
+
error_found = True
|
|
249
|
+
print(f"[pyagrum.ini] Option '{section}.{option}' does not exist.")
|
|
250
|
+
self.run_hooks()
|
|
251
|
+
if error_found:
|
|
252
|
+
self.save()
|
|
253
|
+
else:
|
|
254
|
+
raise FileNotFoundError("No file 'pyagrum.ini' in current directory.")
|
|
255
|
+
|
|
256
|
+
def grep(self, search):
|
|
257
|
+
"""grep in the configuration any section or properties matching the argument. If a section match the argume, all the section is displayed.
|
|
258
|
+
|
|
259
|
+
Arguments:
|
|
260
|
+
search {str} -- the string to find
|
|
261
|
+
"""
|
|
262
|
+
mine = self.__parser
|
|
263
|
+
lowsearch = search.lower()
|
|
264
|
+
|
|
265
|
+
def aff_sec(section, all):
|
|
266
|
+
return (
|
|
267
|
+
"["
|
|
268
|
+
+ section
|
|
269
|
+
+ "]\n"
|
|
270
|
+
+ "\n".join([f" {key} = {mine[section][key]}" for key in mine[section].keys() if all or lowsearch in key])
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
print(
|
|
274
|
+
"\n".join([sec for sec in [aff_sec(section, lowsearch in section) for section in mine.sections()] if "=" in sec])
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def diff(self):
|
|
278
|
+
"""print the diff between actual configuration and the defaults. This is what is saved in the file ``pyagrum.ini`` by the method `PyAgrumConfiguration.save()`"""
|
|
279
|
+
print(self.__diff())
|
|
280
|
+
|
|
281
|
+
def __str__(self):
|
|
282
|
+
mine = self.__parser
|
|
283
|
+
|
|
284
|
+
def aff_sec(section):
|
|
285
|
+
return "[" + section + "]\n" + "\n".join([f" {key} = {mine[section][key]}" for key in mine[section].keys()])
|
|
286
|
+
|
|
287
|
+
return "\n".join([aff_sec(section) for section in mine.sections()])
|
|
288
|
+
|
|
289
|
+
def __repr__(self):
|
|
290
|
+
res = self.__diff()
|
|
291
|
+
if "=" in res:
|
|
292
|
+
return res
|
|
293
|
+
else:
|
|
294
|
+
return "# no customized property\n" + self.__str__()
|
|
295
|
+
|
|
296
|
+
def __getitem__(self, key):
|
|
297
|
+
return self.get(key[0], key[1])
|
|
298
|
+
|
|
299
|
+
def __setitem__(self, key, value):
|
|
300
|
+
return self.set(key[0], key[1], value)
|
|
301
|
+
|
|
302
|
+
def __delitem__(self, key):
|
|
303
|
+
raise SyntaxError("No deletion of item in configuration")
|
|
304
|
+
|
|
305
|
+
def pop(self):
|
|
306
|
+
"""
|
|
307
|
+
Pop the last config from the stack and set it as the current configuration
|
|
308
|
+
"""
|
|
309
|
+
if len(self.__stacks) > 0:
|
|
310
|
+
self.__parser.read_string(self.__stacks.pop())
|
|
311
|
+
self.run_hooks()
|
|
312
|
+
else:
|
|
313
|
+
raise IndexError("[pyAgrum] No configuration to pop")
|
|
314
|
+
|
|
315
|
+
def push(self):
|
|
316
|
+
"""
|
|
317
|
+
Push the current configuration in the stack
|
|
318
|
+
"""
|
|
319
|
+
self.__stacks.append(str(self))
|