From af7099238b4e8179ec2e00f22b44ff667bdeda81 Mon Sep 17 00:00:00 2001 From: James Bayley <36523314+jamesbayley@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:47:40 +0000 Subject: [PATCH] Implement initial behaviour --- gazelle/generics.py | 39 ++++ gazelle/steel/__init__.py | 0 gazelle/steel/errors.py | 22 ++ gazelle/steel/fabricator.py | 238 +++++++++++++++++++ gazelle/steel/sections/__init__.py | 0 gazelle/steel/sections/abc.py | 107 +++++++++ gazelle/steel/sections/collections.py | 28 +++ gazelle/steel/sections/hollow.py | 180 +++++++++++++++ gazelle/steel/sections/universal.py | 107 +++++++++ gazelle/units.py | 314 ++++++++++++++++++++++++++ 10 files changed, 1035 insertions(+) create mode 100644 gazelle/generics.py create mode 100644 gazelle/steel/__init__.py create mode 100644 gazelle/steel/errors.py create mode 100644 gazelle/steel/fabricator.py create mode 100644 gazelle/steel/sections/__init__.py create mode 100644 gazelle/steel/sections/abc.py create mode 100644 gazelle/steel/sections/collections.py create mode 100644 gazelle/steel/sections/hollow.py create mode 100644 gazelle/steel/sections/universal.py create mode 100644 gazelle/units.py diff --git a/gazelle/generics.py b/gazelle/generics.py new file mode 100644 index 0000000..48036c5 --- /dev/null +++ b/gazelle/generics.py @@ -0,0 +1,39 @@ +# Gazelle: a fast, cross-platform engine for structural analysis & design. +# Copyright (C) 2024 James S. Bayley +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from typing import Callable, TypeVar + + +T = TypeVar("T") + +def mutate(data: T, transforms: list[Callable[[T], T]]) -> T: + """ + Apply list of inplace transformations to object. + + Args: + data: The original object to be transformed. + transforms: The transformation functions to apply to the original object. + + Returns: + The original object modified 'inplace' by the list of transformations. + + """ + + for transform in transforms: + data = transform(data) + + return data diff --git a/gazelle/steel/__init__.py b/gazelle/steel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gazelle/steel/errors.py b/gazelle/steel/errors.py new file mode 100644 index 0000000..9351f62 --- /dev/null +++ b/gazelle/steel/errors.py @@ -0,0 +1,22 @@ +class SteelSectionCategoryNotFound(BaseException): + """ + A specified steel section category is either + currently unsupported in our application, or + simply does not exist in the Blue Book data. + """ + + +class SteelSectionDesignationNotFound(BaseException): + """ + A specified steel section designation is + either currently unsupported in our application, + or simply does not exist in the Blue Book data. + """ + + +class SteelFabricatorNotSupported(BaseException): + """ + The specified steel fabricator either is either + currently unsupported in our application, or + simply does not exist. + """ diff --git a/gazelle/steel/fabricator.py b/gazelle/steel/fabricator.py new file mode 100644 index 0000000..df23c96 --- /dev/null +++ b/gazelle/steel/fabricator.py @@ -0,0 +1,238 @@ +# Gazelle: a fast, cross-platform engine for structural analysis & design. +# Copyright (C) 2024 James S. Bayley +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +""" +The Steel for Life 'Blue Book' is the standard tome used by Structural Engineers +in the UK to design and specify structural steel sections. The book contains +a plethora of design data that is fundamental to steel design in accordance with +Eurocode 3 (EC3). + +This module provides a convenient API interface to interact with, and transform, +the data captured within the Blue Book. Data has been sourced from the official +website: https://www.steelforlifebluebook.co.uk. +""" + +import json +import pathlib +from enum import Enum +from typing import Generic, Tuple + +from gazelle.steel.errors import ( + SteelSectionCategoryNotFound, + SteelSectionDesignationNotFound +) +from gazelle.steel.sections.abc import ( + SectionDesignation, + SectionProperties +) +from gazelle.steel.sections.collections import ( + S, + SteelSections +) +from gazelle.steel.sections.hollow import ( + ColdFormedCircularHollowSection, + ColdFormedRectangularHollowSection, + ColdFormedSquareHollowSection, + HotFormedCircularHollowSection, + HotFormedEllipticalHollowSection, + HotFormedRectangularHollowSection, + HotFormedSquareHollowSection +) + +from gazelle.steel.sections.universal import ( + UniversalBeam, + UniversalColumn, + UniversalBearingPile +) +from gazelle.units import ( + Metre +) + + +class SectionCategory(Enum): + UB = "Universal Beam" + UC = "Universal Column" + PFC = "Parallel Flange Channel" + UBP = "Universal Bearing Pile" + EQUAL_L = "Equal L Section" + UNEQUAL_L = "Unequal L Section" + COLD_FORMED_CHS = "Cold Formed Circular Hollow Section" + COLD_FORMED_RHS = "Cold Formed Rectangular Hollow Section" + COLD_FORMED_SHS = "Cold Formed Square Hollow Section" + HOT_FORMED_CHS = "Hot Formed Circular Hollow Section" + HOT_FORMED_RHS = "Hot Formed Rectangular Hollow Section" + HOT_FORMED_SHS = "Hot Formed Square Hollow Section" + HOT_FORMED_EHS = "Hot Formed Elliptical Hollow Section" + T_SPLIT_FROM_UB = "T Split From UB Section" + T_SPLIT_FROM_UC = "T Split From UC Section" + + +class SteelFabricator(Generic[S]): + """ + The Steel Fabricator class handles the configuration and instantiation + of structural steel objects. The class adopts the Static Factory Pattern + providing a single convenient API to generate Steel for Life 'Blue Book' + sections. + """ + + @classmethod + def _load_section_data_for(cls, category: SectionCategory) -> dict[SectionDesignation, SectionProperties]: + """ + Returns the properties and dimensions for all sections in a given category, + as defined in the Steel for Life 'Blue Book'. + + Args: + category: The selected 'Blue Book' steel section category. + + Returns: + The full collection of steel sections in the category, as a dictionary. + + Raises: + SectionCategoryNotFound: If the specified category is unsupported. + FileNotFoundError: If the requested Blue Book JSON data file is missing. + """ + + dir_path = pathlib.Path(__file__).parent.parent / ".d/bluebook/properties" + + categories = { + SectionCategory.UB: dir_path / "ub.json", + SectionCategory.UC: dir_path / "uc.json", + SectionCategory.PFC: dir_path / "pfc.json", + SectionCategory.UBP: dir_path / "ubp.json", + SectionCategory.EQUAL_L: dir_path / "equal-l.json", + SectionCategory.UNEQUAL_L: dir_path / "unequal-l.json", + SectionCategory.COLD_FORMED_CHS: dir_path / "cf-chs.json", + SectionCategory.COLD_FORMED_RHS: dir_path / "cf-rhs.json", + SectionCategory.COLD_FORMED_SHS: dir_path / "cf-shs.json", + SectionCategory.HOT_FORMED_CHS: dir_path / "hf-chs.json", + SectionCategory.HOT_FORMED_RHS: dir_path / "hf-rhs.json", + SectionCategory.HOT_FORMED_SHS: dir_path / "hf-shs.json", + SectionCategory.HOT_FORMED_EHS: dir_path / "hf-ehs.json", + SectionCategory.T_SPLIT_FROM_UB: dir_path / "t-split-from-ub.json", + SectionCategory.T_SPLIT_FROM_UC: dir_path / "t-split-from-uc.json", + } + + if category not in categories: + raise SteelSectionCategoryNotFound(f"{category.name}.") + + file = categories[category] + + if not file.exists(): + raise FileNotFoundError(f"Section Category: {category}.") + + with open(file, "r", encoding="utf-8") as sections: + return json.load(sections) + + + # @classmethod + # def _inject_carbon_data(cls, definitions: dict[SectionDesignation, SectionProperties]) -> dict[SectionDesignation, SectionProperties]: + # """ + # For each structural section listed in the section definitions, + # proceed to compute the mass of carbon per unit length and then + # update the section definition with this new property. + + # :param SectionDefinitions definitions: A dictionary structure containing the + # full collection of steel section data for a given Blue Book category. + # :return: The original SectionDefinitions object modified 'inplace' to augment + # the dictionary structure with 'Carbon Per Metre (CO2/m)' properties. + # """ + + # for section_properties in definitions.values(): + # carbon_per_metre = ( + # section_properties["Mass Per Metre (kg/m)"] * + # MILD_STEEL.carbon_per_kg + # ) + # section_properties["Carbon Per Metre (CO2/m)"] = carbon_per_metre + + # return definitions + + + @classmethod + def _get_sections_and_constructor_for(cls, category: SectionCategory) -> Tuple[S, dict[SectionDesignation, SectionProperties]]: + """ + :param SectionCategory category: Selected 'Blue Book' steel section category. + :return: A tuple containing the constructor and steel section collection. + :raises SectionCategoryNotFound: If the specified category is not supported. + """ + + constructors: dict[SectionCategory, S] = { + SectionCategory.UB: UniversalBeam, + SectionCategory.UC: UniversalColumn, + SectionCategory.UBP: UniversalBearingPile, + SectionCategory.HOT_FORMED_RHS: HotFormedRectangularHollowSection, + SectionCategory.HOT_FORMED_SHS: HotFormedSquareHollowSection, + SectionCategory.HOT_FORMED_CHS: HotFormedCircularHollowSection, + SectionCategory.HOT_FORMED_EHS: HotFormedEllipticalHollowSection, + SectionCategory.COLD_FORMED_RHS: ColdFormedRectangularHollowSection, + SectionCategory.COLD_FORMED_SHS: ColdFormedSquareHollowSection, + SectionCategory.COLD_FORMED_CHS: ColdFormedCircularHollowSection + } + + if category not in constructors: + raise SteelSectionCategoryNotFound(f"{category}.") + + ctor = constructors[category] + sections = cls._load_section_data_for(category) + # mutate(sections, [cls._inject_carbon_data]) + + return ctor, sections + + + @classmethod + def make_section(cls, category: SectionCategory, designation: SectionDesignation, length: Metre = Metre(1.0)) -> S: + """ + Instantiate a specific steel section from a given category. For the full + list of available steel section designations, the user is advised to + consult the Steel for Life 'Blue Book' website: https://www.steelforlifebluebook.co.uk. + + Args: + category: Selected 'Blue Book' steel section category. + designation: Unique identifier for the steel section. + length: The desired steel section length (defaults to 1.0m). + + Returns: + Single steel section of given category with the specified designation. + + Raises: + SectionDesignationNotFound: If unsupported designation is provided. + """ + + (ctor, sections) = cls._get_sections_and_constructor_for(category) + + if designation not in sections: + raise SteelSectionDesignationNotFound(f"{designation}.") + + return ctor(designation, sections[designation], length) + + + @classmethod + def make_all_sections(cls, category: SectionCategory, length: Metre = Metre(1.0)) -> SteelSections[S]: + """ + Instantiates the full collection of steel sections for a given category. + For the full list of available steel section categories, the user is + advised to consult the Steel for Life 'Blue Book' website: https://www.steelforlifebluebook.co.uk. + + Args: + category: Selected 'Blue Book' steel section category. + length: The desired length for all steel sections. + + Returns: + A list of steel section objects. + """ + + (ctor, sections) = cls._get_sections_and_constructor_for(category) + return SteelSections([ctor(d, sections[d], length) for d in sections.keys()]) diff --git a/gazelle/steel/sections/__init__.py b/gazelle/steel/sections/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gazelle/steel/sections/abc.py b/gazelle/steel/sections/abc.py new file mode 100644 index 0000000..2ebb3a6 --- /dev/null +++ b/gazelle/steel/sections/abc.py @@ -0,0 +1,107 @@ +""" +Abstract base class representations common to all steel sections. + +Typically, users will not use this module directly. All 'concrete' +steel sections implemented elsewhere within the package inherit +from classes within this module. Users can reliably depend on the +attributes and methods defined herein as the public interface +for all steel sections and collections of steel sections. + +Nevertheless, specific steel section types do provide more +specialised, refined implementations and additional attributes. +Therefore, if a particular attribute has not been defined +within the abstract base classes, it is worth checking the +specific section implementations. + +The real advantage of this module is that it provides generic +behaviours that are common to all steel sections, regardless +of their specific geometries. For example, all steel sections +have a 'mass per metre length', hence it is sensible to capture +this behaviour at the top of the class hierarchy. +""" + +from abc import ABC, abstractmethod +from typing import NewType, Union + +from gazelle.units import ( + Area, + Carbon, + Centimetre, + Kilogram, + Mass, + Metre, + SurfaceArea, + Tonne +) + + +SectionDesignation = NewType("SectionDesignation", str) +SectionProperties = NewType("SectionProperties", dict[str, Union[bool, float]]) + + +class SteelSection(ABC): + """An abstract class for a steel section.""" + + def __init__(self, des: SectionDesignation, props: SectionProperties, length: Metre): + """Initialise the section.""" + self._properties = props + self.designation = des + self.length = length + + @property + def is_nonstandard(self) -> bool: + """Whether the section is non-standard (i.e., uncommon in the UK).""" + return self._properties["Is Non-Standard"] + + @property + def cross_sectional_area(self) -> Area[Centimetre]: + """The cross-sectional area of the section.""" + return Area(self._properties["Area of Section, A (cm2)"], Centimetre) + + @property + def total_mass(self) -> Kilogram: + """The total mass of the section.""" + return self.mass_per_metre_length * self.length + + @property + def mass_per_metre_length(self) -> Mass.PerUnitLength[Kilogram, Metre]: + """The mass per metre length of the section.""" + return Kilogram(self._properties["Mass Per Metre (kg/m)"]).per_unit_length(Metre) + + @property + def surface_area_per_metre(self) -> SurfaceArea.PerUnitLength[Metre, Metre]: + """The surface area per metre length of the section.""" + return SurfaceArea(self._properties["Surface Area Per Metre (m2)"], Metre) \ + .per_unit_length(Metre) + + @property + def surface_area_per_tonne(self) -> SurfaceArea.PerUnitMass[Metre, Tonne]: + """The surface area per tonne of the section.""" + return SurfaceArea(self._properties["Surface Area Per Tonne (m2)"], Metre) \ + .per_unit_mass(Tonne) + + @property + def carbon_per_metre(self) -> Carbon.PerUnitLength[Metre]: + """The carbon per metre length of the section.""" + return Carbon(self._properties["Carbon Per Metre (CO2/m)"]) \ + .per_unit_length(Metre) + + def to_json(self): + """Return a JSON representation of the section.""" + return { + "sectionClassification": str(self), + "isNonstandard": self.is_nonstandard, + "sectionProperties": { + "length": self.length.to_json(), + "massPerMetreLength": self.mass_per_metre_length.to_json(), + "totalMass": self.total_mass.to_json(), + "crossSectionalArea": self.cross_sectional_area.to_json(), + "surfaceAreaPerMetre": self.surface_area_per_metre.to_json(), + "surfaceAreaPerTonne": self.surface_area_per_tonne.to_json(), + # "carbonPerMetre": self.carbon_per_metre.to_json() + } + } + + @abstractmethod + def __str__(self) -> str: + pass diff --git a/gazelle/steel/sections/collections.py b/gazelle/steel/sections/collections.py new file mode 100644 index 0000000..0ea6e72 --- /dev/null +++ b/gazelle/steel/sections/collections.py @@ -0,0 +1,28 @@ +from typing import Generic, TypeVar + +from gazelle.steel.sections.abc import ( + SteelSection +) + + +S = TypeVar("S", bound=SteelSection) + + +class SteelSections(Generic[S]): + """A collection of steel sections.""" + + def __init__(self, sections: list[S]): + """Initialise the collection.""" + self.sections = sections + + def __len__(self): + """Return the number of sections in the collection.""" + return len(self.sections) + + def __getitem__(self, index: int): + """Return the section at the given index.""" + return self.sections[index] + + def to_json(self): + """Return a JSON representation of the collection.""" + return {section.designation: section.to_json() for section in self.sections} diff --git a/gazelle/steel/sections/hollow.py b/gazelle/steel/sections/hollow.py new file mode 100644 index 0000000..7988ba0 --- /dev/null +++ b/gazelle/steel/sections/hollow.py @@ -0,0 +1,180 @@ +from abc import abstractmethod, ABC + +from gazelle.steel.sections.abc import SteelSection +from gazelle.units import Millimetre + + +class RectangularHollowSection(SteelSection, ABC): + """A rectangular hollow section.""" + + @property + def width(self) -> Millimetre: + """The width of the section.""" + return Millimetre(self.designation.split("x")[0]) + + @property + def depth(self) -> Millimetre: + """The depth of the section.""" + return Millimetre(self.designation.split("x")[1]) + + @property + def flange_thickness(self) -> Millimetre: + """The thickness of each vertical flange.""" + return Millimetre(self.designation.split("x")[2]) + + @property + def web_thickness(self) -> Millimetre: + """The thickness of each horizontal web.""" + return Millimetre(self.designation.split("x")[2]) + + def to_json(self): + """Return a JSON representation of the section.""" + return { + "sectionClassification": str(self), + "isNonstandard": self.is_nonstandard, + "sectionProperties": { + "width": self.width.to_json(), + "depth": self.depth.to_json(), + "length": self.length.to_json(), + "massPerMetreLength": self.mass_per_metre_length.to_json(), + "totalMass": self.total_mass.to_json(), + "crossSectionalArea": self.cross_sectional_area.to_json(), + "surfaceAreaPerMetre": self.surface_area_per_metre.to_json(), + "surfaceAreaPerTonne": self.surface_area_per_tonne.to_json(), + "carbonPerMetre": self.carbon_per_metre.to_json(), + "flangeThickness": self.flange_thickness.to_json(), + "webThickness": self.web_thickness.to_json(), + + } + } + + @abstractmethod + def __str__(self): + pass + + +class EllipticalHollowSection(SteelSection, ABC): + """An elliptical hollow section.""" + + @property + def width(self) -> Millimetre: + """The width of the section.""" + return Millimetre(self.designation.split("x")[0]) + + @property + def depth(self) -> Millimetre: + """The depth of the section.""" + return Millimetre(self.designation.split("x")[1]) + + @property + def plate_thickness(self) -> Millimetre: + """The thickness of each vertical flange.""" + return Millimetre(self.designation.split("x")[1]) + + def to_json(self): + """Return a JSON representation of the section.""" + return { + "sectionClassification": str(self), + "isNonstandard": self.is_nonstandard, + "sectionProperties": { + "width": self.width.to_json(), + "depth": self.depth.to_json(), + "length": self.length.to_json(), + "massPerMetreLength": self.mass_per_metre_length.to_json(), + "totalMass": self.total_mass.to_json(), + "crossSectionalArea": self.cross_sectional_area.to_json(), + "surfaceAreaPerMetre": self.surface_area_per_metre.to_json(), + "surfaceAreaPerTonne": self.surface_area_per_tonne.to_json(), + "carbonPerMetre": self.carbon_per_metre.to_json(), + "plateThickness": self.plate_thickness.to_json(), + } + } + + @abstractmethod + def __str__(self): + pass + + +class CircularHollowSection(SteelSection, ABC): + """A circular hollow section.""" + + @property + def diameter(self) -> Millimetre: + """The width of the section.""" + return Millimetre(self.designation.split("x")[0]) + + @property + def plate_thickness(self) -> Millimetre: + """The thickness of each vertical flange.""" + return Millimetre(self.designation.split("x")[1]) + + def to_json(self): + """Return a JSON representation of the section.""" + return { + "sectionClassification": str(self), + "isNonstandard": self.is_nonstandard, + "sectionProperties": { + "length": self.length.to_json(), + "diameter": self.diameter.to_json(), + "massPerMetreLength": self.mass_per_metre_length.to_json(), + "totalMass": self.total_mass.to_json(), + "crossSectionalArea": self.cross_sectional_area.to_json(), + "surfaceAreaPerMetre": self.surface_area_per_metre.to_json(), + "surfaceAreaPerTonne": self.surface_area_per_tonne.to_json(), + "carbonPerMetre": self.carbon_per_metre.to_json(), + "plateThickness": self.plate_thickness.to_json(), + } + } + + @abstractmethod + def __str__(self): + pass + + +class HotFormedRectangularHollowSection(RectangularHollowSection): + """A hot-formed rectangular hollow section.""" + + def __str__(self): + return f"Hot Formed Rectangular Hollow Section (HF-RHS): {self.designation}" + + +class ColdFormedRectangularHollowSection(RectangularHollowSection): + """A cold-formed rectangular hollow section.""" + + def __str__(self): + return f"Cold Formed Rectangular Hollow Section (CF-RHS): {self.designation}" + + +class HotFormedSquareHollowSection(RectangularHollowSection): + """A hot-formed square hollow section.""" + + def __str__(self): + return f"Hot Formed Square Hollow Section (HF-SHS): {self.designation}" + + +class ColdFormedSquareHollowSection(RectangularHollowSection): + """A cold-formed square hollow section.""" + + def __str__(self): + return f"Cold Formed Square Hollow Section (CF-SHS): {self.designation}" + + +class HotFormedCircularHollowSection(CircularHollowSection): + """A hot-formed circular hollow section.""" + + def __str__(self): + return f"Hot Formed Circular Hollow Section (HF-CHS): {self.designation}" + + +class ColdFormedCircularHollowSection(CircularHollowSection): + """A cold-formed circular hollow section.""" + + def __str__(self): + return f"Cold Formed Circular Hollow Section (CF-CHS): {self.designation}" + + +class HotFormedEllipticalHollowSection(EllipticalHollowSection): + """A hot-formed elliptical hollow section.""" + + def __str__(self): + return f"Hot Formed Elliptical Hollow Section (HF-EHS): {self.designation}" diff --git a/gazelle/steel/sections/universal.py b/gazelle/steel/sections/universal.py new file mode 100644 index 0000000..2942f61 --- /dev/null +++ b/gazelle/steel/sections/universal.py @@ -0,0 +1,107 @@ +""" +The collection of steel section types, as +defined in the Steel for Life 'Blue Book'. +""" + +from abc import abstractmethod, ABC + +from gazelle.units import ( + Centimetre, + Millimetre +) +from gazelle.steel.sections.abc import ( + SteelSection +) + + +class UniversalSection(SteelSection, ABC): + @property + def depth(self) -> Millimetre: + return Millimetre(self._properties["Depth of Section, h (mm)"]) + + @property + def width(self) -> Millimetre: + return Millimetre(self._properties["Width of Section, b (mm)"]) + + @property + def web_thickness(self) -> Millimetre: + return Millimetre(self._properties["Web Thickness, tw (mm)"]) + + @property + def flange_thickness(self) -> Millimetre: + return Millimetre(self._properties["Flange Thickness, tf (mm)"]) + + @property + def root_radius(self) -> Millimetre: + return Millimetre(self._properties["Root Radius, r (mm)"]) + + @property + def depth_between_fillets(self) -> Millimetre: + return Millimetre(self._properties["Depth Between Fillets, d (mm)"]) + + @property + def end_clearance_for_detailing(self) -> Millimetre: + return Millimetre(self._properties["End Clearance Dimension for Detailing, C (mm)"]) + + @property + def long_notch_dimension(self) -> Millimetre: + return Millimetre(self._properties["Longitudinal Notch Dimension, N (mm)"]) + + @property + def vertical_notch_dimension(self) -> Millimetre: + return Millimetre(self._properties["Vertical Notch Dimension, n (mm)"]) + + @property + def radius_of_gyration_yy(self) -> Centimetre: + return Centimetre(self._properties["Radius of Gyration, Y-Y (cm)"]) + + @property + def radius_of_gyration_zz(self) -> Centimetre: + return Centimetre(self._properties["Radius of Gyration, Z-Z (cm)"]) + + def to_json(self): + return { + "sectionClassification": str(self), + "isNonstandard": self.is_nonstandard, + "sectionProperties": { + "length": self.length.to_json(), + "massPerMetreLength": self.mass_per_metre_length.to_json(), + "totalMass": self.total_mass.to_json(), + "crossSectionalArea": self.cross_sectional_area.to_json(), + "surfaceAreaPerMetre": self.surface_area_per_metre.to_json(), + "surfaceAreaPerTonne": self.surface_area_per_tonne.to_json(), + "carbonPerMetre": self.carbon_per_metre.to_json(), + "depth": self.depth.to_json(), + "width": self.width.to_json(), + "webThickness": self.web_thickness.to_json(), + "flangeThickness": self.flange_thickness.to_json(), + "rootRadius": self.root_radius.to_json(), + "depthBetweenFillets": self.depth_between_fillets.to_json(), + "endClearanceForDetailing": self.end_clearance_for_detailing.to_json(), + "longitudinalNotchDimension": self.long_notch_dimension.to_json(), + "verticalNotchDimension": self.vertical_notch_dimension.to_json(), + "radiusOfGyration": { + "yy": self.radius_of_gyration_yy.to_json(), + "zz": self.radius_of_gyration_zz.to_json(), + }, + }, + } + + @abstractmethod + def __str__(self): + pass + + +class UniversalBeam(UniversalSection): + def __str__(self): + return f"Universal Beam (UB) Section: {self.designation}" + + +class UniversalColumn(UniversalSection): + def __str__(self): + return f"Universal Column (UC) Section: {self.designation}" + + +class UniversalBearingPile(UniversalSection): + def __str__(self): + return f"Universal Bearing Pile (UBP) Section: {self.designation}" diff --git a/gazelle/units.py b/gazelle/units.py new file mode 100644 index 0000000..b98b9cc --- /dev/null +++ b/gazelle/units.py @@ -0,0 +1,314 @@ +# Gazelle: a fast, cross-platform engine for structural analysis & design. +# Copyright (C) 2024 James S. Bayley +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + + +from abc import ABC +from math import pi +from typing import Generic, TypeVar, Type, Union + + +M = TypeVar("M", bound="Mass") +L = TypeVar("L", bound="Length") +L2 = TypeVar("L2", bound="Length") +T = TypeVar("T", bound="Dimension") +U = TypeVar("U", bound="Dimension") +W = TypeVar("W", bound="Dimension") + + +class Ratio: + def __init__(self, value: float): + if value >= 0.0: + self._dtype = type(self) + self.value = value + self.units = None + else: + raise ValueError(f"{self.__class__.__name__} < 0.") + + def to_json(self): + return {"value": f"{self.value:.2f}", "units": self.units} + + +class Dimension(Ratio, ABC): + def __init__(self, value: float): + super().__init__(value) + + def __repr__(self) -> str: + return f"{self.value:.2f} {self.units}" + + +class Length(Dimension, ABC): + def __init__(self, value: float): + super().__init__(value) + + def __mul__(self: L, other: L) -> "Area[L]": + if type(self) is type(other): + return Area(self.value * other.value, type(self)) + raise TypeError(f"Incompatible: {type(self)} and {type(other)}.") + + +class Mass(Dimension, ABC): + def __init__(self, value: float): + super().__init__(value) + + def __truediv__(self: M, volume: "Volume[L]") -> "Density[M, L]": + return Density(self, volume) + + def per_unit_length(self, unit_length: Type[L]) -> "Mass.PerUnitLength[L]": + return Mass.PerUnitLength(self, unit_length) + + class PerUnitLength(Dimension, Generic[M, L]): + def __init__(self, mass: M, unit_length: Type[L]): + super().__init__(mass.value) + self._dtype = (mass._dtype, unit_length(1)._dtype) + self.units = f"{self._dtype[0](1).units}/{self._dtype[1](1).units}" + + def __mul__(self, other: L) -> M: + if self._dtype[1] is other._dtype: + return self._dtype[0](self.value * other.value) + raise TypeError(f"Incompatible: {self._dtype[1]} and {other._dtype}.") + + +class Area(Dimension, Generic[L]): + def __init__(self, value: float, dtype: Type[L]): + super().__init__(value) + self._dtype = dtype + self.units = f"{self._dtype(1).units}^2" + + @classmethod + def from_rectangle(cls, width: L, depth: L) -> "Area[L]": + if type(width) is type(depth): + return width * depth + raise TypeError(f"Incompatible: {type(width)} and {type(depth)}.") + + @classmethod + def from_circle(cls, diameter: L) -> "Area[L]": + area = (pi * diameter.value**2.0) / 4.0 + return cls(area, type(diameter)) + + def __mul__(self: "Area[L]", length: L) -> "Volume[L]": + if self._dtype is type(length): + return Volume(self.value * length.value, self._dtype) + raise TypeError(f"Incompatible: {type(length)} and {self._dtype}.") + + +class SurfaceArea(Dimension, Generic[L]): + def __init__(self, value: float, dtype: Type[L]): + super().__init__(value) + self._dtype = dtype + self.units = f"{self._dtype(1).units}^2" + + def per_unit_length( + self, unit_length: Type[L2] + ) -> "SurfaceArea.PerUnitLength[L, L2]": + return SurfaceArea.PerUnitLength(self, unit_length) + + def per_unit_mass(self, unit_mass: Type[M]) -> "SurfaceArea.PerUnitMass[L, M]": + return SurfaceArea.PerUnitMass(self, unit_mass) + + @classmethod + def from_cuboid(cls, width: L, depth: L, length: L) -> "SurfaceArea[L]": + if type(width) is type(depth) and type(width) is type(length): + perimeter = (2 * width.value) + (2 * depth.value) + surface_area = perimeter * length.value + return cls(surface_area, type(width)) + raise TypeError(f"Incompatible: {type(width)}, {type(depth)}, {type(length)}.") + + @classmethod + def from_cylinder(cls, diameter: L, length: L) -> "SurfaceArea[L]": + if type(diameter) is type(length): + surface_area = pi * diameter.value * length.value + return cls(surface_area, type(diameter)) + raise TypeError(f"Incompatible: {type(diameter)} and {type(length)}.") + + class PerUnitLength(Dimension, Generic[L, L2]): + def __init__(self, surface_area: "SurfaceArea[L]", unit_length: Type[L2]): + super().__init__(surface_area.value) + self._dtype = (surface_area._dtype, unit_length(1)._dtype) + self.units = f"{self._dtype[0](1).units}^2/{self._dtype[1](1).units}" + + def __mul__(self, length: L2) -> "SurfaceArea[L]": + if self._dtype[1] is length._dtype: + return SurfaceArea(self.value * length.value, self._dtype[0]) + raise TypeError(f"Incompatible: {self._dtype[1]} and {length._dtype}.") + + class PerUnitMass(Dimension, Generic[L, M]): + def __init__(self, surface_area: "SurfaceArea[L]", unit_mass: Type[M]): + super().__init__(surface_area.value) + self._dtype = (surface_area._dtype, unit_mass(1)._dtype) + self.units = f"{self._dtype[0](1).units}^2/{self._dtype[1](1).units}" + + def __mul__(self, mass: M) -> "SurfaceArea[L]": + if self._dtype[1] is mass._dtype: + return SurfaceArea(self.value * mass.value, self._dtype[0]) + raise TypeError(f"Incompatible: {self._dtype[1]} and {mass._dtype}.") + + +class Volume(Dimension, Generic[L]): + def __init__(self, value: float, dtype: Type[L]): + super().__init__(value) + self._dtype = dtype + self.units = f"{self._dtype(1).units}^3" + + @classmethod + def from_area(cls, area: Area[L], length: L) -> "Volume[L]": + if area._dtype is type(length): + return area * length + raise TypeError(f"Incompatible types: {area._dtype} and {type(length)}.") + + @classmethod + def from_cuboid(cls, width: L, depth: L, length: L) -> "Volume[L]": + if type(width) is type(depth) and type(width) is type(length): + return width * depth * length + raise TypeError(f"Incompatible: {type(width)}, {type(depth)}, {type(length)}.") + + @classmethod + def from_cylinder(cls, diameter: L, length: L) -> "Volume[L]": + if type(diameter) is type(length): + return Area.from_circle(diameter) * length + raise TypeError(f"Incompatible types: {diameter._dtype} and {length._dtype}.") + + def __truediv__(self, other: Union[Area[L], L]) -> Union[Area[L], L]: + if self._dtype is not other._dtype: + raise TypeError(f"Incompatible: {self._dtype} and {other._dtype}.") + + if isinstance(other, Area): + return self._dtype(self.value / other.value) + + if isinstance(other, Length): + return Area(self.value / other.value, self._dtype) + + raise NotImplementedError(f"Unexpected: {type(other)}.") + + +class Carbon(Dimension): + def __init__(self, value): + super().__init__(value) + self.units = "CO2" + + def per_unit_length(self, unit_length: Type[L]) -> "Carbon.PerUnitLength[L]": + return Carbon.PerUnitLength(self, unit_length) + + class PerUnitLength(Dimension, Generic[L]): + def __init__(self, CO2: "Carbon", unit_length: Type[L]): + super().__init__(CO2.value) + self._dtype = (CO2._dtype, unit_length(1)._dtype) + self.units = f"{self._dtype[0](1).units}/{self._dtype[1](1).units}" + + def __mul__(self, other: L) -> "Carbon": + if self._dtype[1] is other._dtype: + return Carbon(self.value * other.value) + raise TypeError(f"Incompatible: {self._dtype[1]} and {other._dtype}.") + + +class Density(Dimension, Generic[M, L]): + def __init__(self, mass: M, volume: Volume[L]): + super().__init__(mass.value / volume.value) + self._dtype = (mass._dtype, volume._dtype) + self.units = f"{self._dtype[0](1).units}/{self._dtype[1](1).units}^3" + + def __mul__(self, other: Volume[L]) -> M: + if self._dtype[1] is other._dtype: + return self._dtype[0](self.value * other.value) + raise TypeError(f"Incompatible: {self._dtype[1]} and {other._dtype}.") + + +class Millimetre(Length): + def __init__(self, value): + super().__init__(value) + self.units = "mm" + + def to_centimetre(self) -> "Centimetre": + return Centimetre(self.value / 10.0) + + def to_metre(self) -> "Metre": + return Metre(self.value / 1000.0) + + +class Centimetre(Length): + def __init__(self, value): + super().__init__(value) + self.units = "cm" + + def to_millimetre(self) -> "Metre": + return Millimetre(self.value * 10.0) + + def to_metre(self) -> "Metre": + return Metre(self.value / 100.0) + + +class Metre(Length): + def __init__(self, value): + super().__init__(value) + self.units = "m" + + def to_millimetre(self) -> Millimetre: + return Millimetre(self.value * 1000.0) + + def to_centimetre(self) -> Centimetre: + return Centimetre(self.value * 100.0) + + +class Kilogram(Mass): + def __init__(self, value): + super().__init__(value) + self.units = "kg" + + def to_tonne(self) -> "Tonne": + return Tonne(self.value / 1000.0) + + +class Tonne(Mass): + def __init__(self, value): + super().__init__(value) + self.units = "tonne" + + def to_kilogram(self) -> "Kilogram": + return Kilogram(self.value * 1000.0) + + +if __name__ == "__main__": + w = Centimetre(2.5) + d = Centimetre(1.5) + m = Metre(4) + a = w * d + kg = Kilogram(5) + v = a * Centimetre(1) + den = kg / v + mpul = kg.per_unit_length(Metre) + length = Metre(2) + CO2 = Carbon(20) + cpul = CO2.per_unit_length(Millimetre) + surface_area = SurfaceArea(20.0, Millimetre) + surface_area_per_metre = surface_area.per_unit_length(Metre) + surface_area_per_metre.to_json() + + print(f"Width: {w}.") + print(f"Depth: {d}.") + print(f"Area: {a}.") + print(f"Mass: {kg}.") + print(f"Volume: {v}.") + print(f"Density: {den}.") + print(f"Density x Volume: {den * v}.") + print(f"Length: {v / a}.") + print(f"Vol / Width: {v / w}.") + print(f"Kilogram Per Metre: {mpul}.") + print(f"5kg/m x 2m: {mpul * length}") + print(f"Carbon: {CO2}.") + print(f"Carbon Per Metre Length: {cpul}.") + print(f"Surface Area: {surface_area}.") + print(f"Surface Area Per Metre Length: {surface_area_per_metre}.") + print(f"{surface_area_per_metre.to_json()}") + print(f"{cpul.to_json()}")