diff --git a/CHANGELOG.md b/CHANGELOG.md index b66509d..14eba34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ Toutes les modifications notables de ce projet seront documentées dans ce fichi Le format est basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et ce projet adhère au [Semantic Versioning](https://semver.org/lang/fr/). +## [2.1.3] + +### Added +- **`Verification_EC5`** : intégration de la vérification feu (EN 1995-1-2) dans le processus de vérification EC5. + - Ajout du paramètre `t_expo` (durée d'exposition au feu en minutes, défaut 30) dans `__init__` et `verify`. + - Vérifications feu (Flexion_feu, Compression_feu, Traction_feu, Cisaillement_feu) calculées uniquement pour les combinaisons contenant "FIRE" (ex: ELU_STR_ACC_FIRE). + - Vérifications normales (Flexion, Compression, Traction, Cisaillement) calculées uniquement pour les combinaisons ne contenant pas "FIRE". + - Configuration par défaut : aucune protection des faces (haut, bas, gauche, droite = "Aucune protection"). + - Logique de comparaison des contraintes pour déterminer le mode dominant : si `sigma_c_0_rd > max(sigma_m_rd_y, sigma_m_rd_z)`, la compression est considérée dominante et l'interaction flexion-compression est adaptée en conséquence. Cette logique s'applique aux combinaisons normales et FIRE. +- **`EC3` - Classe `Profil`** : nouvelle classe pour les profilés laminés acier selon EN 1993-1-1 en cours de développement. + - Permet de générer des poutres en I ou H (IPE, HEA, HEB) à partir de désignations normalisées selon EN 10365. + - Chargement automatique des caractéristiques géométriques depuis `data/profil_acier.csv` (h, b, tw, tf, r, A, Av, Iy, Iz, Wely, Welz, Wply, Wplz, iy, iz, masse). + - Détermination automatique des propriétés mécaniques (fy, fu) selon la classe d'acier et l'épaisseur maximale (max(tw, tf)). + - Hérite de `Plat` (au lieu de `Projet`) pour être compatible avec les classes de vérification (Compression, Traction, Cisaillement, Flexion) via `_from_parent_class`. +- **`data/profil_acier.csv`** : nouveau fichier de données contenant les caractéristiques géométriques des profils standards IPE (80-600), HEA (100-600), HEB (100-600) et UPN (80-200) selon EN 10365. +- **`tests/test_EC3_Element_droit.py`** : ajout de la classe `TestProfil` avec 10 tests couvrant l'initialisation (IPE, HEA, HEB), le chargement des caractéristiques géométriques, les propriétés mécaniques (fy, fu), les validations (désignation, classe acier, classe transversale) et la compatibilité avec Compression. + +### Changed +- **`Compression_feu`** : séparation des paramètres `type_appuis` en `type_appuis_y` et `type_appuis_z` pour une gestion indépendante des conditions d'appui selon les axes de flambement, cohérente avec la classe `Compression` normale. + +### Added (tests) +- **`tests/test_EC5_Verification.py`** : nouvelle classe `TestFireVerification` avec 4 tests couvrant la vérification feu : + - Vérification que les combinaisons FIRE n'ont pas de vérifications normales. + - Vérification que les combinaisons FIRE ont des vérifications feu. + - Vérification que les combinaisons normales n'ont pas de vérifications feu. + - Vérification que le paramètre `t_expo` est correctement utilisé. +- Fixture `fire_beam_verif` pour créer un modèle avec combinaisons FIRE (ELU_STR_ACC=True). + ## [2.1.2] ### Fixed diff --git a/ourocode/data/profil_acier.csv b/ourocode/data/profil_acier.csv new file mode 100644 index 0000000..94ac338 --- /dev/null +++ b/ourocode/data/profil_acier.csv @@ -0,0 +1,21 @@ +type;designation;h;b;tw;tf;r1;A;Av;Avz;Iy;Iz;Wely;Welz;Wply;Wplz;iy;iz;masse +A refaire complètement ! +HEA;HEA 100;96;100;5.0;8.0;12;21.24;12.0;12.0;349;133.5;69.8;26.7;80.1;41.3;4.06;2.51 +HEA;HEA 120;120;120;5.0;8.0;12;25.34;14.0;14.0;606;229.3;101.1;38.2;116.5;59.9;4.90;3.01 +HEA;HEA 140;140;140;5.5;8.5;12;31.42;16.8;16.8;1033;389.2;147.6;55.6;172.5;89.3;5.74;3.52 +HEA;HEA 160;160;160;6.0;9.0;15;38.77;20.2;20.2;1670;623.9;208.7;78.0;249.3;130.7;6.57;4.02 +HEA;HEA 180;180;180;6.5;9.5;15;45.25;23.8;23.8;2510;924.6;278.9;102.7;329.8;171.7;7.45;4.52 +HEA;HEA 200;200;200;7.0;10.0;18;53.83;27.6;27.6;3692;1335.6;369.2;133.6;431.6;222.8;8.28;4.98 +HEA;HEA 220;220;220;7.5;10.5;18;63.53;31.6;31.6;5409;1955.4;492.6;177.8;571.8;294.4;9.23;5.55 +HEA;HEA 240;240;240;7.5;11.0;21;73.85;36.0;36.0;7762;2768.7;646.8;230.7;773.2;391.2;10.27;6.13 +HEA;HEA 260;260;260;8.0;11.5;21;84.37;40.6;40.6;10455;3667.6;804.3;282.1;959.1;507.6;11.11;6.60 +HEA;HEA 280;280;280;8.5;12.0;24;97.30;46.2;46.2;13670;4762.8;976.4;339.9;1165.8;626.5;11.86;7.00 +HEA;HEA 300;300;300;9.0;12.0;24;111.78;51.6;51.6;18262;6310.4;1217.5;420.7;1431.8;836.2;12.78;7.52 +HEA;HEA 320;320;320;10.0;13.5;27;131.13;59.0;59.0;24529;8172.7;1533.0;510.8;1798.8;1072.3;13.70;7.89 +HEA;HEA 340;340;340;10.0;14.5;27;142.94;65.4;65.4;31533;10474.3;1855.0;616.1;2158.9;1308.9;14.85;8.56 +HEA;HEA 360;360;360;12.5;16.5;27;171.18;75.6;75.6;43191;14178.6;2399.5;787.7;2849.2;1674.9;15.88;9.10 +HEA;HEA 400;400;400;13.5;18.0;27;198.58;86.2;86.2;63222;20580.3;3161.1;1029.0;3765.8;2191.2;17.85;10.19 +HEA;HEA 450;450;450;14.0;20.0;27;237.66;100.2;100.2;93819;29798.3;4170.0;1324.4;4880.9;2816.3;19.88;11.19 +HEA;HEA 500;500;500;16.0;23.0;27;298.31;123.4;123.4;131932;42385.5;5277.3;1695.4;6208.0;3616.8;21.04;11.93 +HEA;HEA 550;550;550;16.5;24.5;27;354.41;143.8;143.8;178830;56837.2;6502.9;2066.8;7648.2;4388.9;22.44;12.67 +HEA;HEA 600;600;600;18.0;28.0;27;428.31;170.4;170.4;238620;78762.8;7954.0;2625.4;9370.9;5318.6;23.63;13.57 diff --git a/ourocode/eurocode/core/renderer.py b/ourocode/eurocode/core/_renderer.py similarity index 100% rename from ourocode/eurocode/core/renderer.py rename to ourocode/eurocode/core/_renderer.py diff --git a/ourocode/eurocode/core/objet.py b/ourocode/eurocode/core/objet.py index 34b387f..e6430fe 100644 --- a/ourocode/eurocode/core/objet.py +++ b/ourocode/eurocode/core/objet.py @@ -115,7 +115,7 @@ def set_value(self, value: float | int | str = None, unit: str = MathUtilsMixin. return numeric * self._PHYSICAL_UNITS[unit] - def get_value(self, value: dict|list|str, index: int=None, key: str=None, get_keys: bool=("False", "True"),): + def get_value(self, value: dict|list|str, index: int|None=None, key: str=None, get_keys: bool=("False", "True"),): """Extrait et retourne une valeur depuis une structure de données complexe. Cette méthode utilitaire permet de naviguer dans les dictionnaires, @@ -141,9 +141,9 @@ def get_value(self, value: dict|list|str, index: int=None, key: str=None, get_ke >>> obj.get_value({"x": 1, "y": 2}, get_keys=True) ["x", "y"] """ - if index and isinstance(value, list): + if index is not None and isinstance(value, list): value = value[index] - elif index and isinstance(value, str): + elif index is not None and isinstance(value, str): value = list(value)[index] elif get_keys and isinstance(value, dict): value = list(value.keys()) diff --git a/ourocode/eurocode/ec1/exploitation.py b/ourocode/eurocode/ec1/exploitation.py index 4be12ee..ee2a6a0 100644 --- a/ourocode/eurocode/ec1/exploitation.py +++ b/ourocode/eurocode/ec1/exploitation.py @@ -1,7 +1,7 @@ from math import * import forallpeople as si -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.core.projet import Projet diff --git a/ourocode/eurocode/ec1/neige.py b/ourocode/eurocode/ec1/neige.py index 1c1c293..27dfd5e 100644 --- a/ourocode/eurocode/ec1/neige.py +++ b/ourocode/eurocode/ec1/neige.py @@ -5,7 +5,7 @@ import warnings import forallpeople as si -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.core.batiment import Batiment diff --git a/ourocode/eurocode/ec1/vent.py b/ourocode/eurocode/ec1/vent.py index dfbb18d..87b84dc 100644 --- a/ourocode/eurocode/ec1/vent.py +++ b/ourocode/eurocode/ec1/vent.py @@ -9,7 +9,8 @@ import pandas as pd import forallpeople as si -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc +from ourocode.eurocode.mixins.math_utils import MathUtilsMixin # sys.path.append(os.path.join(os.getcwd(), "ourocode")) # from eurocode.A0_Projet import Batiment @@ -18,22 +19,6 @@ si.environment("structural") -def interpolation_lineaire(x, xa, xb, ya, yb): - """Fait une interpolation linéaire pour trouver un résultat y entre deux valeur xa et xb""" - y = ya + (x - xa) * ((yb - ya) / (xb - xa)) - return y - - -def interpolation_logarithmique(x, xa, xb, ya, yb): - """Fait une interpolation linéaire pour trouver un résultat y entre deux valeur xa et xb""" - y = ( - (x > xb) * yb - + (x < xa) * ya - + (x <= xb) * (x >= 1) * (ya - (ya - yb) * mt.log10(x)) - ) - return y - - class Vent(Batiment): RHO_AIR = 1.225 * si.kg / si.m**3 CPI = [0.2, -0.3] @@ -581,7 +566,7 @@ def _Cpe(self, cle: str): if borne_inf: for i in range(self._df.shape[0]): list_CPE_line.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( h_d, borne_inf, borne_sup, @@ -598,7 +583,7 @@ def _Cpe(self, cle: str): self._df.loc[self._df.shape[0]] = [ self._df.iloc[i, 0], "CPE " + str(round(self.load_area, 2)), - interpolation_logarithmique( + MathUtilsMixin.interpolation_logarithmique( self.load_area, 1, 10, @@ -734,7 +719,7 @@ def _Cpe(self): row = [round(self._hp_h, 3), cpe] for j in range(2, num_columns): row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self._hp_h, minimum, maximum, @@ -750,7 +735,7 @@ def _Cpe(self): row = [df.iloc[i, 0], "CPE " + str(round(self.load_area, 2))] for j in range(2, num_columns): row.append( - interpolation_logarithmique( + MathUtilsMixin.interpolation_logarithmique( self.load_area, 1, 10, df.iloc[i + 1, j], df.iloc[i, j] ) ) @@ -941,7 +926,7 @@ def _Cpe(self, direction: str): row.append(df_max.iloc[i, j]) else: row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self.alpha_toit, minimum, maximum, @@ -957,7 +942,7 @@ def _Cpe(self, direction: str): row = [df.iloc[i, 0], "CPE " + str(round(self.load_area, 2))] for j in range(2, num_columns): row.append( - interpolation_logarithmique( + MathUtilsMixin.interpolation_logarithmique( self.load_area, 1, 10, df.iloc[i + 1, j], df.iloc[i, j] ) ) @@ -1166,7 +1151,7 @@ def _Cpe(self, direction: str): row.append(df_max.iloc[i, j]) else: row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self.alpha_toit, minimum, maximum, @@ -1182,7 +1167,7 @@ def _Cpe(self, direction: str): row = [df.iloc[i, 0], "CPE " + str(round(self.load_area, 2))] for j in range(2, num_columns): row.append( - interpolation_logarithmique( + MathUtilsMixin.interpolation_logarithmique( self.load_area, 1, 10, df.iloc[i + 1, j], df.iloc[i, j] ) ) @@ -1304,7 +1289,7 @@ def _Cp(self): row = [self._df.iloc[i, 0], self.phi] for j in range(2, 6): row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self.phi, 0, 1, @@ -1346,7 +1331,7 @@ def _Cp(self): row.append(df_max.iloc[i, j]) else: row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self.alpha_toit, minimum, maximum, @@ -1480,7 +1465,7 @@ def _Cp(self): row = [self._df.iloc[i, 0], self.phi] for j in range(2, 7): row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self.phi, 0, 1, @@ -1522,7 +1507,7 @@ def _Cp(self): row.append(df_max.iloc[i, j]) else: row.append( - interpolation_lineaire( + MathUtilsMixin.interpolation_lineaire( self.alpha_toit, minimum, maximum, diff --git a/ourocode/eurocode/ec3/assemblage/platine.py b/ourocode/eurocode/ec3/assemblage/platine.py index 6bccb03..fb98eb9 100644 --- a/ourocode/eurocode/ec3/assemblage/platine.py +++ b/ourocode/eurocode/ec3/assemblage/platine.py @@ -6,7 +6,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec3.assemblage.tige import Tige from ourocode.eurocode.ec3.element_droit.plat import Plat diff --git a/ourocode/eurocode/ec3/assemblage/soudure.py b/ourocode/eurocode/ec3/assemblage/soudure.py index e791c1e..49233b4 100644 --- a/ourocode/eurocode/ec3/assemblage/soudure.py +++ b/ourocode/eurocode/ec3/assemblage/soudure.py @@ -9,7 +9,7 @@ from matplotlib.patches import Rectangle, Circle import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec3.element_droit.plat import Plat diff --git a/ourocode/eurocode/ec3/assemblage/tige.py b/ourocode/eurocode/ec3/assemblage/tige.py index 170730a..722a19c 100644 --- a/ourocode/eurocode/ec3/assemblage/tige.py +++ b/ourocode/eurocode/ec3/assemblage/tige.py @@ -7,7 +7,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec3.element_droit.plat import Plat diff --git a/ourocode/eurocode/ec3/element_droit/__init__.py b/ourocode/eurocode/ec3/element_droit/__init__.py index 4c62a7e..3e19b3e 100644 --- a/ourocode/eurocode/ec3/element_droit/__init__.py +++ b/ourocode/eurocode/ec3/element_droit/__init__.py @@ -1,7 +1,8 @@ from ourocode.eurocode.ec3.element_droit.plat import Plat +from ourocode.eurocode.ec3.element_droit.profil import Profil from ourocode.eurocode.ec3.element_droit.traction import Traction from ourocode.eurocode.ec3.element_droit.compression import Compression from ourocode.eurocode.ec3.element_droit.cisaillement import Cisaillement from ourocode.eurocode.ec3.element_droit.flexion import Flexion -__all__ = ["Plat", "Traction", "Compression", "Cisaillement", "Flexion"] +__all__ = ["Plat", "Profil", "Traction", "Compression", "Cisaillement", "Flexion"] diff --git a/ourocode/eurocode/ec3/element_droit/cisaillement.py b/ourocode/eurocode/ec3/element_droit/cisaillement.py index 91da76b..ba3b955 100644 --- a/ourocode/eurocode/ec3/element_droit/cisaillement.py +++ b/ourocode/eurocode/ec3/element_droit/cisaillement.py @@ -5,7 +5,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec3.element_droit.plat import Plat diff --git a/ourocode/eurocode/ec3/element_droit/compression.py b/ourocode/eurocode/ec3/element_droit/compression.py index 597cee0..0f7490d 100644 --- a/ourocode/eurocode/ec3/element_droit/compression.py +++ b/ourocode/eurocode/ec3/element_droit/compression.py @@ -5,7 +5,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec3.element_droit.plat import Plat @@ -17,7 +17,7 @@ class Compression(Plat): "Encastré - Rotule" : 0.7, "Encastré - Encastré" : 0.5, "Encastré - Rouleau" : 1} - def __init__(self, A: si.mm**2, Iy:si.mm**4, Iz:si.mm**4, lo_y: si.mm=0, lo_z: si.mm=0, courbe_flamb: dict="{'y':'c', 'z':'c'}", type_appuis: str=COEF_LF, *args, **kwargs): + def __init__(self, A: si.mm**2, Iy:si.mm**4, Iz:si.mm**4, lo_y: si.mm=0, lo_z: si.mm=0, courbe_flamb: dict="{'y':'c', 'z':'c'}", type_appuis_y: str=COEF_LF, type_appuis_z: str=COEF_LF, *args, **kwargs): """ Classe intégrant les formules de compression et d'instabilité au flambement à l'EC3. Cette classe est hérité de la classe Plat du module EC3_Element_droit.py. @@ -28,12 +28,18 @@ def __init__(self, A: si.mm**2, Iy:si.mm**4, Iz:si.mm**4, lo_y: si.mm=0, lo_z: s Iz (float): Moment quadratique suivant l'axe de rotation z en mm4 lo_y (float): Longueur de flambement suivant l'axe de rotation y en mm. Defaults to 0. lo_z (float): Longueur de flambement suivant l'axe de rotation z en mm. Defaults to 0. - type_appuis (str): Permet de déterminé la forme du flambement en fonction des types d'appui: - Encastré 1 côté : 2 - Rotule - Rotule : 1 - Encastré - Rotule : 0.7 - Encastré - Encastré : 0.5 - Encastré - Rouleau : 1 + type_appuis_y (str): Conditions d'appui pour le flambement selon l'axe de rotation y. + Détermine β_y dans lf_y = β_y · lo_y. + Valeurs possibles (voir COEF_LF): + - "Encastré 1 côté" : β = 2.0 (console) + - "Rotule - Rotule" : β = 1.0 (articulé-articulé) + - "Encastré - Rotule" : β = 0.7 + - "Encastré - Encastré" : β = 0.5 + - "Encastré - Rouleau" : β = 1.0 (encastré-glissière) + Defaults to "Rotule - Rotule". + type_appuis_z (str, optional): Conditions d'appui pour le flambement + selon l'axe de rotation z. + """ super().__init__(*args, **kwargs) self.A = A * si.mm**2 @@ -41,8 +47,10 @@ def __init__(self, A: si.mm**2, Iy:si.mm**4, Iz:si.mm**4, lo_y: si.mm=0, lo_z: s self.Iz = Iz * si.mm**4 self.lo ={'y': lo_y*si.mm, 'z': lo_z*si.mm} self.courbe_flamb = courbe_flamb - self.type_appuis = type_appuis - self.coef_lef = self.COEF_LF[type_appuis] + self.type_appuis_y = type_appuis_y + self.type_appuis_z = type_appuis_z + self.coef_lef_y = self.COEF_LF[type_appuis_y] + self.coef_lef_z = self.COEF_LF[type_appuis_z] @@ -76,8 +84,8 @@ def lamb(self): dict: {"y": λ_y, "z": λ_z} (élancements sans unité). """ lamb = {'y':0, 'z':0} - lamb['y'] = (self.lo['y'].value * 10**3 * self.coef_lef) / mt.sqrt(self.Iy / (self.A)) - lamb['z'] = (self.lo['z'].value * 10**3 * self.coef_lef) / mt.sqrt(self.Iz / (self.A)) + lamb['y'] = (self.lo['y'].value * 10**3 * self.coef_lef_y) / mt.sqrt(self.Iy / (self.A)) + lamb['z'] = (self.lo['z'].value * 10**3 * self.coef_lef_z) / mt.sqrt(self.Iz / (self.A)) return lamb @property diff --git a/ourocode/eurocode/ec3/element_droit/flexion.py b/ourocode/eurocode/ec3/element_droit/flexion.py index 3d5c076..df911d8 100644 --- a/ourocode/eurocode/ec3/element_droit/flexion.py +++ b/ourocode/eurocode/ec3/element_droit/flexion.py @@ -5,7 +5,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec3.element_droit.plat import Plat from ourocode.eurocode.ec3.element_droit.cisaillement import Cisaillement diff --git a/ourocode/eurocode/ec3/element_droit/plat.py b/ourocode/eurocode/ec3/element_droit/plat.py index fd08ba2..3b5e09f 100644 --- a/ourocode/eurocode/ec3/element_droit/plat.py +++ b/ourocode/eurocode/ec3/element_droit/plat.py @@ -6,7 +6,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.core.projet import Projet diff --git a/ourocode/eurocode/ec3/element_droit/profil.py b/ourocode/eurocode/ec3/element_droit/profil.py new file mode 100644 index 0000000..154d72b --- /dev/null +++ b/ourocode/eurocode/ec3/element_droit/profil.py @@ -0,0 +1,125 @@ +# coding in UTF-8 +# by Anthony PARISOT +import pandas as pd + +import forallpeople as si +si.environment("structural") + +from ourocode.eurocode.ec3.element_droit.plat import Plat + + +class _Profil(Plat): + """Classe pour les profilés laminés acier selon EN 1993-1-1. + + Permet de générer des poutres en I ou H (IPE, HEA, HEB, UPN, etc.) + à partir de désignations normalisées selon EN 10365. + + Les caractéristiques géométriques sont chargées depuis le CSV + `profil_acier.csv` et les propriétés mécaniques (fy, fu) sont + déterminées selon la classe d'acier et l'épaisseur maximale. + + Hérite de Plat pour être compatible avec les classes de vérification + (Compression, Traction, Flexion, Cisaillement) via _from_parent_class. + """ + # Chargement des données de profils + _PROFIL_DATA = Plat._data_from_csv(Plat, "profil_acier.csv", index_col=None) + TYPES_PROFIL = tuple(_PROFIL_DATA["type"].unique()) + DESIGNATIONS = tuple(_PROFIL_DATA["designation"]) + + def __init__(self, designation: str, classe_acier: str = "S235", + classe_transv: int = 1, **kwargs): + """Initialise un profilé laminé acier. + + Args: + designation (str): Désignation du profil (ex: "IPE 300", "HEA 200", "HEB 260") + classe_acier (str, optional): Classe d'acier selon EN 1993-1-1 Tableau 3.1 + (ex: "S235", "S275", "S355"). Defaults to "S235". + classe_transv (int, optional): Classe transversale de la section (1, 2 ou 3) + selon EN 1993-1-1 §5.5. Defaults to 1. + **kwargs: Arguments transmis à la classe parent Plat. + + Raises: + ValueError: Si designation n'est pas dans DESIGNATIONS. + """ + # Validation de la désignation avant appel du parent + if designation not in self.DESIGNATIONS: + raise ValueError( + f"Désignation '{designation}' invalide. Valeurs acceptées : {self.DESIGNATIONS}" + ) + + self.designation = designation + + # Chargement des données géométriques depuis le CSV + self._load_profil_data() + + # Appel du parent Plat avec t=tw (épaisseur âme) + # h et b ne sont pas passés car ils seraient écrasés par Plat + super().__init__(t=self.tw.value*10**3, h=self.h.value*10**3, b=self.b.value*10**3, + classe_acier=classe_acier, classe_transv=classe_transv, + **kwargs) + + # Restauration des valeurs de h et b après l'appel du parent + self.h = self._h_backup + self.b = self._b_backup + + # Surcharge de fy et fu selon l'épaisseur maximale (max(tw, tf)) + self.__fy_fu() + + def _load_profil_data(self): + """Charge les caractéristiques géométriques du profil depuis le CSV.""" + df = self._PROFIL_DATA + profil = df[df["designation"] == self.designation].iloc[0] + + # Dimensions géométriques + self.h = profil["h"] * si.mm + self.b = profil["b"] * si.mm + self.tw = profil["tw"] * si.mm # épaisseur âme + self.tf = profil["tf"] * si.mm # épaisseur semelle + self.r = profil["r"] * si.mm # rayon congé + + # Sauvegarde pour restauration après appel super().__init__ + self._h_backup = self.h + self._b_backup = self.b + + # Caractéristiques de section + self.A = profil["A"] * si.mm**2 + self.Av = profil["Av"] * si.mm**2 + self.Avz = profil["Avz"] * si.mm**2 if pd.notna(profil["Avz"]) else None + + # Moments quadratiques + self.Iy = profil["Iy"] * si.mm**4 + self.Iz = profil["Iz"] * si.mm**4 + + # Modules de flexion + self.Wely = profil["Wely"] * si.mm**3 + self.Welz = profil["Welz"] * si.mm**3 + self.Wply = profil["Wply"] * si.mm**3 + self.Wplz = profil["Wplz"] * si.mm**3 + + # Rayons de giration + self.iy = profil["iy"] * si.mm + self.iz = profil["iz"] * si.mm + + # Masse linéique + self.masse = profil["masse"] * si.kg / si.m + + def __fy_fu(self): + """Définit fy et fu en fonction de la classe d'acier et de l'épaisseur maximale. + + Surcharge la méthode de Plat pour utiliser max(tw, tf) au lieu de t uniquement, + car les profilés ont deux épaisseurs différentes (âme et semelle). + """ + t_max = max(self.tw, self.tf) + + if t_max <= 40 * si.mm: + self.fy = self._Plat__classe_acier.loc["t<= 40 fy"] * si.MPa + self.fu = self._Plat__classe_acier.loc["t<= 40 fu"] * si.MPa + elif 40 * si.mm < t_max <= 80 * si.mm: + self.fy = self._Plat__classe_acier.loc["40 dict: """Vérifie une barre structurale en bouclant sur toutes les combos ELU. @@ -251,6 +262,9 @@ def verify( (fallback ``"Rotule - Rotule"`` si absente). type_appuis_z (str, optional): Surcharge du type d'appui de flambement selon z. Même logique que ``type_appuis_y``. + t_expo (int, optional): Surcharge de la durée d'exposition au feu en minutes + pour la vérification incendie (aucune protection des faces). + Si ``None`` (défaut), la valeur de l'initialisation est utilisée. Returns: dict: ``{"name", "taux", "dataframe"}`` où : @@ -268,6 +282,9 @@ def verify( """ self._check_state() + # Surcharge t_expo si fourni + t_expo = t_expo if t_expo is not None else self.t_expo + w_inst_tag = self.DEFAULT_W_INST_TAG w_net_fin_tag = self.DEFAULT_W_NET_FIN_TAG sm = self._model_generator.get_structural_member(name) @@ -295,6 +312,10 @@ def verify( "Cisaillement": {"taux": 0.0, "combinaison": None}, "Traction": {"taux": 0.0, "combinaison": None}, "Compression": {"taux": 0.0, "combinaison": None}, + "Flexion_feu": {"taux": 0.0, "combinaison": None}, + "Cisaillement_feu": {"taux": 0.0, "combinaison": None}, + "Traction_feu": {"taux": 0.0, "combinaison": None}, + "Compression_feu": {"taux": 0.0, "combinaison": None}, } # --- Boucle sur les combinaisons ELU @@ -310,6 +331,7 @@ def verify( pos_charge=pos_charge, type_appuis_y=type_appuis_y, type_appuis_z=type_appuis_z, + t_expo=t_expo, return_objects=True, ) # Stockage dans le cache pour get_combo_objects() @@ -443,6 +465,7 @@ def _verify_combo_elu( pos_charge: str, type_appuis_y: str, type_appuis_z: str, + t_expo: int = 30, return_objects: bool = False, ) -> dict: """Retourne les taux ELU (Flexion, Cisaillement, Traction ou Compression) @@ -475,12 +498,20 @@ def _verify_combo_elu( "Cisaillement": None, "Traction": None, "Compression": None, + "Flexion_feu": None, + "Cisaillement_feu": None, + "Traction_feu": None, + "Compression_feu": None, } objects: dict[str, object] = { "Flexion": None, "Cisaillement": None, "Traction": None, "Compression": None, + "Flexion_feu": None, + "Cisaillement_feu": None, + "Traction_feu": None, + "Compression_feu": None, } barre = Barre(**barre_kwargs) @@ -490,53 +521,149 @@ def _verify_combo_elu( compression_obj = None N_pos = efforts["Nx"]["Max"][0].value # N, > 0 si compresion N_neg = efforts["Nx"]["Min"][0].value # N, < 0 si traction - if N_pos > 1e-9 and N_pos >= abs(N_neg): - compression_obj = Compression._from_parent_class( - barre, - lo_y=lo_flamb_y, lo_z=lo_flamb_z, - type_appuis_y=type_appuis_y, type_appuis_z=type_appuis_z, - ) - compression_obj.f_c_0_d(loadtype=load_time, typecombi=typecombi) - compression_obj.sigma_c_0_d(Fc0d=abs(N_pos / 10**3)) - compression_obj.taux_c_0_d() - taux_combo["Compression"] = max(compression_obj.taux_c_0_rd.values()) - objects["Compression"] = compression_obj - elif abs(N_neg) > 1e-9: - traction_obj = Traction._from_parent_class(barre) - traction_obj.f_t_0_d(loadtype=load_time, typecombi=typecombi) - traction_obj.sigma_t_0_d(Ft0d=N_neg / 10**3) - traction_obj.taux_t_0_d() - taux_combo["Traction"] = max(traction_obj.taux_t_0_rd.values()) - objects["Traction"] = traction_obj - - # --- Flexion (incluant l'interaction) - flexion_obj = None - if Mz_abs > 1e-9 or My_abs > 1e-9: - flexion_obj = Flexion._from_parent_class( - barre, - lo_rel_y=lo_y, lo_rel_z=lo_z, - coeflef_y=coeflef_y, coeflef_z=coeflef_z, - pos=pos_charge, - ) - flexion_obj.f_m_d(loadtype=load_time, typecombi=typecombi) - flexion_obj.sigma_m_d( - My=My_abs / 10**3, Mz=Mz_abs / 10**3, - ) - flexion_obj.taux_m_d( - compression=compression_obj, traction=traction_obj, - ) - taux_combo["Flexion"] = max(flexion_obj.taux_m_rd.values()) - objects["Flexion"] = flexion_obj - - # --- Cisaillement - cisaillement_obj = None - if Vy_abs > 1e-9: - cisaillement_obj = Cisaillement._from_parent_class(barre) - cisaillement_obj.f_v_d(loadtype=load_time, typecombi=typecombi) - cisaillement_obj.tau_d(Vd=Vy_abs / 10**3) - cisaillement_obj.taux_tau_d() - taux_combo["Cisaillement"] = max(cisaillement_obj.taux_tau_rd.values()) - objects["Cisaillement"] = cisaillement_obj + + if not "FIRE" in combo_name: + if N_pos > 1e-9 and N_pos >= abs(N_neg): + compression_obj = Compression._from_parent_class( + barre, + lo_y=lo_flamb_y, lo_z=lo_flamb_z, + type_appuis_y=type_appuis_y, type_appuis_z=type_appuis_z, + ) + compression_obj.f_c_0_d(loadtype=load_time, typecombi=typecombi) + compression_obj.sigma_c_0_d(Fc0d=abs(N_pos / 10**3)) + compression_obj.taux_c_0_d() + taux_combo["Compression"] = max(compression_obj.taux_c_0_rd.values()) + objects["Compression"] = compression_obj + elif abs(N_neg) > 1e-9: + traction_obj = Traction._from_parent_class(barre) + traction_obj.f_t_0_d(loadtype=load_time, typecombi=typecombi) + traction_obj.sigma_t_0_d(Ft0d=N_neg / 10**3) + traction_obj.taux_t_0_d() + taux_combo["Traction"] = max(traction_obj.taux_t_0_rd.values()) + objects["Traction"] = traction_obj + + # --- Flexion (incluant l'interaction) + flexion_obj = None + if Mz_abs > 1e-9 or My_abs > 1e-9: + flexion_obj = Flexion._from_parent_class( + barre, + lo_rel_y=lo_y, lo_rel_z=lo_z, + coeflef_y=coeflef_y, coeflef_z=coeflef_z, + pos=pos_charge, + ) + flexion_obj.f_m_d(loadtype=load_time, typecombi=typecombi) + flexion_obj.sigma_m_d( + My=My_abs / 10**3, Mz=Mz_abs / 10**3, + ) + if compression_obj is not None: + sigma_c_0_rd = compression_obj.sigma_c_0_rd + sigma_m_rd_y = flexion_obj.sigma_m_rd["y"] + sigma_m_rd_z = flexion_obj.sigma_m_rd["z"] + max_sigma_m_rd = max(sigma_m_rd_y, sigma_m_rd_z) + + if sigma_c_0_rd > max_sigma_m_rd: + flexion_obj.taux_m_d(traction=traction_obj) + compression_obj.taux_c_0_d(flexion=flexion_obj) + taux_combo["Compression"] = max(compression_obj.taux_c_0_rd.values()) + objects["Compression"] = compression_obj + else: + flexion_obj.taux_m_d( + compression=compression_obj, traction=traction_obj, + ) + else: + flexion_obj.taux_m_d(traction=traction_obj) + taux_combo["Flexion"] = max(flexion_obj.taux_m_rd.values()) + objects["Flexion"] = flexion_obj + + # --- Cisaillement + cisaillement_obj = None + if Vy_abs > 1e-9: + cisaillement_obj = Cisaillement._from_parent_class(barre) + cisaillement_obj.f_v_d(loadtype=load_time, typecombi=typecombi) + cisaillement_obj.tau_d(Vd=Vy_abs / 10**3) + cisaillement_obj.taux_tau_d() + taux_combo["Cisaillement"] = max(cisaillement_obj.taux_tau_rd.values()) + objects["Cisaillement"] = cisaillement_obj + + # --- Vérification feu (uniquement pour combinaisons FIRE) + if "FIRE" in combo_name or "ELU_STR_ACC G" == combo_name: + feu_kwargs = { + "t_expo": t_expo, + "haut": "Aucune protection", + "bas": "Aucune protection", + "gauche": "Aucune protection", + "droite": "Aucune protection", + **barre_kwargs, + } + + # Compression feu + compression_feu_obj = None + if N_pos > 1e-9 and N_pos >= abs(N_neg): + compression_feu_obj = Compression_feu._from_parent_class( + barre, + lo_y=lo_flamb_y, lo_z=lo_flamb_z, + type_appuis_y=type_appuis_y, type_appuis_z=type_appuis_z, + **feu_kwargs, + ) + sigma_c0d = compression_feu_obj.sigma_c_0_d(Fc0d=abs(N_pos / 10**3)) + compression_feu_obj.f_c_0_d() + compression_feu_obj.taux_c_0_d() + taux_combo["Compression_feu"] = max(compression_feu_obj.taux_c_0_rd.values()) + objects["Compression_feu"] = compression_feu_obj + + # Traction feu + traction_feu_obj = None + if abs(N_neg) > 1e-9: + traction_feu_obj = Traction_feu._from_parent_class(barre, **feu_kwargs) + traction_feu_obj.sigma_t_0_d(Ft0d=N_neg / 10**3) + traction_feu_obj.f_t_0_d() + traction_feu_obj.taux_t_0_d() + taux_combo["Traction_feu"] = max(traction_feu_obj.taux_t_0_rd.values()) + objects["Traction_feu"] = traction_feu_obj + + # Flexion feu + flexion_feu_obj = None + if Mz_abs > 1e-9 or My_abs > 1e-9: + flexion_feu_obj = Flexion_feu._from_parent_class( + barre, + lo_rel_y=lo_y, lo_rel_z=lo_z, + coeflef_y=coeflef_y, coeflef_z=coeflef_z, + pos=pos_charge, + **feu_kwargs, + ) + flexion_feu_obj.sigma_m_d( + My=My_abs / 10**3, Mz=Mz_abs / 10**3, + ) + flexion_feu_obj.f_m_d() + if compression_feu_obj is not None: + sigma_c_0_rd = compression_feu_obj.sigma_c_0_rd.value + sigma_m_rd_y = flexion_feu_obj.sigma_m_rd["y"] + sigma_m_rd_z = flexion_feu_obj.sigma_m_rd["z"] + max_sigma_m_rd = max(sigma_m_rd_y, sigma_m_rd_z) + + if sigma_c_0_rd > max_sigma_m_rd: + flexion_feu_obj.taux_m_d(traction=traction_feu_obj) + compression_feu_obj.taux_c_0_d(flexion=flexion_feu_obj) + taux_combo["Compression_feu"] = max(compression_feu_obj.taux_c_0_rd.values()) + objects["Compression_feu"] = compression_feu_obj + else: + flexion_feu_obj.taux_m_d( + compression=compression_feu_obj, traction=traction_feu_obj, + ) + else: + flexion_feu_obj.taux_m_d(traction=traction_feu_obj) + taux_combo["Flexion_feu"] = max(flexion_feu_obj.taux_m_rd.values()) + objects["Flexion_feu"] = flexion_feu_obj + + # Cisaillement feu + cisaillement_feu_obj = None + if Vy_abs > 1e-9: + cisaillement_feu_obj = Cisaillement_feu._from_parent_class(barre, **feu_kwargs) + cisaillement_feu_obj.tau_d(Vd=Vy_abs / 10**3) + cisaillement_feu_obj.f_v_d() + cisaillement_feu_obj.taux_tau_d() + taux_combo["Cisaillement_feu"] = max(cisaillement_feu_obj.taux_tau_rd.values()) + objects["Cisaillement_feu"] = cisaillement_feu_obj if return_objects: return taux_combo, objects diff --git a/ourocode/eurocode/ec5/feu/cisaillement_feu.py b/ourocode/eurocode/ec5/feu/cisaillement_feu.py index c2e17c5..03acf4f 100644 --- a/ourocode/eurocode/ec5/feu/cisaillement_feu.py +++ b/ourocode/eurocode/ec5/feu/cisaillement_feu.py @@ -9,7 +9,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec5.feu.feu import Feu from ourocode.eurocode.ec5.element_droit.cisaillement import Cisaillement diff --git a/ourocode/eurocode/ec5/feu/compression_feu.py b/ourocode/eurocode/ec5/feu/compression_feu.py index 844e371..4d2d2fd 100644 --- a/ourocode/eurocode/ec5/feu/compression_feu.py +++ b/ourocode/eurocode/ec5/feu/compression_feu.py @@ -9,7 +9,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec5.feu.feu import Feu from ourocode.eurocode.ec5.element_droit.compression import Compression @@ -17,7 +17,8 @@ class Compression_feu(Feu, Compression): - def __init__(self, lo_y: si.mm, lo_z: si.mm, type_appuis: str = Compression.COEF_LF, **kwargs): + def __init__(self, lo_y: si.mm, lo_z: si.mm, + type_appuis_y: str = Compression.COEF_LF, type_appuis_z: str = Compression.COEF_LF, **kwargs): """Classe permettant le calcul de la Compression d'un élément bois selon l'EN 1995. Cette classe est hérité de la classe Feu, provenant du module EC5_Feu.py. @@ -31,7 +32,7 @@ def __init__(self, lo_y: si.mm, lo_z: si.mm, type_appuis: str = Compression.COEF - Encastré - Encastré : 0.5 - Encastré - Rouleau : 1 """ - super().__init__(lo_y=lo_y, lo_z=lo_z, type_appuis=type_appuis, **kwargs) + super().__init__(lo_y=lo_y, lo_z=lo_z, type_appuis_y=type_appuis_y, type_appuis_z=type_appuis_z, **kwargs) @property def lamb_rel_Axe(self) -> tuple: diff --git a/ourocode/eurocode/ec5/feu/feu.py b/ourocode/eurocode/ec5/feu/feu.py index 991b7f3..e59677a 100644 --- a/ourocode/eurocode/ec5/feu/feu.py +++ b/ourocode/eurocode/ec5/feu/feu.py @@ -9,16 +9,11 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc +from ourocode.eurocode.mixins.math_utils import MathUtilsMixin from ourocode.eurocode.ec5.element_droit.barre import Barre -def interpolation_lineaire(x, xa, xb, ya, yb): - """Fait une interpolation linéaire pour trouver un résultat y entre deux valeur xa et xb""" - y = ya + (x - xa) * ((yb - ya) / (xb - xa)) - return y - - # ================================ GLOBAL ================================== @@ -114,8 +109,8 @@ def _get_bar_beta0_and_betan(self): beta_n = 0.7 elif self.classe[0:1] == "D": if rho_k >= 290 and rho_k < 450: - beta_0 = interpolation_lineaire(rho_k, 290, 450, 0.65, 0.5) - beta_n = interpolation_lineaire(rho_k, 290, 450, 0.7, 0.55) + beta_0 = MathUtilsMixin.interpolation_lineaire(rho_k, 290, 450, 0.65, 0.5) + beta_n = MathUtilsMixin.interpolation_lineaire(rho_k, 290, 450, 0.7, 0.55) elif rho_k >= 450: beta_0 = 0.5 beta_n = 0.55 @@ -297,7 +292,7 @@ def _get_k2(self, orientation: str): k2 = 1 - 0.018 * hp # 3.7 elif self.protection[orientation] == "Fibre de roche": if 45 > hp >= 20: - k2 = interpolation_lineaire(hp, 20, 45, 1, 0.6) + k2 = MathUtilsMixin.interpolation_lineaire(hp, 20, 45, 1, 0.6) elif hp >= 45: k2 = 0.6 return k2 diff --git a/ourocode/eurocode/ec5/feu/flexion_feu.py b/ourocode/eurocode/ec5/feu/flexion_feu.py index 231e61f..bcb08c3 100644 --- a/ourocode/eurocode/ec5/feu/flexion_feu.py +++ b/ourocode/eurocode/ec5/feu/flexion_feu.py @@ -9,7 +9,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec5.feu.feu import Feu from ourocode.eurocode.ec5.element_droit.flexion import Flexion diff --git a/ourocode/eurocode/ec5/feu/traction_feu.py b/ourocode/eurocode/ec5/feu/traction_feu.py index acf834f..76e94b6 100644 --- a/ourocode/eurocode/ec5/feu/traction_feu.py +++ b/ourocode/eurocode/ec5/feu/traction_feu.py @@ -9,7 +9,7 @@ import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.ec5.feu.feu import Feu from ourocode.eurocode.ec5.element_droit.traction import Traction diff --git a/ourocode/eurocode/ec8/sismique.py b/ourocode/eurocode/ec8/sismique.py index e0128f0..1df619e 100644 --- a/ourocode/eurocode/ec8/sismique.py +++ b/ourocode/eurocode/ec8/sismique.py @@ -7,7 +7,7 @@ import pandas as pd import forallpeople as si si.environment("structural") -from ourocode.eurocode.core.renderer import handcalc +from ourocode.eurocode.core._renderer import handcalc from ourocode.eurocode.core.batiment import Batiment class Sismique(Batiment): diff --git a/ourocode/eurocode/mixins/math_utils.py b/ourocode/eurocode/mixins/math_utils.py index dec3dc3..c5a967a 100644 --- a/ourocode/eurocode/mixins/math_utils.py +++ b/ourocode/eurocode/mixins/math_utils.py @@ -358,3 +358,41 @@ def _reset_physical_object(cls, objet: object): """ dictionnary = objet.__dict__ return cls._reset_physical_dictionnary(objet, dictionnary) + + @staticmethod + def interpolation_lineaire(x, xa, xb, ya, yb): + """Fait une interpolation linéaire pour trouver un résultat y entre deux valeurs xa et xb. + + Args: + x: Valeur pour laquelle on cherche l'interpolation. + xa: Première borne inférieure. + xb: Première borne supérieure. + ya: Valeur correspondante à xa. + yb: Valeur correspondante à xb. + + Returns: + float: Valeur interpolée y. + """ + y = ya + (x - xa) * ((yb - ya) / (xb - xa)) + return y + + @staticmethod + def interpolation_logarithmique(x, xa, xb, ya, yb): + """Fait une interpolation logarithmique pour trouver un résultat y entre deux valeurs xa et xb. + + Args: + x: Valeur pour laquelle on cherche l'interpolation. + xa: Première borne inférieure. + xb: Première borne supérieure. + ya: Valeur correspondante à xa. + yb: Valeur correspondante à xb. + + Returns: + float: Valeur interpolée y. + """ + y = ( + (x > xb) * yb + + (x < xa) * ya + + (x <= xb) * (x >= 1) * (ya - (ya - yb) * mt.log10(x)) + ) + return y diff --git a/pyproject.toml b/pyproject.toml index 9ebf220..7aed0a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ include = ["ourocode*"] [project] name = "ourocode" -version = "2.1.2" +version = "2.1.3" description = "Ceci est un catalogue de fonction permettant une utilisation rapide pour la réalisation de note de calcul personnalisée." readme = "README.md" authors = [{name = "Anthony PARISOT", email = "contact@ourea-structure.fr"}] diff --git a/tests/test_EC3_Element_droit.py b/tests/test_EC3_Element_droit.py index 07b84cf..64778d7 100644 --- a/tests/test_EC3_Element_droit.py +++ b/tests/test_EC3_Element_droit.py @@ -8,7 +8,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) import forallpeople as si si.environment("structural") -from ourocode.eurocode.ec3.element_droit import Plat, Traction, Compression, Cisaillement, Flexion +from ourocode.eurocode.ec3.element_droit import Plat, Profil, Traction, Compression, Cisaillement, Flexion class TestPlat: """Tests pour la classe de base Plat.""" @@ -225,5 +225,101 @@ def test_fy_fu_epaisseur_superieure_40mm(self): assert p_epais.fy.value < p_mince.fy.value +class TestProfil: + """Tests pour la classe Profil.""" + + def test_initialisation_IPE(self): + """Test l'initialisation d'un profil IPE.""" + profil = Profil(designation="IPE 300", classe_acier="S235", classe_transv=1) + assert profil.designation == "IPE 300" + assert profil.classe_acier == "S235" + assert profil.classe_transv == 1 + assert profil.h == 300.0 * si.mm + assert profil.b == 150.0 * si.mm + assert profil.tw == 7.1 * si.mm + assert profil.tf == 10.7 * si.mm + + def test_initialisation_HEA(self): + """Test l'initialisation d'un profil HEA.""" + profil = Profil(designation="HEA 200", classe_acier="S355", classe_transv=2) + assert profil.designation == "HEA 200" + assert profil.classe_acier == "S355" + assert profil.type_profil == "HEA" + assert profil.h == 200.0 * si.mm + assert profil.b == 200.0 * si.mm + + def test_initialisation_HEB(self): + """Test l'initialisation d'un profil HEB.""" + profil = Profil(designation="HEB 300", classe_acier="S275", classe_transv=1) + assert profil.designation == "HEB 300" + assert profil.classe_acier == "S275" + assert profil.type_profil == "HEB" + assert profil.h == 300.0 * si.mm + assert profil.b == 300.0 * si.mm + + def test_caracteristiques_geometriques(self): + """Test le chargement des caractéristiques géométriques.""" + profil = Profil(designation="IPE 300", classe_acier="S235") + assert profil.A == 53.81 * si.mm**2 + assert profil.Av == 16.7 * si.mm**2 + assert profil.Iy == 8356 * si.mm**4 + assert profil.Iz == 435.5 * si.mm**4 + assert profil.Wely == 557.1 * si.mm**3 + assert profil.Welz == 58.0 * si.mm**3 + assert profil.Wply == 628.4 * si.mm**3 + assert profil.Wplz == 72.9 * si.mm**3 + assert profil.iy == 12.46 * si.mm + assert profil.iz == 2.85 * si.mm + assert profil.masse == 42.2 * si.kg / si.m + + def test_fy_fu_S235(self): + """Test les valeurs de fy et fu pour un profil S235.""" + profil = Profil(designation="IPE 300", classe_acier="S235") + assert profil.fy == 235000000.0 # 235 MPa en Pa + assert profil.fu == 360000000.0 # 360 MPa en Pa + + def test_fy_fu_S355(self): + """Test les valeurs de fy et fu pour un profil S355.""" + profil = Profil(designation="HEA 200", classe_acier="S355") + assert profil.fy == 355000000.0 # 355 MPa en Pa + assert profil.fu == 490000000.0 # 490 MPa en Pa + + def test_designation_invalide(self): + """Test ValueError sur désignation invalide.""" + with pytest.raises(ValueError, match="IPE 999"): + Profil(designation="IPE 999", classe_acier="S235") + + def test_classe_acier_invalide(self): + """Test ValueError sur classe d'acier invalide.""" + with pytest.raises(ValueError, match="S999"): + Profil(designation="IPE 300", classe_acier="S999") + + def test_classe_transv_invalide(self): + """Test ValueError sur classe transversale invalide.""" + with pytest.raises(ValueError, match="4"): + Profil(designation="IPE 300", classe_acier="S235", classe_transv=4) + + def test_compatibility_with_compression(self): + """Test que Profil est compatible avec Compression via _from_parent_class.""" + profil = Profil(designation="IPE 300", classe_acier="S235") + compression = Compression._from_parent_class( + profil, + A=profil.A.value, + Iy=profil.Iy.value, + Iz=profil.Iz.value, + lo_y=3000, + lo_z=3000, + courbe_flamb={"y": "b", "z": "c"}, + type_appuis="Rotule - Rotule" + ) + # Vérification que les attributs de Profil sont hérités + assert compression.designation == "IPE 300" + assert compression.classe_acier == "S235" + assert compression.fy == profil.fy + # Vérification que la méthode Nc_Rd fonctionne + latex_nc, nc = compression.Nc_Rd + assert nc.value > 0 + + if __name__ == "__main__": pytest.main(["-v", "test_EC3_Element_droit.py"]) diff --git a/tests/test_EC5_Verification.py b/tests/test_EC5_Verification.py index b675d91..7bb51c0 100644 --- a/tests/test_EC5_Verification.py +++ b/tests/test_EC5_Verification.py @@ -100,6 +100,29 @@ def continuous_beam_verif(): return model, combi, result, verif +@pytest.fixture +def fire_beam_verif(): + """Poutre simple avec combinaisons FIRE pour tester la vérification incendie.""" + model = _new_model("Poutre feu") + _build_beam(model, [(0, 0), (5000, 0)]) + _apply_gq_loads(model, gkNm=1.0, qkNm=2.0) + combi = Combinaison( + model, ELU_STR=True, ELU_STR_ACC=True, ELS_C=False, ELS_QP=False, + cat="Cat A : habitation", kdef=0.6, type_psy_2="Moyen terme", + ) + result = Model_result(model, analyze_type="Général", check_stability=False) + model.group_members( + "Solive", ["M1"], + role="Solive", + design_params={ + "classe_bois": "C24", "cs": 1, + "type_element_fleche": "Élément structuraux", + }, + ) + verif = Verification_EC5(combi, result, "ELU_ALL", 12, 12, False, 1, "Bâtiments courants", "Élément structuraux", t_expo=30) + return model, combi, result, verif + + # --------------------------------------------------------------------------- # Combinaison.type_combi # --------------------------------------------------------------------------- @@ -312,3 +335,56 @@ def test_synthese_governing_combo_present(self, continuous_beam_verif): df = verif.synthese() combos = df["Combinaison"].tolist() assert any("1.35G" in c and "1.5Q" in c for c in combos if c is not None) + + +# --------------------------------------------------------------------------- +# Verification_EC5 : vérification feu +# --------------------------------------------------------------------------- + +class TestFireVerification: + def test_fire_combo_has_no_normal_verification(self, fire_beam_verif): + """Pour une combinaison FIRE, les vérifications normales doivent être None.""" + _, combi, _, verif = fire_beam_verif + # Vérifier qu'il existe des combinaisons FIRE + fire_combos = [c for c in combi.get_list_combination("ELU_ALL") or [] if "FIRE" in c] + if fire_combos: + # Appeler verify pour peupler le cache + verif.verify("Solive", "Élément structuraux", "Charge sur fibre comprimée", 12, 12, False, 1) + # Récupérer les objets pour une combinaison FIRE + objs = verif.get_combo_objects("Solive", fire_combos[0]) + # Les objets normaux doivent être None pour les combos FIRE + assert objs["Flexion"] is None + assert objs["Cisaillement"] is None + assert objs["Compression"] is None + assert objs["Traction"] is None + + def test_fire_combo_has_fire_verification(self, fire_beam_verif): + """Pour une combinaison FIRE, les vérifications feu doivent être calculées.""" + _, combi, _, verif = fire_beam_verif + # Vérifier qu'il existe des combinaisons FIRE + fire_combos = [c for c in combi.get_list_combination("ELU_ALL") or [] if "FIRE" in c] + if fire_combos: + # Appeler verify pour peupler le cache + verif.verify("Solive", "Élément structuraux", "Charge sur fibre comprimée", 12, 12, False, 1) + # Récupérer les objets pour une combinaison FIRE + objs = verif.get_combo_objects("Solive", fire_combos[0]) + # Au moins un objet feu doit être calculé (flexion ou cisaillement) + assert objs["Flexion_feu"] is not None or objs["Cisaillement_feu"] is not None + + def test_normal_combo_has_no_fire_verification(self, simple_beam_verif): + """Pour une combinaison normale, les vérifications feu doivent être None.""" + _, combi, _, verif = simple_beam_verif + # Appeler verify pour peupler le cache + verif.verify("Solive", "Élément structuraux", "Charge sur fibre comprimée", 12, 12, False, 1) + # Récupérer les objets pour une combinaison normale + objs = verif.get_combo_objects("Solive", "ELU_STR 1.35G + 1.5Q") + # Les objets feu doivent être None pour les combos normaux + assert objs["Flexion_feu"] is None + assert objs["Cisaillement_feu"] is None + assert objs["Compression_feu"] is None + assert objs["Traction_feu"] is None + + def test_t_expo_parameter_used(self, fire_beam_verif): + """Le paramètre t_expo doit être utilisé dans les calculs feu.""" + _, _, _, verif = fire_beam_verif + assert verif.t_expo == 30