ourocode 1.11.3__tar.gz → 2.0.0__tar.gz

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 (124) hide show
  1. {ourocode-1.11.3/ourocode.egg-info → ourocode-2.0.0}/PKG-INFO +6 -5
  2. ourocode-2.0.0/ourocode/eurocode/core/renderer.py +598 -0
  3. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec1/exploitation.py +4 -4
  4. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec1/neige.py +2 -2
  5. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec1/vent.py +2 -2
  6. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/assemblage/platine.py +1 -1
  7. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/assemblage/soudure.py +1 -1
  8. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/assemblage/tige.py +1 -1
  9. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/element_droit/cisaillement.py +1 -1
  10. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/element_droit/compression.py +1 -1
  11. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/element_droit/flexion.py +1 -1
  12. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/element_droit/plat.py +1 -1
  13. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/element_droit/traction.py +1 -1
  14. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/feu.py +2 -2
  15. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/agrafe.py +1 -1
  16. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/assemblage.py +1 -1
  17. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/boulon.py +1 -1
  18. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/broche.py +1 -1
  19. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/embrevement.py +1 -1
  20. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/pointe.py +1 -1
  21. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/tirefond.py +1 -1
  22. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/cvt.py +2 -2
  23. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/element_droit/barre.py +1 -1
  24. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/element_droit/cisaillement.py +1 -1
  25. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/element_droit/compression.py +1 -1
  26. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/element_droit/flexion.py +1 -1
  27. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/element_droit/traction.py +1 -1
  28. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/feu/cisaillement_feu.py +5 -1
  29. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/feu/compression_feu.py +6 -1
  30. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/feu/feu.py +1 -1
  31. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/feu/flexion_feu.py +6 -1
  32. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/feu/traction_feu.py +5 -2
  33. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec8/sismique.py +2 -2
  34. {ourocode-1.11.3 → ourocode-2.0.0/ourocode.egg-info}/PKG-INFO +6 -5
  35. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode.egg-info/SOURCES.txt +1 -0
  36. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode.egg-info/requires.txt +3 -5
  37. {ourocode-1.11.3 → ourocode-2.0.0}/pyproject.toml +6 -6
  38. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC8_Sismique.py +1 -1
  39. {ourocode-1.11.3 → ourocode-2.0.0}/LICENSE +0 -0
  40. {ourocode-1.11.3 → ourocode-2.0.0}/MANIFEST.in +0 -0
  41. {ourocode-1.11.3 → ourocode-2.0.0}/README.md +0 -0
  42. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/__init__.py +0 -0
  43. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/__version__.py +0 -0
  44. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/caracteristique_meca_acier.csv +0 -0
  45. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/caracteristique_meca_beton.csv +0 -0
  46. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/caracteristique_meca_bois.csv +0 -0
  47. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/caracteristique_meca_panel.csv +0 -0
  48. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/carte_action_region.csv +0 -0
  49. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/coeff_psy.csv +0 -0
  50. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/exploitation.csv +0 -0
  51. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/gammaM.csv +0 -0
  52. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/kdef.csv +0 -0
  53. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/kfi.csv +0 -0
  54. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/kmod.csv +0 -0
  55. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/limite_fleche.csv +0 -0
  56. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/qualite_acier.csv +0 -0
  57. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/screenshot/C90_def.png +0 -0
  58. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/screenshot/CNC2M_4.3.3_tab15.png +0 -0
  59. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/screenshot/CNC2M_4.3.3_tab16.png +0 -0
  60. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/screenshot/EN1993-1-8_fig6_11.png +0 -0
  61. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/screenshot/Kv_def.png +0 -0
  62. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/screenshot/sign_convention.png +0 -0
  63. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/section_boulon.csv +0 -0
  64. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/sismique/categorie_importance.csv +0 -0
  65. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/sismique/categorie_importance_ns.csv +0 -0
  66. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/sismique/classe_sol.csv +0 -0
  67. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/sismique/spectre_eleastique_h_type1.csv +0 -0
  68. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/sismique/spectre_eleastique_h_type2.csv +0 -0
  69. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cp_toiture_isolee_1_versant.csv +0 -0
  70. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cp_toiture_isolee_1_versant.png +0 -0
  71. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cp_toiture_isolee_2_versants.csv +0 -0
  72. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cp_toiture_isolee_2_versants.png +0 -0
  73. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_mur_verticaux.csv +0 -0
  74. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_mur_verticaux.png +0 -0
  75. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_1_versant.png +0 -0
  76. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_1_versant_0_degres.csv +0 -0
  77. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_1_versant_180_degres.csv +0 -0
  78. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_1_versant_90_degres.csv +0 -0
  79. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_2_versants.png +0 -0
  80. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_2_versants_0_degres.csv +0 -0
  81. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_2_versants_90_degres.csv +0 -0
  82. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_terrasse.csv +0 -0
  83. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Cpe_toiture_terrasse.png +0 -0
  84. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/data/vent/vent_Ffr.png +0 -0
  85. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/__init__.py +0 -0
  86. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/__init__.py +0 -0
  87. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/batiment.py +0 -0
  88. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/combinaison.py +0 -0
  89. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/model_generator.py +0 -0
  90. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/model_result.py +0 -0
  91. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/objet.py +0 -0
  92. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/core/projet.py +0 -0
  93. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec1/__init__.py +0 -0
  94. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/__init__.py +0 -0
  95. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/assemblage/__init__.py +0 -0
  96. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec3/element_droit/__init__.py +0 -0
  97. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/__init__.py +0 -0
  98. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/assemblage/__init__.py +0 -0
  99. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/blc.py +0 -0
  100. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/element_droit/__init__.py +0 -0
  101. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec5/feu/__init__.py +0 -0
  102. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/ec8/__init__.py +0 -0
  103. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/mixins/__init__.py +0 -0
  104. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/mixins/data_loader.py +0 -0
  105. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/mixins/math_utils.py +0 -0
  106. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/mixins/serialization.py +0 -0
  107. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode/eurocode/mixins/synthese.py +0 -0
  108. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode.egg-info/dependency_links.txt +0 -0
  109. {ourocode-1.11.3 → ourocode-2.0.0}/ourocode.egg-info/top_level.txt +0 -0
  110. {ourocode-1.11.3 → ourocode-2.0.0}/setup.cfg +0 -0
  111. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_A0_Projet.py +0 -0
  112. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC0_Combinaison.py +0 -0
  113. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC1_Exploitation.py +0 -0
  114. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC1_Neige.py +0 -0
  115. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC3_Assemblage.py +0 -0
  116. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC3_Element_droit.py +0 -0
  117. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC3_Feu.py +0 -0
  118. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC3_Platine.py +0 -0
  119. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC5_Assemblage.py +0 -0
  120. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC5_BLC.py +0 -0
  121. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC5_CVT.py +0 -0
  122. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC5_Element_droit.py +0 -0
  123. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC5_Embrevement.py +0 -0
  124. {ourocode-1.11.3 → ourocode-2.0.0}/tests/test_EC5_Feu.py +0 -0
@@ -1,24 +1,25 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ourocode
3
- Version: 1.11.3
3
+ Version: 2.0.0
4
4
  Summary: Ceci est un catalogue de fonction permettant une utilisation rapide pour la réalisation de note de calcul personnalisée.
5
5
  Author-email: Anthony PARISOT <contact@ourea-structure.fr>
6
6
  License-Expression: Apache-2.0
7
7
  Project-URL: Homepage, https://github.com/AnthonyPrst/ourocode
8
8
  Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
9
11
  Classifier: Operating System :: OS Independent
10
12
  Requires-Python: >=3.12
11
13
  Description-Content-Type: text/markdown
12
14
  License-File: LICENSE
13
- Requires-Dist: handcalcs
14
15
  Requires-Dist: forallpeople
15
16
  Requires-Dist: pandas
16
17
  Requires-Dist: Pillow
18
+ Requires-Dist: matplotlib
19
+ Requires-Dist: pyvista[all,trame]
20
+ Requires-Dist: PyNiteFEA
17
21
  Provides-Extra: gui
18
22
  Requires-Dist: PySide6; extra == "gui"
19
- Provides-Extra: mef
20
- Requires-Dist: PyNiteFEA; extra == "mef"
21
- Requires-Dist: pyvista[all,trame]; extra == "mef"
22
23
  Provides-Extra: full
23
24
  Requires-Dist: ourocode[gui,mef]; extra == "full"
24
25
  Provides-Extra: dev
@@ -0,0 +1,598 @@
1
+ # coding in UTF-8
2
+ # by Anthony PARISOT
3
+ """Remplacement de handcalcs.decorator.handcalc compatible Python 3.13+.
4
+
5
+ Ce module implémente un décorateur @handcalc qui génère du LaTeX à partir
6
+ du code source Python via l'AST (ast module), sans dépendance à innerscope.
7
+ Il reproduit le comportement utilisé dans ourocode : rendu des assignations
8
+ d'une fonction interne avec substitution symbolique et numérique.
9
+
10
+ Retour du décorateur : tuple (latex_str, valeur_retournée)
11
+ - latex_str : chaîne LaTeX prête à injecter dans IPython.display.Latex
12
+ - valeur : résultat numérique de la fonction (scalaire, tuple, dict…)
13
+ """
14
+
15
+ import ast
16
+ import inspect
17
+ import re
18
+ import textwrap
19
+ from math import sqrt, pi, sin, cos, tan, radians, log, log10, exp, floor, ceil, fabs
20
+ from typing import Any
21
+
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # Helpers de conversion AST → LaTeX
25
+ # ---------------------------------------------------------------------------
26
+
27
+ _GREEK = {
28
+ "alpha": r"\alpha", "beta": r"\beta", "gamma": r"\gamma", "delta": r"\delta",
29
+ "epsilon": r"\epsilon", "zeta": r"\zeta", "eta": r"\eta", "theta": r"\theta",
30
+ "lambda": r"\lambda", "mu": r"\mu", "nu": r"\nu", "xi": r"\xi",
31
+ "pi": r"\pi", "rho": r"\rho", "sigma": r"\sigma", "tau": r"\tau",
32
+ "phi": r"\phi", "chi": r"\chi", "psi": r"\psi", "omega": r"\omega",
33
+ "Gamma": r"\Gamma", "Delta": r"\Delta", "Theta": r"\Theta",
34
+ "Lambda": r"\Lambda", "Xi": r"\Xi", "Pi": r"\Pi", "Sigma": r"\Sigma",
35
+ "Phi": r"\Phi", "Psi": r"\Psi", "Omega": r"\Omega",
36
+ }
37
+
38
+ _FUNC_MAP = {
39
+ "sqrt": r"\sqrt", "sin": r"\sin", "cos": r"\cos", "tan": r"\tan",
40
+ "log": r"\log", "log10": r"\log_{10}", "exp": r"\exp",
41
+ "abs": r"\left|", "fabs": r"\left|",
42
+ "floor": r"\lfloor", "ceil": r"\lceil",
43
+ "radians": r"\frac{\pi}{180}",
44
+ "min": r"\min", "max": r"\max",
45
+ }
46
+
47
+
48
+ def _name_to_latex(name: str) -> str:
49
+ """Convertit un nom Python en symbole LaTeX (grecs, indices)."""
50
+ if name in _GREEK:
51
+ return _GREEK[name]
52
+ # Gestion des sous-indices : taux_6_11 → taux_{6,11} ; sigma_m_d → \sigma_{m,d}
53
+ parts = name.split("_")
54
+ if len(parts) == 1:
55
+ return name
56
+ base = parts[0]
57
+ subs = parts[1:]
58
+ base_latex = _GREEK.get(base, base)
59
+ sub_str = ",".join(subs)
60
+ return f"{base_latex}_{{{sub_str}}}"
61
+
62
+
63
+ def _is_physical(value: Any) -> bool:
64
+ """Retourne True si la valeur est un objet forallpeople.Physical."""
65
+ return hasattr(value, "latex") and hasattr(value, "dimensions")
66
+
67
+
68
+ def _num_to_latex(value: Any, precision: int) -> str:
69
+ """Convertit une valeur numérique en chaîne LaTeX (sans unité)."""
70
+ try:
71
+ if _is_physical(value):
72
+ # str(x) -> "1.500 MPa" : on extrait la partie numérique affichée
73
+ s = str(value)
74
+ parts = s.rsplit(" ", 1)
75
+ num_str = parts[0].strip()
76
+ try:
77
+ v = float(num_str)
78
+ if abs(v) >= 1e6 or (abs(v) < 1e-3 and v != 0):
79
+ return f"{v:.{precision}e}"
80
+ return f"{round(v, precision)}"
81
+ except ValueError:
82
+ return num_str
83
+ v = value
84
+ if isinstance(v, int):
85
+ return str(v)
86
+ if isinstance(v, float):
87
+ if abs(v) >= 1e6 or (abs(v) < 1e-3 and v != 0):
88
+ return f"{v:.{precision}e}"
89
+ return f"{round(v, precision)}"
90
+ return str(round(float(v), precision))
91
+ except Exception:
92
+ return str(value)
93
+
94
+
95
+ class _ASTNumRenderer(ast.NodeVisitor):
96
+ """Visite l'AST d'une expression et substitue les Name par leurs valeurs numériques.
97
+
98
+ Produit la forme numérique intermédiaire : \\min(0.77 + 3.5/25, 1).
99
+ """
100
+
101
+ def __init__(self, local_vars: dict, precision: int):
102
+ self.local_vars = local_vars
103
+ self.precision = precision
104
+
105
+ def render(self, node: ast.AST) -> str:
106
+ return self.visit(node)
107
+
108
+ def visit_Name(self, node: ast.Name) -> str:
109
+ name = node.id
110
+ if name in self.local_vars:
111
+ val = self.local_vars[name]
112
+ num = _num_to_latex(val, self.precision + 1)
113
+ unit = _get_unit_str(val)
114
+ return f"{num}{unit}" if unit else num
115
+ return _name_to_latex(name)
116
+
117
+ def visit_Constant(self, node: ast.Constant) -> str:
118
+ if isinstance(node.value, float):
119
+ return f"{node.value}"
120
+ return str(node.value)
121
+
122
+ def visit_BinOp(self, node: ast.BinOp) -> str:
123
+ sym = _ASTRenderer(self.local_vars, self.precision)
124
+ left = self.visit(node.left)
125
+ right = self.visit(node.right)
126
+ op = node.op
127
+ if isinstance(op, ast.Add):
128
+ return f"{left} + {right}"
129
+ if isinstance(op, ast.Sub):
130
+ return f"{left} - {right}"
131
+ if isinstance(op, ast.Mult):
132
+ return f"{left} \\cdot {right}"
133
+ if isinstance(op, ast.Div):
134
+ return f"\\frac{{{left}}}{{{right}}}"
135
+ if isinstance(op, ast.Pow):
136
+ base = left if _is_simple(node.left) else f"\\left({left}\\right)"
137
+ exp_str = right if _is_simple(node.right) else f"{{{right}}}"
138
+ return f"{base}^{{{exp_str}}}"
139
+ if isinstance(op, ast.FloorDiv):
140
+ return f"\\lfloor\\frac{{{left}}}{{{right}}}\\rfloor"
141
+ return f"{left} \\circ {right}"
142
+
143
+ def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
144
+ operand = self.visit(node.operand)
145
+ if isinstance(node.op, ast.USub):
146
+ return f"-{operand}"
147
+ return operand
148
+
149
+ def visit_Call(self, node: ast.Call) -> str:
150
+ func_name = ""
151
+ if isinstance(node.func, ast.Name):
152
+ func_name = node.func.id
153
+ elif isinstance(node.func, ast.Attribute):
154
+ func_name = node.func.attr
155
+ args = [self.visit(a) for a in node.args]
156
+ if func_name == "sqrt":
157
+ return f"\\sqrt{{{args[0]}}}" if args else r"\sqrt{\cdot}"
158
+ if func_name in ("sin", "cos", "tan"):
159
+ return f"\\{func_name}\\left({args[0]}\\right)" if args else f"\\{func_name}()"
160
+ if func_name in ("min", "max"):
161
+ return f"\\{func_name}\\left({', '.join(args)}\\right)"
162
+ if func_name in ("abs", "fabs"):
163
+ return f"\\left|{args[0]}\\right|" if args else r"\left|\cdot\right|"
164
+ if func_name == "log":
165
+ return f"\\ln\\left({args[0]}\\right)" if args else r"\ln(\cdot)"
166
+ if func_name == "exp":
167
+ return f"e^{{{args[0]}}}" if args else "e^{\\cdot}"
168
+ if func_name == "radians":
169
+ return f"\\frac{{\\pi \\cdot {args[0]}}}{{180}}" if args else ""
170
+ if func_name == "floor":
171
+ return f"\\lfloor {args[0]} \\rfloor" if args else ""
172
+ if func_name == "ceil":
173
+ return f"\\lceil {args[0]} \\rceil" if args else ""
174
+ return f"{_name_to_latex(func_name)}\\left({', '.join(args)}\\right)"
175
+
176
+ def visit_Compare(self, node: ast.Compare) -> str:
177
+ parts = [self.visit(node.left)]
178
+ ops_map = {ast.Lt: "<", ast.LtE: "\\leq", ast.Gt: ">", ast.GtE: "\\geq",
179
+ ast.Eq: "=", ast.NotEq: "\\neq"}
180
+ for op, comp in zip(node.ops, node.comparators):
181
+ parts.append(ops_map.get(type(op), "?"))
182
+ parts.append(self.visit(comp))
183
+ return " ".join(parts)
184
+
185
+ def visit_IfExp(self, node: ast.IfExp) -> str:
186
+ body = self.visit(node.body)
187
+ test = self.visit(node.test)
188
+ orelse = self.visit(node.orelse)
189
+ return f"{body} \\text{{ si }} {test} \\text{{ sinon }} {orelse}"
190
+
191
+ def generic_visit(self, node: ast.AST) -> str:
192
+ return "\\cdot"
193
+
194
+
195
+ class _ASTRenderer(ast.NodeVisitor):
196
+ """Visite l'AST d'une expression et génère du LaTeX."""
197
+
198
+ def __init__(self, local_vars: dict, precision: int):
199
+ self.local_vars = local_vars
200
+ self.precision = precision
201
+
202
+ def render(self, node: ast.AST) -> str:
203
+ return self.visit(node)
204
+
205
+ # --- Nœuds terminaux ---
206
+
207
+ def visit_Name(self, node: ast.Name) -> str:
208
+ return _name_to_latex(node.id)
209
+
210
+ def visit_Constant(self, node: ast.Constant) -> str:
211
+ if isinstance(node.value, float):
212
+ return f"{node.value}"
213
+ return str(node.value)
214
+
215
+ # --- Opérations binaires ---
216
+
217
+ def visit_BinOp(self, node: ast.BinOp) -> str:
218
+ left = self.visit(node.left)
219
+ right = self.visit(node.right)
220
+ op = node.op
221
+
222
+ if isinstance(op, ast.Add):
223
+ return f"{left} + {right}"
224
+ if isinstance(op, ast.Sub):
225
+ return f"{left} - {right}"
226
+ if isinstance(op, ast.Mult):
227
+ # Évite \cdot entre deux nombres
228
+ return f"{left} \\cdot {right}"
229
+ if isinstance(op, ast.Div):
230
+ return f"\\frac{{{left}}}{{{right}}}"
231
+ if isinstance(op, ast.Pow):
232
+ # Parenthèses si la base est complexe
233
+ base = left if _is_simple(node.left) else f"\\left({left}\\right)"
234
+ exp_str = right if _is_simple(node.right) else f"{{{right}}}"
235
+ return f"{base}^{{{exp_str}}}"
236
+ if isinstance(op, ast.FloorDiv):
237
+ return f"\\lfloor\\frac{{{left}}}{{{right}}}\\rfloor"
238
+ if isinstance(op, ast.Mod):
239
+ return f"{left} \\bmod {right}"
240
+ return f"{left} \\circ {right}"
241
+
242
+ def visit_UnaryOp(self, node: ast.UnaryOp) -> str:
243
+ operand = self.visit(node.operand)
244
+ if isinstance(node.op, ast.USub):
245
+ return f"-{operand}"
246
+ if isinstance(node.op, ast.UAdd):
247
+ return operand
248
+ return operand
249
+
250
+ # --- Appels de fonctions ---
251
+
252
+ def visit_Call(self, node: ast.Call) -> str:
253
+ func_name = ""
254
+ if isinstance(node.func, ast.Name):
255
+ func_name = node.func.id
256
+ elif isinstance(node.func, ast.Attribute):
257
+ func_name = node.func.attr
258
+
259
+ args_latex = [self.visit(a) for a in node.args]
260
+
261
+ if func_name == "sqrt":
262
+ return f"\\sqrt{{{args_latex[0]}}}" if args_latex else r"\sqrt{\cdot}"
263
+ if func_name in ("sin", "cos", "tan"):
264
+ return f"\\{func_name}\\left({args_latex[0]}\\right)" if args_latex else f"\\{func_name}()"
265
+ if func_name in ("min", "max"):
266
+ return f"\\{func_name}\\left({', '.join(args_latex)}\\right)"
267
+ if func_name == "abs" or func_name == "fabs":
268
+ return f"\\left|{args_latex[0]}\\right|" if args_latex else r"\left|\cdot\right|"
269
+ if func_name == "log":
270
+ return f"\\ln\\left({args_latex[0]}\\right)" if args_latex else r"\ln(\cdot)"
271
+ if func_name == "log10":
272
+ return f"\\log_{{10}}\\left({args_latex[0]}\\right)" if args_latex else r"\log_{10}(\cdot)"
273
+ if func_name == "exp":
274
+ return f"e^{{{args_latex[0]}}}" if args_latex else "e^{\\cdot}"
275
+ if func_name == "radians":
276
+ return f"\\frac{{\\pi \\cdot {args_latex[0]}}}{{180}}" if args_latex else ""
277
+ if func_name == "floor":
278
+ return f"\\lfloor {args_latex[0]} \\rfloor" if args_latex else ""
279
+ if func_name == "ceil":
280
+ return f"\\lceil {args_latex[0]} \\rceil" if args_latex else ""
281
+ # Fonction générique
282
+ name_latex = _name_to_latex(func_name)
283
+ return f"{name_latex}\\left({', '.join(args_latex)}\\right)"
284
+
285
+ # --- Comparaisons ---
286
+
287
+ def visit_Compare(self, node: ast.Compare) -> str:
288
+ parts = [self.visit(node.left)]
289
+ ops_map = {
290
+ ast.Lt: "<", ast.LtE: "\\leq", ast.Gt: ">", ast.GtE: "\\geq",
291
+ ast.Eq: "=", ast.NotEq: "\\neq",
292
+ }
293
+ for op, comp in zip(node.ops, node.comparators):
294
+ parts.append(ops_map.get(type(op), "?"))
295
+ parts.append(self.visit(comp))
296
+ return " ".join(parts)
297
+
298
+ # --- Tuple/List/Dict ---
299
+
300
+ def visit_Tuple(self, node: ast.Tuple) -> str:
301
+ return "\\left(" + ", ".join(self.visit(e) for e in node.elts) + "\\right)"
302
+
303
+ def visit_List(self, node: ast.List) -> str:
304
+ return "\\left[" + ", ".join(self.visit(e) for e in node.elts) + "\\right]"
305
+
306
+ def visit_Dict(self, node: ast.Dict) -> str:
307
+ return ""
308
+
309
+ def visit_Subscript(self, node: ast.Subscript) -> str:
310
+ val = self.visit(node.value)
311
+ sl = self.visit(node.slice)
312
+ return f"{val}_{{{sl}}}"
313
+
314
+ def visit_IfExp(self, node: ast.IfExp) -> str:
315
+ body = self.visit(node.body)
316
+ test = self.visit(node.test)
317
+ orelse = self.visit(node.orelse)
318
+ return f"{body} \\text{{ si }} {test} \\text{{ sinon }} {orelse}"
319
+
320
+ def generic_visit(self, node: ast.AST) -> str:
321
+ return "\\cdot"
322
+
323
+
324
+ def _is_simple(node: ast.AST) -> bool:
325
+ """Retourne True si le nœud est suffisamment simple pour ne pas nécessiter de parenthèses."""
326
+ return isinstance(node, (ast.Name, ast.Constant, ast.Attribute))
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # Extraction des lignes d'assignation depuis l'AST de la fonction
331
+ # ---------------------------------------------------------------------------
332
+
333
+ def _extract_assignments(func_source: str) -> list[ast.Assign]:
334
+ """Extrait les nœuds Assign et AugAssign du corps de la fonction."""
335
+ tree = ast.parse(func_source)
336
+ assignments = []
337
+ for node in ast.walk(tree):
338
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
339
+ for stmt in node.body:
340
+ if isinstance(stmt, ast.Assign):
341
+ assignments.append(stmt)
342
+ elif isinstance(stmt, ast.AugAssign):
343
+ assignments.append(stmt)
344
+ # Les Expr seuls (noms sans affectation comme `axe` ou `direction`)
345
+ # sont ignorés — c'était des "labels" handcalcs, inutiles en LaTeX
346
+ return assignments
347
+
348
+ # Rendu LaTeX d'une assignation
349
+ # ---------------------------------------------------------------------------
350
+
351
+ def _render_assignment(assign: ast.Assign, local_vars: dict, precision: int,
352
+ long: bool = False) -> str:
353
+ """Génère une ligne LaTeX 'lhs &= rhs_sym = rhs_num = valeur_unité'.
354
+
355
+ Si long=True, chaque étape (sym / num / résultat) est sur sa propre ligne alignée.
356
+ """
357
+ sym_renderer = _ASTRenderer(local_vars, precision)
358
+ num_renderer = _ASTNumRenderer(local_vars, precision)
359
+
360
+ # Cible (LHS)
361
+ target = assign.targets[0] if isinstance(assign, ast.Assign) else assign.target
362
+ lhs = sym_renderer.visit(target)
363
+
364
+ # Expression symbolique
365
+ rhs_sym = sym_renderer.visit(assign.value)
366
+
367
+ # Expression numérique (substitution des variables connues)
368
+ rhs_num = num_renderer.visit(assign.value)
369
+
370
+ # Valeur finale calculée
371
+ target_name = None
372
+ if isinstance(target, ast.Name):
373
+ target_name = target.id
374
+ elif isinstance(target, ast.Tuple):
375
+ target_name = None
376
+
377
+ has_num_sub = rhs_num != rhs_sym
378
+
379
+ if target_name and target_name in local_vars:
380
+ val = local_vars[target_name]
381
+ val_str = _num_to_latex(val, precision)
382
+ unit_str = _get_unit_str(val)
383
+ if unit_str:
384
+ val_str = f"{val_str}{unit_str}"
385
+ if long and has_num_sub:
386
+ return (
387
+ f"{lhs} &= {rhs_sym} \\\\\n"
388
+ f" &= {rhs_num} \\\\\n"
389
+ f" &= {val_str} \\\\\n"
390
+ )
391
+ elif long:
392
+ return (
393
+ f"{lhs} &= {rhs_sym} \\\\\n"
394
+ f" &= {val_str} \\\\\n"
395
+ )
396
+ elif has_num_sub:
397
+ return f"{lhs} &= {rhs_sym} = {rhs_num} = {val_str} \\\\\n"
398
+ else:
399
+ return f"{lhs} &= {rhs_sym} = {val_str} \\\\\n"
400
+ else:
401
+ if long and has_num_sub:
402
+ return (
403
+ f"{lhs} &= {rhs_sym} \\\\\n"
404
+ f" &= {rhs_num} \\\\\n"
405
+ )
406
+ elif has_num_sub:
407
+ return f"{lhs} &= {rhs_sym} = {rhs_num} \\\\\n"
408
+ return f"{lhs} &= {rhs_sym} \\\\\n"
409
+
410
+
411
+ def _get_unit_str(value: Any) -> str:
412
+ """Extrait l'unité d'une grandeur forallpeople en LaTeX (ex: \\mathrm{MPa})."""
413
+ try:
414
+ if _is_physical(value):
415
+ s = str(value)
416
+ parts = s.rsplit(" ", 1)
417
+ if len(parts) == 2:
418
+ unit = parts[1].strip()
419
+ unit = unit.replace("**", "^").replace("*", " \\cdot ")
420
+ return f"\\,\\mathrm{{{unit}}}"
421
+ except Exception:
422
+ pass
423
+ return ""
424
+
425
+
426
+ # ---------------------------------------------------------------------------
427
+ # Décorateur principal
428
+ # ---------------------------------------------------------------------------
429
+
430
+
431
+ def handcalc(
432
+ override: str = "short",
433
+ precision: int = 2,
434
+ jupyter_display: bool = False,
435
+ left: str = "\\[",
436
+ right: str = "\\]",
437
+ ):
438
+ """Remplace handcalcs.decorator.handcalc — compatible Python 3.13+.
439
+
440
+ Usage identique à l'original :
441
+
442
+ @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY,
443
+ left="\\\\[", right="\\\\]")
444
+ def val():
445
+ x = a + b
446
+ return x
447
+
448
+ latex_str, result = val()
449
+
450
+ Args:
451
+ override: "short" (défaut) ou "long".
452
+ precision: Nombre de décimales pour les valeurs numériques.
453
+ jupyter_display: Si True, affiche le LaTeX dans Jupyter via IPython.display.
454
+ left / right: Délimiteurs LaTeX du bloc (ex. "$$", "\\\\[").
455
+
456
+ Returns:
457
+ Décorateur qui transforme la fonction en un appelable retournant
458
+ ``(latex_str, résultat_numérique)``.
459
+ """
460
+ def decorator(func):
461
+ def wrapper(*args, **kwargs):
462
+ # 1. Exécuter la fonction pour obtenir les valeurs numériques
463
+ result = func(*args, **kwargs)
464
+
465
+ # 2. Récupérer le code source de la fonction
466
+ try:
467
+ raw_source = inspect.getsource(func)
468
+ func_source = textwrap.dedent(raw_source)
469
+ except (OSError, TypeError):
470
+ # Impossible de récupérer le source → retour sans LaTeX
471
+ return ("", result)
472
+
473
+ # 3. Construire le dict des variables locales disponibles
474
+ # = globals du scope + fermeture (closure) + arguments
475
+ local_vars: dict = {}
476
+
477
+ # Variables du scope module (globals) — permet la substitution numérique
478
+ if func.__globals__:
479
+ local_vars.update({
480
+ k: v for k, v in func.__globals__.items()
481
+ if not k.startswith("__") and not callable(v)
482
+ })
483
+
484
+ # Variables de fermeture (closures)
485
+ if func.__code__.co_freevars and func.__closure__:
486
+ for name, cell in zip(func.__code__.co_freevars, func.__closure__):
487
+ try:
488
+ local_vars[name] = cell.cell_contents
489
+ except ValueError:
490
+ pass
491
+
492
+
493
+ # Arguments positionnels (cas `def comp(arg1, arg2, ...)`)
494
+ arg_names = func.__code__.co_varnames[: func.__code__.co_argcount]
495
+ for name, val in zip(arg_names, args):
496
+ local_vars[name] = val
497
+ local_vars.update(kwargs)
498
+
499
+ # Résultats de la fonction — injecte les valeurs calculées
500
+ # On récupère les noms des variables locales via co_varnames
501
+ # En exécutant la fonction une seconde fois dans un namespace capturé
502
+ local_capture: dict = dict(local_vars)
503
+ try:
504
+ _exec_and_capture(func, args, kwargs, local_capture)
505
+ except Exception:
506
+ pass
507
+ local_vars.update(local_capture)
508
+
509
+ # 4. Extraire les assignations de l'AST
510
+ try:
511
+ assignments = _extract_assignments(func_source)
512
+ except SyntaxError:
513
+ return ("", result)
514
+
515
+ # 5. Construire le LaTeX
516
+ is_long = override == "long"
517
+ lines = []
518
+ for assign in assignments:
519
+ line = _render_assignment(assign, local_vars, precision, long=is_long)
520
+ lines.append(line)
521
+
522
+ if not lines:
523
+ latex_body = ""
524
+ elif override == "long" or len(lines) > 1:
525
+ inner = "".join(lines)
526
+ latex_body = f"\\begin{{aligned}}\n{inner}\\end{{aligned}}"
527
+ else:
528
+ # short : une seule ligne, format compact
529
+ latex_body = "".join(lines).replace(" &=", " =").replace("\\\\\n", "")
530
+
531
+ latex_str = f"{left}\n{latex_body}\n{right}"
532
+
533
+ # 6. Affichage Jupyter si demandé
534
+ if jupyter_display:
535
+ try:
536
+ from IPython.display import display, Latex as IPyLatex
537
+ if display and IPyLatex:
538
+ display(IPyLatex(latex_str))
539
+ except ImportError:
540
+ pass
541
+
542
+ return (latex_str, result)
543
+
544
+ return wrapper
545
+ return decorator
546
+
547
+
548
+ # ---------------------------------------------------------------------------
549
+ # Capture des variables locales sans innerscope
550
+ # ---------------------------------------------------------------------------
551
+
552
+ def _exec_and_capture(func, args, kwargs, capture: dict) -> None:
553
+ """Exécute func dans un namespace étendu et capture les variables locales.
554
+
555
+ Utilise exec() sur le corps de la fonction avec les variables de fermeture
556
+ et arguments déjà présents dans `capture`. Met à jour `capture` in-place.
557
+ """
558
+ try:
559
+ raw = inspect.getsource(func)
560
+ source = textwrap.dedent(raw)
561
+ except (OSError, TypeError):
562
+ return
563
+
564
+ tree = ast.parse(source)
565
+
566
+ # On cherche le premier FunctionDef
567
+ func_def = None
568
+ for node in ast.walk(tree):
569
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
570
+ func_def = node
571
+ break
572
+ if func_def is None:
573
+ return
574
+
575
+ # Reconstruire le corps sans le 'return' final comme module exécutable
576
+ body_stmts = [s for s in func_def.body if not isinstance(s, ast.Return)]
577
+ if not body_stmts:
578
+ return
579
+
580
+ # Créer un nouveau module AST avec ces instructions
581
+ new_module = ast.Module(body=body_stmts, type_ignores=[])
582
+ ast.fix_missing_locations(new_module)
583
+
584
+ # Injecter les fonctions math disponibles + globals de la fonction (scope module)
585
+ exec_globals = {
586
+ "sqrt": sqrt, "pi": pi, "sin": sin, "cos": cos, "tan": tan,
587
+ "radians": radians, "log": log, "log10": log10, "exp": exp,
588
+ "floor": floor, "ceil": ceil, "fabs": fabs, "abs": abs,
589
+ "min": min, "max": max, "round": round,
590
+ }
591
+ if func.__globals__:
592
+ exec_globals.update(func.__globals__)
593
+ exec_globals.update(capture)
594
+
595
+ try:
596
+ exec(compile(new_module, "<handcalc>", "exec"), exec_globals, capture)
597
+ except Exception:
598
+ pass
@@ -1,7 +1,7 @@
1
1
  from math import *
2
2
 
3
3
  import forallpeople as si
4
- from handcalcs.decorator import handcalc
4
+ from ourocode.eurocode.core.renderer import handcalc
5
5
 
6
6
  from ourocode.eurocode.core.projet import Projet
7
7
 
@@ -56,7 +56,7 @@ class Exploitation(Projet):
56
56
  q_k = self.qk
57
57
 
58
58
  if self.categorie in ("A_plancher", "A_escalier", "A_balcons", "B", "C3", "D1", "F"):
59
- @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY, left="\[", right="\]")
59
+ @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY, left="\\[", right="\\]")
60
60
  def val():
61
61
  alpha_A = min(0.77 + A_0 / A, 1) #EN 1991-1-1 et son AN §6.2.1(4) et 6.3.1.2(10)
62
62
  q_k_alpha_A = q_k * alpha_A
@@ -80,14 +80,14 @@ class Exploitation(Projet):
80
80
  """
81
81
  if n > 2:
82
82
  if self.categorie in ("B", "F"):
83
- @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY, left="\[", right="\]")
83
+ @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY, left="\\[", right="\\]")
84
84
  def val():
85
85
  alpha_n = 0.7 + 0.8 / n #EN 1991-1-1 §6.2.2(2) et 6.3.1.2(11)
86
86
  return alpha_n
87
87
  return val()
88
88
 
89
89
  elif self.categorie in ("A_plancher", "A_escalier", "A_balcons"):
90
- @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY, left="\[", right="\]")
90
+ @handcalc(override="short", precision=2, jupyter_display=self.JUPYTER_DISPLAY, left="\\[", right="\\]")
91
91
  def val():
92
92
  alpha_n = 0.5 + 1.36 / n #EN 1991-1-1 §6.2.2(2) et 6.3.1.2(11)
93
93
  return alpha_n
@@ -1,11 +1,11 @@
1
- #! env\Scripts\python.exe
1
+ #! env\Scripts\python.exe
2
2
  # Encoding in UTF-8 by Anthony PARISOT
3
3
  from math import sin, radians
4
4
  import pandas as pd
5
5
  import warnings
6
6
 
7
7
  import forallpeople as si
8
- from handcalcs.decorator import handcalc
8
+ from ourocode.eurocode.core.renderer import handcalc
9
9
 
10
10
  from ourocode.eurocode.core.batiment import Batiment
11
11