pyAgrum-nightly 2.1.1.9.dev202506061747485979__cp310-abi3-manylinux2014_aarch64.whl → 2.3.1.9.dev202601031765915415__cp310-abi3-manylinux2014_aarch64.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.
Files changed (110) hide show
  1. pyagrum/__init__.py +6 -2
  2. pyagrum/_pyagrum.so +0 -0
  3. pyagrum/bnmixture/BNMInference.py +6 -2
  4. pyagrum/bnmixture/BNMLearning.py +12 -2
  5. pyagrum/bnmixture/BNMixture.py +6 -2
  6. pyagrum/bnmixture/__init__.py +6 -2
  7. pyagrum/bnmixture/notebook.py +6 -2
  8. pyagrum/causal/_CausalFormula.py +6 -2
  9. pyagrum/causal/_CausalModel.py +6 -2
  10. pyagrum/causal/__init__.py +6 -2
  11. pyagrum/causal/_causalImpact.py +6 -2
  12. pyagrum/causal/_dSeparation.py +6 -2
  13. pyagrum/causal/_doAST.py +6 -2
  14. pyagrum/causal/_doCalculus.py +6 -2
  15. pyagrum/causal/_doorCriteria.py +6 -2
  16. pyagrum/causal/_exceptions.py +6 -2
  17. pyagrum/causal/_types.py +6 -2
  18. pyagrum/causal/causalEffectEstimation/_CausalEffectEstimation.py +6 -2
  19. pyagrum/causal/causalEffectEstimation/_IVEstimators.py +6 -2
  20. pyagrum/causal/causalEffectEstimation/_RCTEstimators.py +6 -2
  21. pyagrum/causal/causalEffectEstimation/__init__.py +6 -2
  22. pyagrum/causal/causalEffectEstimation/_backdoorEstimators.py +6 -2
  23. pyagrum/causal/causalEffectEstimation/_causalBNEstimator.py +6 -2
  24. pyagrum/causal/causalEffectEstimation/_frontdoorEstimators.py +6 -2
  25. pyagrum/causal/causalEffectEstimation/_learners.py +6 -2
  26. pyagrum/causal/causalEffectEstimation/_utils.py +6 -2
  27. pyagrum/causal/notebook.py +8 -3
  28. pyagrum/clg/CLG.py +6 -2
  29. pyagrum/clg/GaussianVariable.py +6 -2
  30. pyagrum/clg/SEM.py +6 -2
  31. pyagrum/clg/__init__.py +6 -2
  32. pyagrum/clg/canonicalForm.py +6 -2
  33. pyagrum/clg/constants.py +6 -2
  34. pyagrum/clg/forwardSampling.py +6 -2
  35. pyagrum/clg/learning.py +6 -2
  36. pyagrum/clg/notebook.py +6 -2
  37. pyagrum/clg/variableElimination.py +6 -2
  38. pyagrum/common.py +7 -3
  39. pyagrum/config.py +7 -2
  40. pyagrum/ctbn/CIM.py +6 -2
  41. pyagrum/ctbn/CTBN.py +6 -2
  42. pyagrum/ctbn/CTBNGenerator.py +6 -2
  43. pyagrum/ctbn/CTBNInference.py +6 -2
  44. pyagrum/ctbn/CTBNLearner.py +6 -2
  45. pyagrum/ctbn/SamplesStats.py +6 -2
  46. pyagrum/ctbn/StatsIndepTest.py +6 -2
  47. pyagrum/ctbn/__init__.py +6 -2
  48. pyagrum/ctbn/constants.py +6 -2
  49. pyagrum/ctbn/notebook.py +6 -2
  50. pyagrum/deprecated.py +6 -2
  51. pyagrum/explain/_ComputationCausal.py +75 -0
  52. pyagrum/explain/_ComputationConditional.py +48 -0
  53. pyagrum/explain/_ComputationMarginal.py +48 -0
  54. pyagrum/explain/_CustomShapleyCache.py +110 -0
  55. pyagrum/explain/_Explainer.py +176 -0
  56. pyagrum/explain/_Explanation.py +70 -0
  57. pyagrum/explain/_FIFOCache.py +54 -0
  58. pyagrum/explain/_ShallCausalValues.py +204 -0
  59. pyagrum/explain/_ShallConditionalValues.py +155 -0
  60. pyagrum/explain/_ShallMarginalValues.py +155 -0
  61. pyagrum/explain/_ShallValues.py +296 -0
  62. pyagrum/explain/_ShapCausalValues.py +208 -0
  63. pyagrum/explain/_ShapConditionalValues.py +126 -0
  64. pyagrum/explain/_ShapMarginalValues.py +191 -0
  65. pyagrum/explain/_ShapleyValues.py +298 -0
  66. pyagrum/explain/__init__.py +81 -0
  67. pyagrum/explain/_explGeneralizedMarkovBlanket.py +152 -0
  68. pyagrum/explain/_explIndependenceListForPairs.py +146 -0
  69. pyagrum/explain/_explInformationGraph.py +264 -0
  70. pyagrum/explain/notebook/__init__.py +54 -0
  71. pyagrum/explain/notebook/_bar.py +142 -0
  72. pyagrum/explain/notebook/_beeswarm.py +174 -0
  73. pyagrum/explain/notebook/_showShapValues.py +97 -0
  74. pyagrum/explain/notebook/_waterfall.py +220 -0
  75. pyagrum/explain/shapley.py +225 -0
  76. pyagrum/lib/__init__.py +6 -2
  77. pyagrum/lib/_colors.py +6 -2
  78. pyagrum/lib/bn2graph.py +6 -2
  79. pyagrum/lib/bn2roc.py +6 -2
  80. pyagrum/lib/bn2scores.py +6 -2
  81. pyagrum/lib/bn_vs_bn.py +6 -2
  82. pyagrum/lib/cn2graph.py +6 -2
  83. pyagrum/lib/discreteTypeProcessor.py +99 -81
  84. pyagrum/lib/discretizer.py +6 -2
  85. pyagrum/lib/dynamicBN.py +6 -2
  86. pyagrum/lib/explain.py +17 -492
  87. pyagrum/lib/export.py +6 -2
  88. pyagrum/lib/id2graph.py +6 -2
  89. pyagrum/lib/image.py +6 -2
  90. pyagrum/lib/ipython.py +6 -2
  91. pyagrum/lib/mrf2graph.py +6 -2
  92. pyagrum/lib/notebook.py +6 -2
  93. pyagrum/lib/proba_histogram.py +6 -2
  94. pyagrum/lib/utils.py +6 -2
  95. pyagrum/pyagrum.py +976 -126
  96. pyagrum/skbn/_MBCalcul.py +6 -2
  97. pyagrum/skbn/__init__.py +6 -2
  98. pyagrum/skbn/_learningMethods.py +6 -2
  99. pyagrum/skbn/_utils.py +6 -2
  100. pyagrum/skbn/bnclassifier.py +6 -2
  101. pyagrum_nightly-2.1.1.9.dev202506061747485979.dist-info/LICENSE → pyagrum_nightly-2.3.1.9.dev202601031765915415.dist-info/LICENSE.md +3 -1
  102. pyagrum_nightly-2.3.1.9.dev202601031765915415.dist-info/LICENSES/LGPL-3.0-or-later.txt +304 -0
  103. pyagrum_nightly-2.3.1.9.dev202601031765915415.dist-info/LICENSES/MIT.txt +18 -0
  104. {pyagrum_nightly-2.1.1.9.dev202506061747485979.dist-info → pyagrum_nightly-2.3.1.9.dev202601031765915415.dist-info}/METADATA +3 -1
  105. pyagrum_nightly-2.3.1.9.dev202601031765915415.dist-info/RECORD +107 -0
  106. {pyagrum_nightly-2.1.1.9.dev202506061747485979.dist-info → pyagrum_nightly-2.3.1.9.dev202601031765915415.dist-info}/WHEEL +1 -1
  107. pyagrum/lib/shapley.py +0 -657
  108. pyagrum_nightly-2.1.1.9.dev202506061747485979.dist-info/LICENSE.LGPL +0 -165
  109. pyagrum_nightly-2.1.1.9.dev202506061747485979.dist-info/LICENSE.MIT +0 -17
  110. pyagrum_nightly-2.1.1.9.dev202506061747485979.dist-info/RECORD +0 -83
@@ -25,8 +25,12 @@
25
25
  # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
26
26
  # OTHER DEALINGS IN THE SOFTWARE. #
27
27
  # #
28
- # See the GNU Lesser General Public License (LICENSE.LGPL) and the MIT #
29
- # licence (LICENSE.MIT) for more details. #
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 #
30
34
  # #
31
35
  # Contact : info_at_agrum_dot_org #
32
36
  # homepage : http://agrum.gitlab.io #
@@ -125,7 +129,12 @@ class DiscreteTypeProcessor:
125
129
 
126
130
  """
127
131
 
128
- def __init__(self, defaultDiscretizationMethod="quantile", defaultNumberOfBins=10, discretizationThreshold=25):
132
+ def __init__(
133
+ self,
134
+ defaultDiscretizationMethod="quantile",
135
+ defaultNumberOfBins=10,
136
+ discretizationThreshold=25,
137
+ ):
129
138
  """
130
139
  Initializes the DiscreteTypeProcessor object.
131
140
 
@@ -174,7 +183,7 @@ class DiscreteTypeProcessor:
174
183
  self.discretizationParametersDictionary = {}
175
184
 
176
185
  @gum.deprecated_arg(newA="parameters", oldA="paramDiscretizationMethod", version="2.0.0")
177
- def setDiscretizationParameters(self, variableName=None, method=None, parameters=None):
186
+ def setDiscretizationParameters(self, variableName: str, method: str, parameters: Any = None):
178
187
  """
179
188
  Sets the discretization parameters for a variable. If variableName is None, sets the default parameters.
180
189
 
@@ -197,44 +206,40 @@ class DiscreteTypeProcessor:
197
206
  - 'expert': this parameter is the set of ticks proposed by the expert. The discretized variable will set the flag
198
207
  'empirical' which means that if the values found in the data are not in the proposed intervals, they did not raise
199
208
  any exception but are nevertheless accepted (as belonging to the smallest or biggest interval).
200
- - 'NoDiscretization': this parameter is a superset of the values for the variable found in the database.
209
+ - 'NoDiscretization': this parameter is a superset of the values for the variable found in the database (or None).
201
210
  """
202
- if variableName in self.discretizationParametersDictionary:
203
- oldParamDiscretizationMethod = self.discretizationParametersDictionary[variableName]["param"]
204
- oldMethod = self.discretizationParametersDictionary[variableName]["method"]
205
- else:
206
- oldParamDiscretizationMethod = self.defaultParamDiscretizationMethod
207
- oldMethod = self.defaultMethod
208
-
209
- if method is None:
210
- method = oldMethod
211
-
212
- if parameters is None and method not in {"NoDiscretization", "expert"}:
213
- parameters = oldParamDiscretizationMethod
214
-
215
- if method not in {"kmeans", "uniform", "quantile", "NML", "MDLP", "CAIM", "NoDiscretization", "expert"}:
216
- raise ValueError(
217
- "This discretization method is not recognized! Possible values are kmeans, uniform, quantile, NML, "
218
- "CAIM, MDLP, NoDiscretization or expert. You have entered " + str(method)
219
- )
211
+ if parameters is None:
212
+ parameters = self.defaultParamDiscretizationMethod
220
213
 
221
- if parameters == "elbowMethod":
222
- if method == "NML":
223
- raise ValueError(
224
- "The elbow Method cannot be used as the number of bins for the algorithm NML. Please select an integer value"
225
- )
226
- elif method not in {"NoDiscretization", "expert"}:
227
- try:
228
- _ = int(parameters)
229
- except ValueError:
230
- raise ValueError(
231
- "The possible values for paramDiscretizationMethod are any integer or the string 'elbowMethod'. You have entered: "
232
- + str(parameters)
233
- )
234
- else:
235
- if parameters is not None and not isinstance(parameters, list):
214
+ match method:
215
+ case "quantile" | "NML":
216
+ if type(parameters) is not int:
217
+ raise ValueError(
218
+ "The parameter for the quantile/NML method must be an integer. You have entered: " + str(parameters)
219
+ )
220
+ case "kmeans" | "uniform":
221
+ if type(parameters) is not int and str(parameters) != "elbowMethod":
222
+ raise ValueError(
223
+ "The parameter for the kmeans/uniform method must be an integer or the string 'elbowMethod'. You have entered: "
224
+ + str(parameters)
225
+ )
226
+ case "expert":
227
+ if not (isinstance(parameters, list) and all(map(check_float, parameters))):
228
+ raise ValueError(
229
+ "The parameter for the expert method must be a list of float. You have entered: " + str(parameters)
230
+ )
231
+ case "NoDiscretization":
232
+ if parameters is not None and not (isinstance(parameters, str)):
233
+ raise ValueError(
234
+ "The parameter for the NoDiscretization method must be a string (fastVar syntax) or None. You have "
235
+ "entered: " + str(parameters)
236
+ )
237
+ case "CAIM" | "MDLP":
238
+ pass
239
+ case _:
236
240
  raise ValueError(
237
- f"For a NotDiscretized/expert method, the parameter has to be None or a list of values but not '{parameters}'."
241
+ "This discretization method is not recognized! Possible values are kmeans, uniform, quantile, NML, "
242
+ "CAIM, MDLP, NoDiscretization or expert. You have entered " + str(method)
238
243
  )
239
244
 
240
245
  if variableName is None:
@@ -378,7 +383,12 @@ class DiscreteTypeProcessor:
378
383
  Xsorted = X[X.argsort(axis=None)]
379
384
  binEdgeMatrix = [[]] * 14
380
385
  for k in range(2, 16):
381
- discretizer = skp.KBinsDiscretizer(k, strategy=discretizationStrategy, subsample=None)
386
+ discretizer = skp.KBinsDiscretizer(
387
+ k,
388
+ strategy=discretizationStrategy,
389
+ quantile_method="averaged_inverted_cdf",
390
+ subsample=None,
391
+ )
382
392
  discretizer.fit(Xsorted)
383
393
  binEdges = discretizer.bin_edges_[0]
384
394
  centresArray = (binEdges[1:] + binEdges[:-1]) / 2
@@ -404,7 +414,8 @@ class DiscreteTypeProcessor:
404
414
  transformedCoordinates = numpy.zeros((2, 14))
405
415
  for i in range(14):
406
416
  transformedCoordinates[:, i] = numpy.matmul(
407
- coordinateChangeMatrix, numpy.array([[i], [variationArray[i] - variationArray[0]]])
417
+ coordinateChangeMatrix,
418
+ numpy.array([[i], [variationArray[i] - variationArray[0]]]),
408
419
  ).reshape(2)
409
420
 
410
421
  # we search for the minimum in our newly obtained curve
@@ -414,7 +425,8 @@ class DiscreteTypeProcessor:
414
425
  minkIndex = k
415
426
  # when we have found the minimum, we apply the inverse linear transformation to recover the optimal value of k
416
427
  minimumVector = numpy.matmul(
417
- numpy.linalg.inv(coordinateChangeMatrix), transformedCoordinates[:, minkIndex].reshape(2, 1)
428
+ numpy.linalg.inv(coordinateChangeMatrix),
429
+ transformedCoordinates[:, minkIndex].reshape(2, 1),
418
430
  )
419
431
 
420
432
  # we return the list of bin edges found using said optimal number of k
@@ -511,7 +523,10 @@ class DiscreteTypeProcessor:
511
523
  break
512
524
  else:
513
525
  currentValues["boundaryIndex"] = binEdgesIndex[position - 1] + 1
514
- (currentValues["leftSubintervalClass0"], currentValues["leftSubintervalClass1"]) = (0, 0)
526
+ (
527
+ currentValues["leftSubintervalClass0"],
528
+ currentValues["leftSubintervalClass1"],
529
+ ) = (0, 0)
515
530
  minimalValues["classInformationEntropy"] = math.inf
516
531
  continue
517
532
 
@@ -854,52 +869,46 @@ class DiscreteTypeProcessor:
854
869
  else: # The user has manually set the discretization parameters for this variable
855
870
  usingDefaultParameters = False
856
871
  if self.discretizationParametersDictionary[variableName]["method"] != "NoDiscretization" and not isNumeric:
857
- raise ValueError("The variable " + variableName + " is not numeric and cannot be discretized!")
872
+ raise ValueError(f"The variable {variableName} is not numeric and cannot be discretized!")
858
873
 
859
874
  if self.discretizationParametersDictionary[variableName]["method"] == "NoDiscretization":
860
875
  is_int_var = True
861
- min_v = max_v = None
862
876
 
863
- possibleValuesX = None
877
+ varSyntax = ""
864
878
  if "param" in self.discretizationParametersDictionary[variableName]:
865
- possibleValuesX = self.discretizationParametersDictionary[variableName]["param"]
866
-
867
- if possibleValuesX is None:
868
- possibleValuesX = sorted(foundValuesX)
869
- else:
870
- # foundValuesX must be in possibleValuesX
871
- if not foundValuesX.issubset(possibleValuesX):
879
+ varSyntax = self.discretizationParametersDictionary[variableName]["param"]
880
+ if varSyntax is None:
881
+ varSyntax = ""
882
+
883
+ if varSyntax != "":
884
+ var = gum.fastVariable(variableName + varSyntax)
885
+ possibleValuesX = set(var.labels())
886
+ f = {str(x) for x in foundValuesX}
887
+ if not f.issubset(possibleValuesX):
872
888
  raise ValueError(
873
- f"The values passed in possibleValues ({possibleValuesX}) do not match database values ({foundValuesX})"
889
+ f"The values passed in possibleValues ({sorted(possibleValuesX)}) do not match database values ("
890
+ f"{sorted(f)})"
874
891
  )
892
+ return var
875
893
 
876
- for value in possibleValuesX:
877
- if check_int(value):
878
- v = int(value)
879
- if min_v is None or min_v > v:
880
- min_v = v
881
- if max_v is None or max_v < v:
882
- max_v = v
883
- else:
884
- is_int_var = False
885
- break
886
-
894
+ possibleValuesX = sorted(foundValuesX)
895
+ is_int_var = all(map(check_int, possibleValuesX))
887
896
  if is_int_var:
897
+ possibleValuesX = [int(x) for x in possibleValuesX]
898
+ max_v = int(possibleValuesX[-1]) # sorted
899
+ min_v = int(possibleValuesX[0])
900
+
888
901
  if len(possibleValuesX) == max_v - min_v + 1: # no hole in the list of int
889
- var = gum.RangeVariable(variableName, variableName, min_v, max_v)
902
+ return gum.RangeVariable(variableName, variableName, min_v, max_v)
890
903
  else:
891
- var = gum.IntegerVariable(variableName, variableName, [int(v) for v in possibleValuesX])
892
- else:
893
- is_float_var = True
894
- for value in possibleValuesX:
895
- if not check_float(value):
896
- is_float_var = False
897
- break
904
+ return gum.IntegerVariable(variableName, variableName, possibleValuesX)
898
905
 
899
- if is_float_var:
900
- var = gum.NumericalDiscreteVariable(variableName, variableName, [float(v) for v in possibleValuesX])
901
- else:
902
- var = gum.LabelizedVariable(variableName, variableName, [str(v) for v in possibleValuesX])
906
+ is_float_var = all(map(check_float, possibleValuesX))
907
+ if is_float_var:
908
+ possibleValuesX = [float(x) for x in possibleValuesX]
909
+ return gum.NumericalDiscreteVariable(variableName, variableName, possibleValuesX)
910
+ else:
911
+ return gum.LabelizedVariable(variableName, variableName, [str(v) for v in possibleValuesX])
903
912
  else:
904
913
  self.numberOfContinuous += 1
905
914
  if self.discretizationParametersDictionary[variableName]["method"] == "expert":
@@ -913,7 +922,10 @@ class DiscreteTypeProcessor:
913
922
  if possibleValuesY is None:
914
923
  possibleValuesY = numpy.unique(y)
915
924
  binEdges = self._discretizationCAIM(
916
- Xtransformed.reshape(n, 1), y.reshape(n, 1), numpy.unique(Xtransformed), possibleValuesY
925
+ Xtransformed.reshape(n, 1),
926
+ y.reshape(n, 1),
927
+ numpy.unique(Xtransformed),
928
+ possibleValuesY,
917
929
  )
918
930
  elif self.discretizationParametersDictionary[variableName]["method"] == "MDLP":
919
931
  if y is None:
@@ -924,7 +936,10 @@ class DiscreteTypeProcessor:
924
936
  if possibleValuesY is None:
925
937
  possibleValuesY = numpy.unique(y)
926
938
  binEdges = self._discretizationMDLP(
927
- Xtransformed.reshape(n, 1), y.reshape(n, 1), numpy.unique(Xtransformed), possibleValuesY
939
+ Xtransformed.reshape(n, 1),
940
+ y.reshape(n, 1),
941
+ numpy.unique(Xtransformed),
942
+ possibleValuesY,
928
943
  )
929
944
  elif self.discretizationParametersDictionary[variableName]["method"] == "NML":
930
945
  binEdges = self._discretizationNML(
@@ -935,12 +950,14 @@ class DiscreteTypeProcessor:
935
950
  else:
936
951
  if self.discretizationParametersDictionary[variableName]["param"] == "elbowMethod":
937
952
  binEdges = self._discretizationElbowMethodRotation(
938
- self.discretizationParametersDictionary[variableName]["method"], Xtransformed.flatten()
953
+ self.discretizationParametersDictionary[variableName]["method"],
954
+ Xtransformed.flatten(),
939
955
  )
940
956
  else:
941
957
  discre = skp.KBinsDiscretizer(
942
958
  self.discretizationParametersDictionary[variableName]["param"],
943
959
  strategy=self.discretizationParametersDictionary[variableName]["method"],
960
+ quantile_method="averaged_inverted_cdf",
944
961
  subsample=None,
945
962
  )
946
963
  discre.fit(X.reshape(-1, 1))
@@ -1001,7 +1018,8 @@ class DiscreteTypeProcessor:
1001
1018
  Class1ByLargeInterval.insert(position + 1, minimalValues["rightSubintervalClass1"])
1002
1019
  continueDividingInterval.insert(position + 1, True)
1003
1020
  totalCountByLargeInterval.insert(
1004
- position + 1, minimalValues["rightSubintervalClass0"] + minimalValues["rightSubintervalClass1"]
1021
+ position + 1,
1022
+ minimalValues["rightSubintervalClass0"] + minimalValues["rightSubintervalClass1"],
1005
1023
  )
1006
1024
  shannonEntropyByLargeInterval.insert(position + 1, minimalValues["rightSubintervalShannonEntropy"])
1007
1025
 
@@ -25,8 +25,12 @@
25
25
  # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
26
26
  # OTHER DEALINGS IN THE SOFTWARE. #
27
27
  # #
28
- # See the GNU Lesser General Public License (LICENSE.LGPL) and the MIT #
29
- # licence (LICENSE.MIT) for more details. #
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 #
30
34
  # #
31
35
  # Contact : info_at_agrum_dot_org #
32
36
  # homepage : http://agrum.gitlab.io #
pyagrum/lib/dynamicBN.py CHANGED
@@ -25,8 +25,12 @@
25
25
  # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR #
26
26
  # OTHER DEALINGS IN THE SOFTWARE. #
27
27
  # #
28
- # See the GNU Lesser General Public License (LICENSE.LGPL) and the MIT #
29
- # licence (LICENSE.MIT) for more details. #
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 #
30
34
  # #
31
35
  # Contact : info_at_agrum_dot_org #
32
36
  # homepage : http://agrum.gitlab.io #