Repository: fib-international/structuralcodes

Issue:
constitutive_law.get_stress return return zero when given a float and the law is set to bilinearcompression
```python
from structuralcodes import set_design_code
from structuralcodes.materials.concrete import create_concrete

set_design_code("ec2_2004")
concrete = create_concrete(fck=45, gamma_c=1.5)
concrete.constitutive_law = "bilinearcompression"

sig = concrete.constitutive_law.get_stress(-0.003) # sig = 0
sig = concrete.constitutive_law.get_stress([-0.003]) # sig = [-30]
````

Discussion:
@talledodiego: Thanks @twj-strukturel for catching this up. I will take a look ASAP!@twj-strukturel: @talledodiego , I have created a pull request :)@talledodiego: Excellent! Great! Can you assign the  review to me?@twj-strukturel: @talledodiego I think I dont have permission to do that?

![Image](https://github.com/user-attachments/assets/282178fb-3ee6-49bc-b9a7-8d923750d125)@talledodiego: mmm , no worries, I'll do it myself!@mortenengen: Solved in #216 👍 

Solution patch:
diff --git a/structuralcodes/geometry/_circular.py b/structuralcodes/geometry/_circular.py
index 9d68f0e..2d3754e 100644
--- a/structuralcodes/geometry/_circular.py
+++ b/structuralcodes/geometry/_circular.py
@@ -42,6 +42,8 @@ class CircularGeometry(SurfaceGeometry):
         density: t.Optional[float] = None,
         concrete: bool = False,
         origin: t.Optional[ArrayLike] = None,
+        name: t.Optional[str] = None,
+        group_label: t.Optional[str] = None,
     ) -> None:
         """Initialize a CircularGeometry.
 
@@ -58,6 +60,8 @@ class CircularGeometry(SurfaceGeometry):
             concrete (bool): Flag to indicate if the geometry is concrete.
             origin (Optional(ArrayLike)): The center point of the circle.
                 (0.0, 0.0) is used as default.
+            name (Optional(str)): The name to be given to the object.
+            group_label (Optional(str)): A label for grouping several objects.
 
         Note:
             The CircularGeometry is simply a wrapper for a SurfaceGeometry
@@ -78,7 +82,12 @@ class CircularGeometry(SurfaceGeometry):
         )
         # Pass everything to the base class
         super().__init__(
-            poly=polygon, material=material, density=density, concrete=concrete
+            poly=polygon,
+            material=material,
+            density=density,
+            concrete=concrete,
+            name=name,
+            group_label=group_label,
         )
 
     @property
diff --git a/structuralcodes/geometry/_geometry.py b/structuralcodes/geometry/_geometry.py
index 4542233..e9186e8 100644
--- a/structuralcodes/geometry/_geometry.py
+++ b/structuralcodes/geometry/_geometry.py
@@ -323,7 +323,7 @@ def create_line_point_angle(
     return LineString([(x1, y1), (x2, y2)])
 
 
-class SurfaceGeometry:
+class SurfaceGeometry(Geometry):
     """Class for a surface geometry with material.
 
     Basically it is a wrapper for shapely polygon including the material (and
@@ -339,6 +339,8 @@ class SurfaceGeometry:
         material: t.Union[Material, ConstitutiveLaw],
         density: t.Optional[float] = None,
         concrete: bool = False,
+        name: t.Optional[str] = None,
+        group_label: t.Optional[str] = None,
     ) -> None:
         """Initializes a SurfaceGeometry object.
 
@@ -350,7 +352,10 @@ class SurfaceGeometry:
                 the density can be provided by this argument. When mat is a
                 Material object the density is taken from the material.
             concrete (bool): Flag to indicate if the geometry is concrete.
+            name (Optional(str)): The name to be given to the object.
+            group_label (Optional(str)): A label for grouping several objects.
         """
+        super().__init__(name=name, group_label=group_label)
         # Check if inputs are of the correct type, otherwise return error
         if not isinstance(poly, Polygon):
             raise TypeError(
diff --git a/structuralcodes/geometry/_rectangular.py b/structuralcodes/geometry/_rectangular.py
index f9df205..7e0d179 100644
--- a/structuralcodes/geometry/_rectangular.py
+++ b/structuralcodes/geometry/_rectangular.py
@@ -32,6 +32,8 @@ class RectangularGeometry(SurfaceGeometry):
         density: t.Optional[float] = None,
         concrete: bool = False,
         origin: t.Optional[ArrayLike] = None,
+        name: t.Optional[str] = None,
+        group_label: t.Optional[str] = None,
     ) -> None:
         """Initialize a RectangularGeometry.
 
@@ -48,6 +50,8 @@ class RectangularGeometry(SurfaceGeometry):
                 passing a Material as material, this is automatically inferred.
             origin (Optional(ArrayLike)): The center point of the rectangle.
                 (0.0, 0.0) is used as default.
+            name (Optional(str)): The name to be given to the object.
+            group_label (Optional(str)): A label for grouping several objects.
 
         Note:
             The RectangularGeometry is simply a wrapper for a SurfaceGeometry
@@ -78,7 +82,12 @@ class RectangularGeometry(SurfaceGeometry):
         )
         # Pass everything to the base class
         super().__init__(
-            poly=polygon, material=material, density=density, concrete=concrete
+            poly=polygon,
+            material=material,
+            density=density,
+            concrete=concrete,
+            name=name,
+            group_label=group_label,
         )
 
     @property
diff --git a/structuralcodes/materials/constitutive_laws/_bilinearcompression.py b/structuralcodes/materials/constitutive_laws/_bilinearcompression.py
index ba12bbc..4709c95 100644
--- a/structuralcodes/materials/constitutive_laws/_bilinearcompression.py
+++ b/structuralcodes/materials/constitutive_laws/_bilinearcompression.py
@@ -51,10 +51,10 @@ class BilinearCompression(ConstitutiveLaw):
         # Compute stress
         # If it is a scalar
         if np.isscalar(eps):
-            sig = 0
-            if self._fc / self._E <= eps <= 0:
-                sig = self._E * eps
-            return sig
+            if eps > 0 or eps < self._eps_cu:
+               return 0
+            return max(self._E * eps, self._fc)
+        
         # If it is an array
         sig = self._E * eps
         sig[sig < self._fc] = self._fc
@@ -69,13 +69,14 @@ class BilinearCompression(ConstitutiveLaw):
         eps = eps if np.isscalar(eps) else np.atleast_1d(eps)
         # If it is a scalar
         if np.isscalar(eps):
-            tangent = 0
-            if self._fc / self._E <= eps <= 0:
-                tangent = self._E
-            return tangent
+            if self._eps_c < eps <= 0:
+                return self._E
+            return 0
+
         # If it is an array
         tangent = np.ones_like(eps) * self._E
-        tangent[eps < self._eps_c] = 0.0
+        tangent[eps >= 0] = 0
+        tangent[eps < self._eps_c] = 0
 
         return tangent
 


Test patch:
diff --git a/tests/test_geometry/test_circular.py b/tests/test_geometry/test_circular.py
index 91b0ff7..d01c796 100644
--- a/tests/test_geometry/test_circular.py
+++ b/tests/test_geometry/test_circular.py
@@ -267,3 +267,24 @@ def test_reinforcement_circular_spacing_number_error():
             start_angle=0,
             stop_angle=np.pi,
         )
+
+
+def test_circular_geometry_name_group_label():
+    """Test the name and group label attribute of a CircularGeometry."""
+    # Arrange
+    diameter = 500
+    concrete = ConcreteMC2010(fck=35)
+    name = 'concrete_geometry'
+    group_label = 'concrete'
+
+    # Act
+    geometry = CircularGeometry(
+        diameter=diameter,
+        material=concrete,
+        name=name,
+        group_label=group_label,
+    )
+
+    # Assert
+    assert geometry.name == name
+    assert geometry.group_label == group_label
diff --git a/tests/test_geometry/test_geometry.py b/tests/test_geometry/test_geometry.py
index 11018b7..63a00fb 100644
--- a/tests/test_geometry/test_geometry.py
+++ b/tests/test_geometry/test_geometry.py
@@ -649,3 +649,32 @@ def test_reinforcement_group_label_line():
     assert len(point_geometries) == n_bars
     for point in point_geometries:
         assert point.group_label == group_label
+
+
+def test_surface_geometry_name_group_label():
+    """Test the name and group label attribute of a SurfaceGeometry."""
+    # Arrange
+    width = 200
+    height = 500
+    concrete = ConcreteMC2010(fck=35)
+    name = 'concrete_geometry'
+    group_label = 'concrete'
+
+    # Act
+    geometry = SurfaceGeometry(
+        poly=Polygon(
+            (
+                (-width / 2, -height / 2),
+                (width / 2, -height / 2),
+                (width / 2, height / 2),
+                (-width / 2, height / 2),
+            )
+        ),
+        material=concrete,
+        name=name,
+        group_label=group_label,
+    )
+
+    # Assert
+    assert geometry.name == name
+    assert geometry.group_label == group_label
diff --git a/tests/test_geometry/test_rectangular.py b/tests/test_geometry/test_rectangular.py
index 7efc67d..fdc1b7b 100644
--- a/tests/test_geometry/test_rectangular.py
+++ b/tests/test_geometry/test_rectangular.py
@@ -121,3 +121,26 @@ def test_reinforced_rectangular_geometry(w, h, c, n):
         n=n,
     )
     assert len(rc.point_geometries) == 2 * n
+
+
+def test_rectangular_geometry_name_group_label():
+    """Test the name and group label attribute of a RectangularGeometry."""
+    # Arrange
+    width = 200
+    height = 500
+    concrete = ConcreteMC2010(fck=35)
+    name = 'concrete_geometry'
+    group_label = 'concrete'
+
+    # Act
+    geometry = RectangularGeometry(
+        width=width,
+        height=height,
+        material=concrete,
+        name=name,
+        group_label=group_label,
+    )
+
+    # Assert
+    assert geometry.name == name
+    assert geometry.group_label == group_label
diff --git a/tests/test_materials/test_constitutive_laws.py b/tests/test_materials/test_constitutive_laws.py
index ce2dcf0..e1a2659 100644
--- a/tests/test_materials/test_constitutive_laws.py
+++ b/tests/test_materials/test_constitutive_laws.py
@@ -489,23 +489,31 @@ def test_bilinearcompression(fc, eps_c, eps_cu):
     """Test BilinearCompression material."""
     law = BilinearCompression(fc=fc, eps_c=eps_c, eps_cu=eps_cu)
 
-    eps = np.linspace(0, eps_cu, 20)
+    eps = np.linspace(-2*eps_cu, eps_c, 20)
 
     # compute expected
     E = fc / eps_c
     sig_expected = E * eps
-    sig_expected[sig_expected > fc] = fc
-    tan_expected = np.zeros_like(sig_expected)
-    tan_expected[sig_expected < fc] = E
-    sig_expected *= -1
+    sig_expected[sig_expected < -fc] = -fc
+    sig_expected[eps > 0] = 0
+    sig_expected[eps < -eps_cu] = 0
+    tan_expected = np.ones_like(eps) * E
+    tan_expected[eps >= 0] = 0
+    tan_expected[eps < -eps_c] = 0
 
     # compute from BilinearCompression
-    sig_computed = law.get_stress(-eps)
-    tan_computed = law.get_tangent(-eps)
+    sig_computed_array = law.get_stress(eps)
+    sig_computed_scalar = np.array([law.get_stress(eps_scalar) for eps_scalar in eps])
+    tan_computed_array = law.get_tangent(eps)
+    tan_computed_scalar = np.array([law.get_tangent(eps_scalar) for eps_scalar in eps])
+
 
     # Compare the two
-    assert_allclose(sig_computed, sig_expected)
-    assert_allclose(tan_computed, tan_expected)
+    assert_allclose(sig_computed_array, sig_expected)
+    assert_allclose(sig_computed_scalar, sig_expected)
+    assert_allclose(tan_computed_array, tan_expected)
+    assert_allclose(tan_computed_scalar, tan_expected)
+
 
     # Test getting ultimate strain
     eps_min, eps_max = law.get_ultimate_strain()
@@ -514,4 +522,4 @@ def test_bilinearcompression(fc, eps_c, eps_cu):
 
     eps_min, eps_max = law.get_ultimate_strain(yielding=True)
     assert math.isclose(eps_min, -eps_c)
-    assert math.isclose(eps_max, 100)
+    assert math.isclose(eps_max, 100)
\ No newline at end of file