Skip to content
7 changes: 7 additions & 0 deletions structuralcodes/core/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
"""Core functionality shared between other modules."""

from ._units import UnitConverter, UnitSet

__all__ = [
'UnitConverter',
'UnitSet',
]
156 changes: 156 additions & 0 deletions structuralcodes/core/_units.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
"""Classes related to unit handling."""

import typing as t
from dataclasses import dataclass

from numpy.typing import ArrayLike

# Type annotations for units
_LENGTH_LITERAL = t.Literal['mm', 'm', 'inch', 'foot']
_FORCE_LITERAL = t.Literal['N', 'kN', 'MN']

# Unit conversion
_MILLIMETER = 1.0 # Only used as a reference and has no physical meaning
_NEWTON = 1e-3 # Only used as a reference and has no physical meaning

_UNITS = {
'length': {
'm': _MILLIMETER * 1e3, # 1000 mm in one m
'mm': _MILLIMETER,
'inch': _MILLIMETER * 25.4, # 25.4 mm in one inch
'foot': _MILLIMETER * 304.8, # 304.8 mm in one foot
},
'force': {
'N': _NEWTON,
'kN': _NEWTON * 1e3, # 1000 N in one kN
'MN': _NEWTON * 1e6, # 1000000 N in one MN
},
}


@dataclass
class UnitSet:
"""A set of units that can be used in the UnitConverter."""

length: _LENGTH_LITERAL = 'mm'
force: _FORCE_LITERAL = 'N'

@property
def length_unit(self):
return _UNITS['length'][self.length]

@property
def force_unit(self):
return _UNITS['force'][self.force]

@property
def stress_unit(self):
return self.force_unit / self.length_unit**2

def __post_init__(self):
"""Validate the provided units."""
for attr in ('length', 'force'):
try:
# Try to find the provided unit among the available
_UNITS[attr][getattr(self, attr)]
except KeyError:
# The provided unit was not found
# Try to see if there was perhaps a mix of upper and lower case
# letters
for key in _UNITS[attr]:
if key.lower() == getattr(self, attr).lower().strip():
setattr(self, attr, key)
break
else:
raise ValueError(
f'{getattr(self, attr)} is not a valid {attr} unit. '
f'Use one of {", ".join(_UNITS[attr].keys())}.'
)


class UnitConverter:
"""A class responsible for converting between different sets of units."""

_from_units: UnitSet
_to_units: UnitSet

def __init__(
self,
from_units: UnitSet,
to_units: t.Optional[UnitSet] = None,
) -> None:
"""Initialize a UnitConverter.

Args:
from_units (UnitSet): The set of units to convert forwards from or
backwards to.
to_units (Optional(UnitSet)): The set of units to convert forwards
to or backwards from. If None, it is treated as equal to
from_units, and no conversion happens.
"""
self._from_units = from_units
self._to_units = to_units

def convert_stress_backwards(
self, stress: t.Union[float, ArrayLike]
) -> t.Union[float, ArrayLike]:
"""Convert stress backwards."""
if self.from_units == self.to_units:
return stress
return stress * self.to_units.stress_unit / self.from_units.stress_unit

def convert_stress_forwards(
self, stress: t.Union[float, ArrayLike]
) -> t.Union[float, ArrayLike]:
"""Convert stress forwards."""
if self.from_units == self.to_units:
return stress
return stress * self.from_units.stress_unit / self.to_units.stress_unit

def convert_length_backwards(
self, length: t.Union[float, ArrayLike]
) -> t.Union[float, ArrayLike]:
"""Convert length backwards."""
if self.from_units == self.to_units:
return length
return length * self.to_units.length_unit / self.from_units.length_unit

def convert_length_forwards(
self, length: t.Union[float, ArrayLike]
) -> t.Union[float, ArrayLike]:
"""Convert length forwards."""
if self.from_units == self.to_units:
return length
return length * self.from_units.length_unit / self.to_units.length_unit

def convert_force_backwards(
self, force: t.Union[float, ArrayLike]
) -> t.Union[float, ArrayLike]:
"""Convert force backwards."""
if self.from_units == self.to_units:
return force
return force * self.to_units.force_unit / self.from_units.force_unit

def convert_force_forwards(
self, force: t.Union[float, ArrayLike]
) -> t.Union[float, ArrayLike]:
"""Convert length forwards."""
if self.from_units == self.to_units:
return force
return force * self.from_units.force_unit / self.to_units.force_unit

@property
def from_units(self) -> UnitSet:
"""The set of units to convert from, i.e. we convert forwards from this
set of units.
"""
return self._from_units

@property
def to_units(self) -> UnitSet:
"""The set of units to convert to, i.e. we convert forwards to this set
of units.
"""
return (
self._to_units if self._to_units is not None else self._from_units
)
27 changes: 23 additions & 4 deletions structuralcodes/core/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,38 @@
from numpy.typing import ArrayLike

import structuralcodes.core._section_results as s_res
from structuralcodes.core._units import UnitConverter, UnitSet


class Material(abc.ABC):
"""Abstract base class for materials."""

_constitutive_law = None

def __init__(self, density: float, name: t.Optional[str] = None) -> None:
_unit_converter: UnitConverter
_default_units: UnitSet
_units: UnitSet

def __init__(
self,
density: float,
name: t.Optional[str] = None,
units: t.Optional[UnitSet] = None,
) -> None:
"""Initializes an instance of a new material.

Args:
density (float): density of the material in kg/m3
density (float): Density of the material in kg/m3.

Keyword Args:
name (Optional[str]): descriptive name of the material
name (Optional[str]): Descriptive name of the material.
units (Optional[UnitSet]): The selected set of units to work in. If
not set, the default set of units for the specific class is
used.
"""
self._density = abs(density)
self._name = name if name is not None else 'Material'
self._units = units
self._unit_converter = UnitConverter(self._default_units, units)

def update_attributes(self, updated_attributes: t.Dict) -> None:
"""Function for updating the attributes specified in the input
Expand Down Expand Up @@ -64,6 +78,11 @@ def density(self):
"""Returns the density of the material in kg/m3."""
return self._density

@property
def unit_converter(self) -> UnitConverter:
"""Returns the unit converter of the material."""
return self._unit_converter


class ConstitutiveLaw(abc.ABC):
"""Abstract base class for constitutive laws."""
Expand Down
8 changes: 5 additions & 3 deletions structuralcodes/materials/concrete/_concrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import abc
import typing as t

from structuralcodes.core._units import UnitSet
from structuralcodes.core.base import ConstitutiveLaw, Material
from structuralcodes.materials.constitutive_laws import create_constitutive_law

Expand All @@ -22,10 +23,11 @@ def __init__(
density: float = 2400,
gamma_c: t.Optional[float] = None,
existing: t.Optional[bool] = False,
units: t.Optional[UnitSet] = None,
) -> None:
"""Initializes an abstract concrete material."""
name = name if name is not None else 'Concrete'
super().__init__(density=density, name=name)
super().__init__(density=density, name=name, units=units)

self._fck = abs(fck)
if existing:
Expand All @@ -38,12 +40,12 @@ def __init__(

@property
def fck(self) -> float:
"""Returns fck in MPa."""
"""Returns fck."""
return self._fck

@fck.setter
def fck(self, fck: float) -> None:
"""Setter for fck (in MPa)."""
"""Setter for fck."""
self._fck = abs(fck)
self._reset_attributes()

Expand Down
Loading