Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion structuralcodes/codes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import types
import typing as t

from . import ec2_2004, ec2_2023, mc2010, mc2020
from . import aci318_19, ec2_2004, ec2_2023, mc2010, mc2020

__all__ = [
'aci318_19',
'mc2010',
'mc2020',
'ec2_2023',
Expand All @@ -23,6 +24,7 @@

# Design code registry
_DESIGN_CODES = {
'aci318_19': aci318_19,
'mc2010': mc2010,
'mc2020': mc2020,
'ec2_2004': ec2_2004,
Expand Down
97 changes: 97 additions & 0 deletions structuralcodes/codes/aci318_19/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""ACI 318-19."""

import typing as t

from ._concrete_material_properties import (
MINIMUM_STRESS_BLOCK_FC,
Ec,
alpha1,
beta1,
eps_c0,
eps_cu,
fr,
lambda_factor,
)
from ._reinforcement_material_properties import (
Es,
epsyd,
fy_design,
reinforcement_grade_props,
)
from ._unit_conversions import (
INCH_TO_MM,
KIP_TO_N,
KSI_TO_MPA,
LB_FORCE_TO_N,
LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER,
PSI_TO_MPA,
in2_to_mm2,
in3_to_mm3,
in4_to_mm4,
in_to_mm,
kg_per_m3_to_pcf,
kip_in_to_nmm,
kip_to_n,
ksi_to_mpa,
lb_in_to_nmm,
lb_to_n,
mm2_to_in2,
mm3_to_in3,
mm4_to_in4,
mm_to_in,
mpa_to_ksi,
mpa_to_psi,
n_to_kip,
n_to_lb,
nmm_to_kip_in,
nmm_to_lb_in,
pcf_to_kg_per_m3,
psi_to_mpa,
)

__all__ = [
'Ec',
'Es',
'INCH_TO_MM',
'KIP_TO_N',
'KSI_TO_MPA',
'LB_FORCE_TO_N',
'LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER',
'MINIMUM_STRESS_BLOCK_FC',
'PSI_TO_MPA',
'alpha1',
'beta1',
'eps_c0',
'eps_cu',
'epsyd',
'fr',
'fy_design',
'in2_to_mm2',
'in3_to_mm3',
'in4_to_mm4',
'in_to_mm',
'kg_per_m3_to_pcf',
'kip_in_to_nmm',
'kip_to_n',
'ksi_to_mpa',
'lambda_factor',
'lb_in_to_nmm',
'lb_to_n',
'mm2_to_in2',
'mm3_to_in3',
'mm4_to_in4',
'mm_to_in',
'mpa_to_ksi',
'mpa_to_psi',
'n_to_kip',
'n_to_lb',
'nmm_to_kip_in',
'nmm_to_lb_in',
'pcf_to_kg_per_m3',
'psi_to_mpa',
'reinforcement_grade_props',
]

__title__: str = 'ACI 318-19'
__year__: str = '2019'
__materials__: t.Tuple[str] = ('concrete', 'reinforcement')
159 changes: 159 additions & 0 deletions structuralcodes/codes/aci318_19/_concrete_material_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""Concrete material properties according to ACI 318-19."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest we order the methods as they appear in the code. Let's not jump from Chaper 19 to 22 and then back to 19.

from __future__ import annotations

import math
import typing as t

from ._unit_conversions import LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lambda factor in ACI-318-19 is a function of the weight and lies between 1.00 and 0.75. You provide 'sand-lightweight as an intermediate value. Consider programing the equation as a function of the weight (as done for the modulus of elasticity) or adding the values from Table 19.2.4.1 'lightweight, fine blend' and 'sand-lightweight, corase blend'

LIGHTWEIGHT_LAMBDA_LIMIT = 100.0 * LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER
NORMALWEIGHT_LAMBDA_LIMIT = 135.0 * LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER
MINIMUM_STRESS_BLOCK_FC = 17.0


def Ec(fc: float, wc: t.Optional[float] = None) -> float:
"""The modulus of elasticity of concrete.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default value of concrete. The typical specific weight of concrete used in North American practice is 150 lbs/cf, which I roughly equivalent to a density of 150/(2.2*0.3048^3)≈2400 kg/m3. I understand this my have to do with going from specific weight to density using g=9.81 m/s2, but it is not clear for me how you derive it, as the units are not consistent. On the other hand, Eq. 19.2.2.1b provides a factor of 4700 which would lead to a value of w=2286 kg/m3.

ACI 318-19, Table 19.2.2.1.

Args:
fc (float): The specified compressive strength of concrete in
MPa.

Keyword Args:
wc (float, optional): The equilibrium density of concrete in
kg/m3. If omitted, the normalweight expression
``4700 * sqrt(fc)`` is used.

Returns:
float: The modulus of elasticity in MPa.

Raises:
ValueError: If fc is not positive.
ValueError: If wc is given outside the range 1440-2560 kg/m3.

Note:
wc is the ACI equilibrium density for Eq. 19.2.2.1, not the
base material density stored on a concrete object. Omitting wc
selects the ACI normalweight expression.
"""
if fc <= 0:
raise ValueError(f'fc={fc} must be positive')
if wc is None:
return 4700.0 * math.sqrt(fc)
if wc < 1440 or wc > 2560:
raise ValueError(f'wc={wc} must be between 1440 and 2560 kg/m3')
return wc**1.5 * 0.043 * math.sqrt(fc)


def fr(fc: float, lambda_s: float = 1.0) -> float:
"""The modulus of rupture of concrete.

ACI 318-19, Eq. 19.2.3.1.

Args:
fc (float): The specified compressive strength of concrete in
MPa.

Keyword Args:
lambda_s (float): The modification factor for lightweight
concrete. Default is 1.0 (normalweight).

Returns:
float: The modulus of rupture in MPa.

Raises:
ValueError: If fc is not positive.
ValueError: If lambda_s is not in (0, 1].
"""
if fc <= 0:
raise ValueError(f'fc={fc} must be positive')
if lambda_s <= 0 or lambda_s > 1.0:
raise ValueError(f'lambda_s={lambda_s} must be in the range (0, 1]')
return 0.62 * lambda_s * math.sqrt(fc)


def lambda_factor(wc: float) -> float:
"""The modification factor for lightweight concrete.

ACI 318-19, Table 19.2.4.1(a).

Args:
wc (float): The equilibrium density of concrete in kg/m3.

Returns:
float: The lightweight modification factor (dimensionless).

Raises:
ValueError: If wc is not positive.
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Table 22.2.2.4.3. f'c must be greater that 17. We should set this as a lower limit. Not 0.

if wc <= 0:
raise ValueError(f'wc={wc} must be positive')
if wc <= LIGHTWEIGHT_LAMBDA_LIMIT:
return 0.75
if wc >= NORMALWEIGHT_LAMBDA_LIMIT:
return 1.0
return min(1.0, 0.0075 * wc / LB_PER_CUBIC_FOOT_TO_KG_PER_CUBIC_METER)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace expression: 0.85 - 0.05 * (fc - 28) / 7 by 0.85-0.20/27*(fc-28) - why simplify and lose accuracy? I could drive some people crazy...


def eps_cu() -> float:
"""The maximum usable strain at the extreme concrete compression fiber.

ACI 318-19, Section 22.2.2.1.

Returns:
float: The ultimate concrete strain (dimensionless).
"""
return 0.003


def eps_c0() -> float:
"""The default strain at peak concrete compression.

Returns:
float: The peak concrete strain (dimensionless).

Note:
ACI 318-19 does not prescribe a complete concrete stress-strain
curve. The value 0.002 is the library default peak-strain
parameter for the ACI parabolic and bilinear constitutive laws.
"""
return 0.002


def alpha1() -> float:
"""The ratio of equivalent rectangular stress block intensity.

ACI 318-19, Section 22.2.2.4.1.

Returns:
float: The stress block intensity factor (dimensionless).
"""
return 0.85


def beta1(fc: float) -> float:
"""The Whitney stress block depth factor.

@aperezcaldentey aperezcaldentey Jun 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formula used in this method does not correspond to ACI 318-19, Section 19.2.4.3, as stated. We should possibly delete this method as fct is not defined in ACI 318-19.

ACI 318-19, Table 22.2.2.4.3.

Args:
fc (float): The specified compressive strength of concrete in
MPa.

Returns:
float: The stress block depth factor (dimensionless).

Raises:
ValueError: If fc is below 17 MPa.
"""
if fc < MINIMUM_STRESS_BLOCK_FC:
raise ValueError(
f'fc={fc} must be at least {MINIMUM_STRESS_BLOCK_FC} MPa'
)
if fc <= 28:
return 0.85
if fc <= 55:
return 0.85 - 0.20 / 27 * (fc - 28)
return 0.65
100 changes: 100 additions & 0 deletions structuralcodes/codes/aci318_19/_reinforcement_material_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Reinforcement material properties according to ACI 318-19."""

from __future__ import annotations

import typing as t

# ACI 318-19, Table 20.2.1.3(a), SI equivalents for ASTM A615
# reinforcement grades recognized by the code.
REINFORCEMENT_GRADES = {
'40': {'fy': 280.0, 'fu': 420.0},
'60': {'fy': 420.0, 'fu': 550.0},
'80': {'fy': 550.0, 'fu': 690.0},
'100': {'fy': 690.0, 'fu': 860.0},
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Values for Reinforcement grades based on table 20.2.1.3(a). Please add this reference to comments.


def Es() -> float:
"""The modulus of elasticity of reinforcement.

ACI 318-19, Section 20.2.2.2.

Returns:
float: The modulus of elasticity in MPa.
"""
return 200000.0


def fy_design(fy: float) -> float:
"""The design yield strength of reinforcement.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you say, ACI does not consider a partial factor on steel. Why do we need to provide one? The user should not be able to introduce a value different that 1.00. If we need this method for compatibility reasons we shoudl delete phi as a KWARG.

ACI 318-19 applies strength reduction factors (phi) at the
member capacity level, not the material level. This function
therefore returns the unreduced yield strength.

Args:
fy (float): The specified yield strength in MPa.

Returns:
float: The design yield strength in MPa.

Raises:
ValueError: If fy is not positive.
"""
if fy <= 0:
raise ValueError(f'fy={fy} must be positive')
return fy


def epsyd(fy: float, _Es: float = 200000.0) -> float:
"""The yield strain of reinforcement.

Args:
fy (float): The specified yield strength in MPa.

Keyword Args:
_Es (float): The modulus of elasticity in MPa.
Default is 200000 MPa.

Returns:
float: The yield strain (dimensionless).

Raises:
ValueError: If fy is not positive.
"""
if fy <= 0:
raise ValueError(f'fy={fy} must be positive')
return fy / _Es


def reinforcement_grade_props(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does ACI 318-19 also provide values for the strain at ultimate strength? If so, please consider including it in the return dictionary.

grade: t.Literal['40', '60', '80', '100'],
) -> t.Dict[str, float]:
"""Return the minimum specified properties for a reinforcement grade.

ACI 318-19, Table 20.2.1.3(a) (SI equivalents).

Args:
grade (str): The ASTM reinforcement grade designation.
One of '40', '60', '80', or '100'.

Returns:
Dict[str, float]: A dict with keys 'fy' (yield strength in
MPa) and 'fu' (ultimate strength in MPa).

Note:
ACI 318-19 Table 20.2.1.3(a) does not provide a single
grade-level ultimate strain. Applicable elongation requirements
depend on the reinforcement specification and bar size, so they
are not returned here.

Raises:
ValueError: If the grade is not recognized.
"""
props = REINFORCEMENT_GRADES.get(str(grade))
if props is None:
raise ValueError(
f'Unknown reinforcement grade: {grade}. '
f'Valid grades: {list(REINFORCEMENT_GRADES.keys())}'
)
return dict(props)
Loading