diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 5855ba3..5853db8 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1 +1 @@
-FROM mcr.microsoft.com/devcontainers/go:1.22-bookworm
\ No newline at end of file
+FROM mcr.microsoft.com/devcontainers/dotnet:1-8.0-bookworm
\ No newline at end of file
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index b3e92f3..f62c2d8 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -2,22 +2,25 @@
"name": "Gazelle",
"runArgs": ["--name", "gazelle"],
"build": { "dockerfile": "Dockerfile" },
+ "postCreateCommand": "pwsh Setup.ps1",
+ "privileged": true,
"features": {
"ghcr.io/devcontainers/features/git:1": {},
- "ghcr.io/devcontainers/features/github-cli:1": {},
- "ghcr.io/devcontainers/features/powershell:1": {}
+ "ghcr.io/devcontainers/features/github-cli:1": {}
},
"customizations": {
"vscode": {
"extensions": [
- "golang.go",
"github.copilot",
"github.vscode-github-actions",
- "ms-azuretools.vscode-docker",
- "ms-vscode.powershell",
- "ritwickdey.liveserver",
"grapecity.gc-excelviewer",
- "bierner.markdown-preview-github-styles"
+ "ms-dotnettools.csdevkit",
+ "ionide.ionide-fsharp",
+ "ms-vscode.powershell",
+ "ms-azuretools.vscode-docker",
+ "ms-dotnettools.dotnet-interactive-vscode",
+ "bierner.markdown-preview-github-styles",
+ "tamasfe.even-better-toml"
]
}
}
diff --git a/.github/CITATION.cff b/.github/CITATION.cff
new file mode 100644
index 0000000..fa21c9e
--- /dev/null
+++ b/.github/CITATION.cff
@@ -0,0 +1,17 @@
+cff-version: 1.2.0
+title: Gazelle
+message: >-
+ If you use this software, please cite it using the
+ metadata from this file.
+type: software
+authors:
+ - given-names: James
+ family-names: Bayley
+repository-code: 'https://github.com/gazellekit/gazelle'
+abstract: >-
+ A fast, cross-platform engine for structural analysis &
+ design.
+keywords:
+ - Civil Engineering
+ - Structural Engineering
+license: AGPL-3.0-or-later
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
similarity index 100%
rename from CODE_OF_CONDUCT.md
rename to .github/CODE_OF_CONDUCT.md
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..4db073b
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [jamesbayley]
\ No newline at end of file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a20c546..04183ad 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -11,15 +11,24 @@ defaults:
shell: pwsh
jobs:
- build:
- name: Build
+ setup:
+ name: CI
runs-on: Ubuntu-Latest
-
+
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- - name: Setup Go
- uses: actions/setup-go@v4
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v3
with:
- go-version-file: ./go.mod
\ No newline at end of file
+ dotnet-version: "8.0.x"
+
+ - name: Restore Project
+ run: ./Setup.ps1
+
+ - name: Check Code Formatting
+ run: dotnet fantomas . --check
+
+ - name: Run Test Suite
+ run: dotnet test
diff --git a/.gitignore b/.gitignore
index a40388d..cffb04d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
-# Go
+# .NET
bin/
-build/
+obj/
+.fake
# JavaScript
node_modules/
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index a20ae51..5bd305c 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,12 +1,14 @@
{
"recommendations": [
- "golang.go",
"github.copilot",
"github.vscode-github-actions",
- "ms-azuretools.vscode-docker",
- "ms-vscode.powershell",
- "ritwickdey.liveserver",
"grapecity.gc-excelviewer",
- "bierner.markdown-preview-github-styles"
+ "ms-dotnettools.csdevkit",
+ "ionide.ionide-fsharp",
+ "ms-vscode.powershell",
+ "ms-azuretools.vscode-docker",
+ "ms-dotnettools.dotnet-interactive-vscode",
+ "bierner.markdown-preview-github-styles",
+ "tamasfe.even-better-toml"
]
}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 79a9bd7..8af98d0 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,52 +1,46 @@
{
- "editor.bracketPairColorization.enabled": true,
- "editor.guides.bracketPairs": "active",
- "editor.inlineSuggest.enabled": true,
- "editor.rulers": [100],
- "editor.tabSize": 2,
-
- "debug.internalConsoleOptions": "neverOpen",
-
- "diffEditor.ignoreTrimWhitespace": false,
-
- "files.exclude": {
- "**/.git": false,
- },
-
- "git.autofetch": true,
- "git.confirmSync": false,
- "git.enableSmartCommit": true,
-
- "github.copilot.enable": {
- "*": true,
- "yaml": true,
- "plaintext": false,
- "markdown": false
- },
-
- "go.gopath": "/go",
- "go.useLanguageServer": true,
- "go.languageServerFlags": ["-rpc.trace"],
- "gopls": {
- "ui.semanticTokens": true
- },
-
- "[go]": {
- "editor.rulers": [80]
- },
-
- "powershell.powerShellDefaultVersion": "7",
- "powershell.powerShellAdditionalExePaths": {
- "pwsh": "/usr/local/bin/pwsh"
- },
-
- "liveServer.settings.root": "/docs",
- "liveServer.settings.file": "index.html",
- "liveServer.settings.host": "localhost",
- "liveServer.settings.port": 3000,
-
- "terminal.integrated.fontFamily": "'FiraCode Nerd Font Mono', 'Fira Code', Menlo, Monaco",
-
- "workbench.editor.highlightModifiedTabs": true,
- "workbench.editor.wrapTabs": true,
+ "editor.rulers": [80],
+ "editor.bracketPairColorization.enabled": true,
+ "editor.guides.bracketPairs": "active",
+ "editor.inlineSuggest.enabled": true,
+ "editor.detectIndentation": false,
+ "editor.insertSpaces": true,
+ "editor.tabSize": 2,
+
+ "debug.internalConsoleOptions": "neverOpen",
+
+ "diffEditor.ignoreTrimWhitespace": false,
+
+ "files.exclude": {
+ "**/.git": false,
+ "**/obj": true,
+ "**/bin": true
+ },
+
+ "git.autofetch": true,
+ "git.confirmSync": false,
+ "git.enableSmartCommit": true,
+
+ "github.copilot.enable": {
+ "*": true,
+ "yaml": true,
+ "plaintext": false,
+ "markdown": false
+ },
+
+ "[fsharp]": {
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "Ionide.Ionide-fsharp"
+ },
+
+ "powershell.powerShellDefaultVersion": "7",
+ "powershell.integratedConsole.showOnStartup": false,
+ "powershell.powerShellAdditionalExePaths": {
+ "pwsh": "/usr/bin/pwsh"
+ },
+
+ "terminal.integrated.fontFamily": "'FiraCode Nerd Font Mono', 'Fira Code', Menlo, Monaco",
+
+ "workbench.editor.highlightModifiedTabs": true,
+ "workbench.editor.wrapTabs": true,
}
\ No newline at end of file
diff --git a/Build.ps1 b/Build.ps1
deleted file mode 100644
index 38d523b..0000000
--- a/Build.ps1
+++ /dev/null
@@ -1,19 +0,0 @@
-Set-Location "$PSScriptRoot/.."
-
-$Platforms = @(
- @{ GOOS = "windows"; GOARCH = "amd64" }
- @{ GOOS = "linux"; GOARCH = "amd64" }
- @{ GOOS = "darwin"; GOARCH = "amd64" }
-)
-
-$InputFile = "./cmd/gazelle/main.go"
-
-$Platforms | ForEach-Object {
- $Env:GOOS = $_.GOOS
- $Env:GOARCH = $_.GOARCH
-
- $Extension = if ($_.GOOS -eq "windows") { ".exe" } else { "" }
- $Executable = "gazelle-$($_.GOOS)-$($_.GOARCH)$Extension"
-
- go build -o "./build/$Executable"
-}
diff --git a/Gazelle.sln b/Gazelle.sln
new file mode 100644
index 0000000..85b7dbb
--- /dev/null
+++ b/Gazelle.sln
@@ -0,0 +1,28 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gazelle", "src\Gazelle.fsproj", "{5428DD04-09FD-41FD-B89B-E58C64DCBDCF}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Gazelle.Tests", "tests\Gazelle.Tests.fsproj", "{D539F49E-5374-4272-BFBB-F198068F1665}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {5428DD04-09FD-41FD-B89B-E58C64DCBDCF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5428DD04-09FD-41FD-B89B-E58C64DCBDCF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5428DD04-09FD-41FD-B89B-E58C64DCBDCF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5428DD04-09FD-41FD-B89B-E58C64DCBDCF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D539F49E-5374-4272-BFBB-F198068F1665}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D539F49E-5374-4272-BFBB-F198068F1665}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D539F49E-5374-4272-BFBB-F198068F1665}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D539F49E-5374-4272-BFBB-F198068F1665}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/README.md b/README.md
index 97c7d61..a5d2acc 100644
--- a/README.md
+++ b/README.md
@@ -4,12 +4,12 @@
A fast, cross-platform engine for structural analysis & design.
[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/noblesource/gazelle)
- [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](https://github.com/noblesource/gazelle/blob/main/CODE_OF_CONDUCT.md)
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](https://github.com/gazellekit/gazelle/blob/main/CODE_OF_CONDUCT.md)
- [![CI](https://github.com/noblesource/gazelle/actions/workflows/ci.yml/badge.svg)](https://github.com/noblesource/gazelle/actions/workflows/build.yml)
- [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-blueviolet)](https://choosealicense.com/licenses/agpl-3.0/)
+ [![CI](https://github.com/gazellekit/gazelle/actions/workflows/ci.yml/badge.svg)](https://github.com/gazellekit/gazelle/actions/workflows/ci.yml)
+ [![License: AGPL-3.0](https://img.shields.io/badge/License-AGPL--3.0-00add8)](https://choosealicense.com/licenses/agpl-3.0/)
- [![Go](https://img.shields.io/badge/Go-1.22-00add8)](https://go.dev/)
+ [![.NET](https://img.shields.io/badge/.NET-8.0-8a2be2)](https://dotnet.microsoft.com)
## Table of Contents
@@ -77,7 +77,7 @@
- Develop bespoke plugins so that other software can benefit from Gazelle,
- Organise local meetups to discuss how Gazelle could enhance your existing workflows,
- Plan conferences and community engagement events to broaden Gazelle's adoption,
-- For Go developers, consider importing Gazelle as a package into your own projects.
+- For .NET developers, consider importing Gazelle as a library into your own projects.
## Errata
diff --git a/Setup.ps1 b/Setup.ps1
new file mode 100644
index 0000000..0709640
--- /dev/null
+++ b/Setup.ps1
@@ -0,0 +1,13 @@
+Set-Location $PSScriptRoot
+
+$DirectoryCommands = @(
+ @{ RelativePath = "."; Command = "sudo dotnet workload update" }
+ @{ RelativePath = "."; Command = "dotnet tool restore" }
+ @{ RelativePath = "."; Command = "dotnet restore" }
+)
+
+$DirectoryCommands | ForEach-Object {
+ Push-Location $_.RelativePath
+ Invoke-Expression $_.Command
+ Pop-Location
+}
\ No newline at end of file
diff --git a/dotnet-tools.json b/dotnet-tools.json
new file mode 100644
index 0000000..56449ee
--- /dev/null
+++ b/dotnet-tools.json
@@ -0,0 +1,18 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "fantomas": {
+ "version": "6.3.4",
+ "commands": [
+ "fantomas"
+ ]
+ },
+ "powershell": {
+ "version": "7.4.2",
+ "commands": [
+ "pwsh"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/bending-moment.ipynb b/examples/bending-moment.ipynb
new file mode 100644
index 0000000..c38d63e
--- /dev/null
+++ b/examples/bending-moment.ipynb
@@ -0,0 +1,93 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "metadata": {
+ "dotnet_interactive": {
+ "language": "fsharp"
+ },
+ "polyglot_notebook": {
+ "kernelName": "fsharp"
+ },
+ "vscode": {
+ "languageId": "polyglot-notebook"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "z_bal: 123.00 mm\n"
+ ]
+ }
+ ],
+ "source": [
+ "open FSharp.Data.UnitSystems.SI.UnitSymbols\n",
+ "\n",
+ "[]\n",
+ "type mm\n",
+ "\n",
+ "// 1. Define partial safety factors.\n",
+ "let γ_c = 1.5\n",
+ "let γ_s = 1.15\n",
+ "\n",
+ "// 2. Factors to account for long-term effects on concrete.\n",
+ "let α_cc = 0.85\n",
+ "let α_ct = 1.0\n",
+ "\n",
+ "// 3. Selected concrete strength.\n",
+ "let fck = 25.0\n",
+ "\n",
+ "// 4. Ultimate concrete design stress.\n",
+ "let fcd = α_cc * fck / 1.5\n",
+ "\n",
+ "// 5. Concrete strain limits.\n",
+ "let εc2 = 0.002\n",
+ "let εcu2 = 0.0035\n",
+ "\n",
+ "// 6. Rebar steel strength.\n",
+ "let fyk = 500.0\n",
+ "\n",
+ "// 7. Rebar steel modulus.\n",
+ "let Es = 200_000.0\n",
+ "\n",
+ "// 8. Rebar design yield strain.\n",
+ "let εy = fyk / (γ_s * Es)\n",
+ "\n",
+ "// 9. Equation to calculate neutral axis, x.\n",
+ "let x εst εcu2 (d: float) = d / (1.0 + (εst / εcu2))\n",
+ "let x_partial = x 0.00217 0.0035\n",
+ "\n",
+ "// 10. Stress block reduced to 0.8x depth.\n",
+ "let s d = (x_partial d) * 0.8\n",
+ "\n",
+ "// 11. Calculate concrete compressive force.\n",
+ "let Fcc d (b: float) = fcd * (s d) * b\n",
+ "\n",
+ "// 12. Calculate moment.\n",
+ "let z d = d - (s d / 2.0)\n",
+ "let M d b = (Fcc d b) * (z d)\n",
+ "\n",
+ "// 13. Calculate area of steel.\n",
+ "let As d b = (M d b) / (0.87 * fyk * (z d))\n",
+ "\n",
+ "M 440.0 260.0 / 10E6\n",
+ "As 440.0 260.0\n",
+ "\n",
+ "let x_limit = 0.45\n",
+ "let z_bal (d: float) = d - (0.8 * x_limit * d) / 2.0\n",
+ "\n",
+ "printfn $\"z_bal: %.2f{z_bal 150.0} mm\""
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "csharp"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..ccbaf06
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "8.0.203",
+ "rollForward": "latestMinor"
+ }
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
deleted file mode 100644
index e6646d2..0000000
--- a/go.mod
+++ /dev/null
@@ -1,3 +0,0 @@
-module github.com/GazelleKit/gazelle
-
-go 1.22.0
diff --git a/go.sum b/go.sum
deleted file mode 100644
index e69de29..0000000
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..0e585fb
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pkg/bluebook/doc.go b/pkg/bluebook/doc.go
deleted file mode 100644
index 60b14f7..0000000
--- a/pkg/bluebook/doc.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// As Structural Engineers adopt digital workflows, the availability
-// of data in consumable formats becomes increasingly important.
-// Structural design data is often captured within Excel spreadsheets,
-// books, tables and figures, which is inconvenient from a software
-// perspective. Moreover, mapping data from one format to another can
-// be time-consuming and a frequent source of error. The bluebook
-// package encapsulates the Steel for Life steel section data as a
-// library with a stable and convenient API. The original .xlsx data
-// files were sourced from: https://www.steelforlifebluebook.co.uk.
-package bluebook
diff --git a/pkg/bluebook/universalsection.go b/pkg/bluebook/universalsection.go
deleted file mode 100644
index 8273e9f..0000000
--- a/pkg/bluebook/universalsection.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package bluebook
-
-import (
- units "github.com/GazelleKit/gazelle/pkg/units"
-)
-
-type DimensionsAndProperties struct {
- Depth units.Millimetre
- Width units.Millimetre
- Length units.Millimetre
- TotalMass units.Kilogram
- MassPerMetreLength float64
- CrossSectionalArea float64
- SurfaceAreaPerMetre float64
- SurfaceAreaPerTonne float64
- WebThickness float64
- FlangeThickness float64
- RootRadius float64
- DepthBetweenFillets float64
- EndClearanceForDetailing float64
- LongitudinalNotchDimension float64
- VerticalNotchDimension float64
- RadiusOfGyration float64
-}
diff --git a/pkg/geometry/properties.go b/pkg/geometry/properties.go
deleted file mode 100644
index f8a98f3..0000000
--- a/pkg/geometry/properties.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package geometry
-
-type FlexuralAxis int
-
-const (
- XX FlexuralAxis = iota
- YY
-)
-
-type LinearAxis int
-
-const (
- X LinearAxis = iota
- Y
-)
diff --git a/pkg/steel/sections.go b/pkg/steel/sections.go
deleted file mode 100644
index caa4a2a..0000000
--- a/pkg/steel/sections.go
+++ /dev/null
@@ -1,35 +0,0 @@
-package steel
-
-import (
- bluebook "github.com/GazelleKit/gazelle/pkg/bluebook"
-)
-
-type UniversalBeam struct {
- SectionClassification string
- IsNonStandard bool
- DimensionsAndProperties bluebook.DimensionsAndProperties
-}
-
-type UniversalColumn struct {
- SectionClassification string
- IsNonStandard bool
- DimensionsAndProperties bluebook.DimensionsAndProperties
-}
-
-type UniversalBearingPile struct {
- SectionClassification string
- IsNonStandard bool
- DimensionsAndProperties bluebook.DimensionsAndProperties
-}
-
-func (UniversalBeam) String() string {
- return "Universal Beam"
-}
-
-func (UniversalColumn) String() string {
- return "Universal Column"
-}
-
-func (UniversalBearingPile) String() string {
- return "Universal Bearing Pile"
-}
diff --git a/pkg/units/units.go b/pkg/units/units.go
deleted file mode 100644
index 029f231..0000000
--- a/pkg/units/units.go
+++ /dev/null
@@ -1,93 +0,0 @@
-package units
-
-import (
- "fmt"
-)
-
-type Gram float64
-type Kilogram float64
-type Tonne float64
-type Metre float64
-type Millimetre float64
-type Newton float64
-type Kilonewton float64
-type Area float64
-type Perimeter float64
-type SurfaceArea float64
-type Volume float64
-
-type SecondMomentOfArea struct {
- XX, YY float64
-}
-
-type RadiusOfGyration struct {
- XX, YY float64
-}
-
-func (g Gram) ToKilogram() Kilogram {
- return Kilogram(g / 1000.0)
-}
-
-func (g Gram) ToTonne() Tonne {
- return Tonne(g / 1_000_000.0)
-}
-
-func (g Gram) String() string {
- return fmt.Sprintf("%f Gram(s)", g)
-}
-
-func (kg Kilogram) ToGram() Gram {
- return Gram(kg * 1000.0)
-}
-
-func (kg Kilogram) ToTonne() Tonne {
- return Tonne(kg / 1000.0)
-}
-
-func (kg Kilogram) String() string {
- return fmt.Sprintf("%f Kilogram(s)", kg)
-}
-
-func (t Tonne) ToGram() Gram {
- return Gram(t * 1_000_000.0)
-}
-
-func (t Tonne) ToKilogram() Kilogram {
- return Kilogram(t * 1000.0)
-}
-
-func (t Tonne) String() string {
- return fmt.Sprintf("%f Tonne(s)", t)
-}
-
-func (m Metre) ToMillimetre() Millimetre {
- return Millimetre(m * 1000.0)
-}
-
-func (m Metre) String() string {
- return fmt.Sprintf("%f Metre(s)", m)
-}
-
-func (mm Millimetre) ToMetre() Metre {
- return Metre(mm / 1000.0)
-}
-
-func (m Millimetre) String() string {
- return fmt.Sprintf("%f Millimetre(s)", m)
-}
-
-func (N Newton) ToKilonewton() Kilonewton {
- return Kilonewton(N / 1000.0)
-}
-
-func (N Newton) String() string {
- return fmt.Sprintf("%f Newton(s)", N)
-}
-
-func (kN Kilonewton) ToNewton() Newton {
- return Newton(kN * 1000.0)
-}
-
-func (kN Kilonewton) String() string {
- return fmt.Sprintf("%f Kilonewton(s)", kN)
-}
diff --git a/pkg/bluebook/.d/properties/cf-chs.json b/src/.d/bluebook/properties/cf-chs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/cf-chs.json
rename to src/.d/bluebook/properties/cf-chs.json
diff --git a/pkg/bluebook/.d/properties/cf-rhs.json b/src/.d/bluebook/properties/cf-rhs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/cf-rhs.json
rename to src/.d/bluebook/properties/cf-rhs.json
diff --git a/pkg/bluebook/.d/properties/cf-shs.json b/src/.d/bluebook/properties/cf-shs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/cf-shs.json
rename to src/.d/bluebook/properties/cf-shs.json
diff --git a/pkg/bluebook/.d/properties/equal-l.json b/src/.d/bluebook/properties/equal-l.json
similarity index 100%
rename from pkg/bluebook/.d/properties/equal-l.json
rename to src/.d/bluebook/properties/equal-l.json
diff --git a/pkg/bluebook/.d/properties/hf-chs.json b/src/.d/bluebook/properties/hf-chs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/hf-chs.json
rename to src/.d/bluebook/properties/hf-chs.json
diff --git a/pkg/bluebook/.d/properties/hf-ehs.json b/src/.d/bluebook/properties/hf-ehs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/hf-ehs.json
rename to src/.d/bluebook/properties/hf-ehs.json
diff --git a/pkg/bluebook/.d/properties/hf-rhs.json b/src/.d/bluebook/properties/hf-rhs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/hf-rhs.json
rename to src/.d/bluebook/properties/hf-rhs.json
diff --git a/pkg/bluebook/.d/properties/hf-shs.json b/src/.d/bluebook/properties/hf-shs.json
similarity index 100%
rename from pkg/bluebook/.d/properties/hf-shs.json
rename to src/.d/bluebook/properties/hf-shs.json
diff --git a/pkg/bluebook/.d/properties/pfc.json b/src/.d/bluebook/properties/pfc.json
similarity index 100%
rename from pkg/bluebook/.d/properties/pfc.json
rename to src/.d/bluebook/properties/pfc.json
diff --git a/pkg/bluebook/.d/properties/t-split-from-ub.json b/src/.d/bluebook/properties/t-split-from-ub.json
similarity index 100%
rename from pkg/bluebook/.d/properties/t-split-from-ub.json
rename to src/.d/bluebook/properties/t-split-from-ub.json
diff --git a/pkg/bluebook/.d/properties/t-split-from-uc.json b/src/.d/bluebook/properties/t-split-from-uc.json
similarity index 100%
rename from pkg/bluebook/.d/properties/t-split-from-uc.json
rename to src/.d/bluebook/properties/t-split-from-uc.json
diff --git a/pkg/bluebook/.d/properties/ub.json b/src/.d/bluebook/properties/ub.json
similarity index 100%
rename from pkg/bluebook/.d/properties/ub.json
rename to src/.d/bluebook/properties/ub.json
diff --git a/pkg/bluebook/.d/properties/ubp.json b/src/.d/bluebook/properties/ubp.json
similarity index 100%
rename from pkg/bluebook/.d/properties/ubp.json
rename to src/.d/bluebook/properties/ubp.json
diff --git a/pkg/bluebook/.d/properties/uc.json b/src/.d/bluebook/properties/uc.json
similarity index 100%
rename from pkg/bluebook/.d/properties/uc.json
rename to src/.d/bluebook/properties/uc.json
diff --git a/pkg/bluebook/.d/properties/unequal-l.json b/src/.d/bluebook/properties/unequal-l.json
similarity index 100%
rename from pkg/bluebook/.d/properties/unequal-l.json
rename to src/.d/bluebook/properties/unequal-l.json
diff --git a/src/Gazelle.fsproj b/src/Gazelle.fsproj
new file mode 100644
index 0000000..5b5fc27
--- /dev/null
+++ b/src/Gazelle.fsproj
@@ -0,0 +1,30 @@
+
+
+
+ James S. Bayley
+ GazelleKit
+ net8.0
+ https://github.com/gazellekit/gazelle
+ true
+ true
+ AGPL-3.0-or-later
+ A fast, cross-platform engine for structural analysis and design.
+ README.md
+ Gazelle
+ true
+ 0.0.1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Geometry.fs b/src/Geometry.fs
new file mode 100644
index 0000000..4622b90
--- /dev/null
+++ b/src/Geometry.fs
@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Geometry
+
+///
+/// Orthogonal geometric axis.
+///
+type Axis =
+ | X
+ | Y
+
+///
+/// Orthogonal axis about which rotation occurs.
+///
+type RotationalAxis =
+ | XX
+ | YY
+
+///
+/// Length dimension annotated with unit-of-measure.
+///
+type Length<[] 'TUnit> = Length of float<'TUnit>
+
+///
+/// Width dimension annotated with unit-of-measure.
+///
+type Width<[] 'TUnit> = Width of float<'TUnit>
+
+///
+/// Depth dimension annotated with unit-of-measure.
+///
+type Depth<[] 'TUnit> = Depth of float<'TUnit>
+
+///
+/// Diameter dimension annotated with unit-of-measure.
+///
+type Diameter<[] 'TUnit> = Diameter of float<'TUnit>
+
+///
+/// Perimeter dimension annotated with unit-of-measure.
+///
+type Perimeter<[] 'TUnit> = Perimeter of float<'TUnit>
+
+///
+/// Area dimension annotated with unit-of-measure.
+/// Union type represents possible area semantics.
+///
+type Area<[] 'TUnit> =
+ | CrossSectionalArea of float<'TUnit^2>
+ | SurfaceArea of float<'TUnit^2>
+
+///
+/// Volume dimension annotated with unit-of-measure.
+///
+type Volume<[] 'TUnit> = Volume of float<'TUnit^3>
+
+///
+/// Second moment of area dimension annotated with unit-of-measure.
+///
+type SecondMomentOfArea<[] 'TUnit> = SecondMomentOfArea of float<'TUnit^4>
diff --git a/src/README.md b/src/README.md
new file mode 100644
index 0000000..73a4204
--- /dev/null
+++ b/src/README.md
@@ -0,0 +1,3 @@
+# Gazelle
+
+A fast, cross-platform engine for structural analysis & design.
\ No newline at end of file
diff --git a/src/concrete/Cages.fs b/src/concrete/Cages.fs
new file mode 100644
index 0000000..65efee3
--- /dev/null
+++ b/src/concrete/Cages.fs
@@ -0,0 +1,496 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+//open System
+
+///// 3D rebar cage dimensions measured between centrelines of outermost reinforcement.
+//type CageDimensions<[] 'T> =
+// | RectangularCage of Width<'T> * Depth<'T> * Length<'T>
+
+// member this.Centreline =
+// match this with
+// | RectangularCage (_, _, Length l) ->
+// (Point3D (0.0, 0.0, 0.0),
+// Point3D (0.0, 0.0, stripUnitsFromFloat l))
+// ||> createCentreline
+
+// member this.Length =
+// match this with
+// | RectangularCage (_, _, l) -> l
+
+
+//[]
+//module ColumnCage =
+
+// /// Rectangular column cage face.
+// type Face = Top | Left | Right | Bottom
+
+// /// Rectangular column cage corners.
+// type Corner = TopRight | TopLeft | BottomRight | BottomLeft
+
+// /// Straight reinforcing bars positioned at cage corners.
+// type CornerBars =
+// { TopRight: StraightBar
+// TopLeft: StraightBar
+// BottomRight: StraightBar
+// BottomLeft: StraightBar }
+
+// /// Collection of straight reinforcing bars for respective cage faces.
+// type SideBars =
+// { TopFace: StraightBar list
+// LeftFace: StraightBar list
+// RightFace: StraightBar list
+// BottomFace: StraightBar list }
+
+// /// Defines rectangular column cage rebar configuration.
+// type RectangularCageSpecification =
+// { CornerBarSize: BarSize
+// TopFaceBars: NumberOfBarsAndBarSize
+// BottomFaceBars: NumberOfBarsAndBarSize
+// LeftFaceBars: NumberOfBarsAndBarSize
+// RightFaceBars: NumberOfBarsAndBarSize
+// Links: LinkSpacingAndLinkSize }
+
+// /// Rebar specifications for alternative column cage geometries.
+// type Specification =
+// | RectangularCageSpec of RectangularCageSpecification
+
+// /// Fully configured column cage.
+// type Cage =
+// { Dimensions: CageDimensions
+// CornerBars: CornerBars
+// SideBars: SideBars
+// Links: Link list }
+
+
+// []
+// module Links =
+
+// /// Calculates no. of links within given length at the prescribed spacing.
+// let getNumberOfLinks (BarSpacing sp) (Length l) =
+// Math.Ceiling (l / sp)
+// |> int
+// |> (+) 1
+// |> AddUnitsToInteger32
+// |> NumberOfBars
+
+// /// Calculates link spacing for a discrete no. of links over a given length.
+// let getLinkSpacing (NumberOfBars n) (Length l) =
+// l / (float n - 1.0)
+// |> BarSpacing
+
+// /// For a given spacing, returns all link centroids along a centreline.
+// let getLinkCentroidsAlongCentreline (Centreline l) (BarSpacing spacing) =
+// let spacing = stripUnitsFromFloat spacing
+// let p1, p2 = l.StartPoint, l.EndPoint
+// [ for z in p1.Z .. spacing .. p2.Z ->
+// Point3D (p1.X, p1.Y, z)
+// |> Centroid ]
+
+// /// Returns shear links for full column cage length.
+// let create steel linkSize spacing (cageDims: CageDimensions) =
+// let cl = cageDims.Centreline
+// let length = cageDims.Length
+// let quantity = getNumberOfLinks spacing length
+// let updatedSpacing = getLinkSpacing quantity length
+// let centroids = getLinkCentroidsAlongCentreline cl updatedSpacing
+// [for ct in centroids ->
+// match cageDims with
+// | RectangularCage (Width w, Depth d, _) ->
+// RectangularLink.create
+// ct
+// (Width w)
+// (Depth d)
+// linkSize
+// steel]
+
+
+// []
+// module CornerBars =
+
+// /// Locates the geometric bar centroid, for a selected corner and given bar size.
+// let findCorner link (barSize: BarSize) corner =
+// let b = link.Geometry.InnerWidth / 2.0
+// let d = link.Geometry.InnerDepth / 2.0
+// let ctX, ctY = link.Geometry.CentroidCoords
+// let Φ = barSize.diameter / 2.0
+
+// match corner with
+// | TopRight ->
+// let x = ctX + b - Φ
+// let y = ctY + d - Φ
+// x, y
+// | TopLeft ->
+// let x = ctX - b + Φ
+// let y = ctY + d - Φ
+// x, y
+// | BottomRight ->
+// let x = ctX + b - Φ
+// let y = ctY - d + Φ
+// x, y
+// | BottomLeft ->
+// let x = ctX - b + Φ
+// let y = ctY - d + Φ
+// x, y
+
+// /// Returns collection of straight steel bars for rectangular cage corners.
+// let create barSize steel link (Length len) =
+// let createAt corner =
+// corner
+// |> findCorner link barSize
+// |> createSymmetricEndPoints len
+// ||> StraightBar.create barSize steel
+
+// { TopRight = createAt TopRight
+// TopLeft = createAt TopLeft
+// BottomRight = createAt BottomRight
+// BottomLeft = createAt BottomLeft }
+
+
+// []
+// module SideBars =
+
+// /// Returns the spacing interval for intermediate side bars.
+// let getSideBarSpacing cornerBars side numberOfBars =
+// match side with
+// | Top | Bottom ->
+// distanceBetweenPoints
+// cornerBars.TopRight.Geometry.StartPoint
+// cornerBars.TopLeft.Geometry.StartPoint
+// | Left | Right ->
+// distanceBetweenPoints
+// cornerBars.TopLeft.Geometry.StartPoint
+// cornerBars.BottomLeft.Geometry.StartPoint
+// |> AddUnitsToFloat
+// |> Length
+// |> getIntermediateBarSpacing numberOfBars
+
+// /// Returns the 'constant' x or y coordinate for a given side.
+// let getSideBarConstantCoord (barSize: BarSize) side (link: Link) =
+// let Φ = barSize.diameter |> stripUnitsFromFloat
+// let ctX, ctY, w, d, sd =
+// match link.Geometry with
+// | RectangularHoop (Centroid ct, Width w, Depth d, SectionDiameter sd) ->
+// ct.X, ct.Y, w, d, sd
+// | CircularHoop (Centroid ct, Diameter d, SectionDiameter sd) ->
+// ct.X, ct.Y, d, d, sd
+// match side with
+// | Top -> ctY + (d / 2.0<_>) - (sd / 2.0<_>) - (Φ / 2.0)
+// | Bottom -> ctY - (d / 2.0<_>) + (sd / 2.0<_>) + (Φ / 2.0)
+// | Left -> ctX - (w / 2.0<_>) + (sd / 2.0<_>) + (Φ / 2.0)
+// | Right -> ctX + (w / 2.0<_>) - (sd / 2.0<_>) - (Φ / 2.0)
+
+// /// Locates the side bar centroids.
+// let getSideBar2DCoords side numberOfBars cornerBars link barSize =
+// let spacing =
+// getSideBarSpacing cornerBars side numberOfBars
+// |> unwrapBarSpacing
+// |> stripUnitsFromFloat
+// let constantCoord = getSideBarConstantCoord barSize side link
+// match side with
+// | Top | Bottom ->
+// let xStart = cornerBars.TopLeft.Geometry.StartPoint.X + spacing
+// let xEnd = cornerBars.TopRight.Geometry.StartPoint.X - spacing
+// [for x in xStart .. spacing .. xEnd -> (x, constantCoord)]
+// | Left | Right ->
+// let yStart = cornerBars.BottomLeft.Geometry.StartPoint.Y + spacing
+// let yEnd = cornerBars.TopLeft.Geometry.StartPoint.Y - spacing
+// [for y in yStart .. spacing .. yEnd -> (constantCoord, y)]
+
+// /// Returns list of straight steel bars for a single cage side.
+// let create side cornerBars link barSize numberOfBars steel =
+// let xyCoords = getSideBar2DCoords side numberOfBars cornerBars link barSize
+// let zStart = cornerBars.TopRight.Geometry.StartPoint.Z
+// let zEnd = cornerBars.TopRight.Geometry.EndPoint.Z
+// [ for x, y in xyCoords ->
+// StraightBar.create
+// barSize
+// steel
+// (Point3D (x, y, zStart))
+// (Point3D (x, y, zEnd)) ]
+
+
+// /// Returns a fully configured column rebar cage.
+// let create (rebarSpec: Specification) steel (dimensions: CageDimensions) =
+// let length = match dimensions with RectangularCage (_, _, l) -> l
+
+// match rebarSpec with
+// | RectangularCageSpec spec ->
+
+// let links =
+// let linkSize = spec.Links.LinkSize
+// let linkSpacing = spec.Links.LinkSpacing
+// Links.create steel linkSize linkSpacing dimensions
+
+// let cornerBars =
+// let barSize = spec.CornerBarSize
+// CornerBars.create barSize steel links.[0] length
+
+// let sideBars =
+
+// let topFaceBars =
+// let barSize = spec.TopFaceBars.BarSize
+// let numOfBars = spec.TopFaceBars.NumberOfBars
+// SideBars.create Top cornerBars links.[0] barSize numOfBars steel
+
+// let leftFaceBars =
+// let barSize = spec.LeftFaceBars.BarSize
+// let numOfBars = spec.LeftFaceBars.NumberOfBars
+// SideBars.create Left cornerBars links.[0] barSize numOfBars steel
+
+// let rightFaceBars =
+// let barSize = spec.RightFaceBars.BarSize
+// let numOfBars = spec.RightFaceBars.NumberOfBars
+// SideBars.create Right cornerBars links.[0] barSize numOfBars steel
+
+// let bottomFaceBars =
+// let barSize = spec.BottomFaceBars.BarSize
+// let numOfBars = spec.BottomFaceBars.NumberOfBars
+// SideBars.create Bottom cornerBars links.[0] barSize numOfBars steel
+
+// { TopFace = topFaceBars
+// LeftFace = leftFaceBars
+// RightFace = rightFaceBars
+// BottomFace = bottomFaceBars }
+
+// { Dimensions = dimensions
+// CornerBars = cornerBars
+// SideBars = sideBars
+// Links = links }
+
+
+//[]
+//module WallCage =
+
+// /// Respective wall face.
+// type Face = LeftFace | RightFace
+
+// /// Respective wall end zone.
+// type EndZone = Top | Bottom
+
+// /// Number of pairwise bar layers in each end zone.
+// type NumberOfBarLayersInEndZone = NumberOfBarLayersInEndZone of int
+
+// /// Defines bar configuration for wall end zones.
+// type EndZoneBarSpecification =
+// { NumberOfLayers: NumberOfBarLayersInEndZone
+// BarSize: BarSize }
+
+// /// Defines bar configuration for wall central zone.
+// type CentralZoneBarSpecification = { BarSize: BarSize; Spacing: BarSpacing }
+
+// /// Defines bar configuration for horizontal wall bars.
+// type HorizontalBarSpecification = { BarSize: BarSize; Spacing: BarSpacing }
+
+// /// Defines wall cage rebar configurations for all zones.
+// type Specification =
+// { HorizontalBarSpec: HorizontalBarSpecification
+// EndZoneBarSpec: EndZoneBarSpecification
+// CentralZoneBarSpec: CentralZoneBarSpecification }
+
+// /// Fully configured wall cage.
+// type Cage =
+// { Dimensions: CageDimensions
+// HorizontalBars: StraightBar list
+// EndZoneBars: StraightBar list
+// CentralZoneBars: StraightBar list }
+
+
+// []
+// module HorizontalBars =
+
+// /// Returns x- and y-coordinates for horizontal wall bars.
+// let findHorizontalBarXYCoords cageDims =
+// let (Width w), (Depth d), _ = cageDims
+// let xLeft = -w / 2.0
+// let xRight = w / 2.0
+// let yTop = d / 2.0
+// let yBottom = -d / 2.0
+// xLeft, xRight, yTop, yBottom
+
+// /// Returns z-coordinates for horizontal wall bars.
+// let findHorizontalBarZCoords cageDims horizBarSpacing =
+// let _, _, l = cageDims
+// let numOfBars = getNumberOfIntermediateBars horizBarSpacing l
+// let updatedSpacing =
+// getIntermediateBarSpacing numOfBars l
+// |> unwrapBarSpacing
+// |> stripUnitsFromFloat
+// let length =
+// let unwrapLength (Length l) = l
+// l |> unwrapLength |> stripUnitsFromFloat
+// let zStart, zEnd = 0.0, length
+// [for z in zStart .. updatedSpacing .. zEnd -> z]
+
+// /// Groups horizontal wall bar x-, y- and z-coordinates for a given face.
+// let combineHorizontalXYZBarCoords xyCoords zCoords face =
+// let xLeft, xRight, yTop, yBottom = xyCoords
+// zCoords
+// |> match face with
+// | LeftFace ->
+// List.map (fun z ->
+// Point3D (stripUnitsFromFloat xLeft, stripUnitsFromFloat yBottom, z),
+// Point3D (stripUnitsFromFloat xLeft, stripUnitsFromFloat yTop, z))
+// | RightFace ->
+// List.map (fun z ->
+// Point3D (stripUnitsFromFloat xRight, stripUnitsFromFloat yBottom, z),
+// Point3D (stripUnitsFromFloat xRight, stripUnitsFromFloat yTop, z))
+
+// /// Returns all steel horizontal wall bars.
+// let create (cageDims: CageDimensions) horizBarSpacing barSize steel =
+// let cageDims =
+// match cageDims with
+// | RectangularCage (w, d, l) -> (w, d, l)
+
+// let xyCoords = findHorizontalBarXYCoords cageDims
+// let zCoords = findHorizontalBarZCoords cageDims horizBarSpacing
+// let barCoordsLeft = combineHorizontalXYZBarCoords xyCoords zCoords LeftFace
+// let barCoordsRight = combineHorizontalXYZBarCoords xyCoords zCoords RightFace
+// barCoordsLeft @ barCoordsRight
+// |> List.map (fun (p1, p2) ->
+// StraightBar.create barSize steel p1 p2)
+
+// /// Returns clear space between horizontal wall bars.
+// let getClearSpaceBetweenHorizontalBars cageDims (horizontalBar: BarSize) =
+// let (Width cageWidth), _, _ = cageDims
+// cageWidth - horizontalBar.diameter
+// |> ClearSpace
+
+
+// []
+// module VerticalBars =
+
+// /// Returns x-coordinate of vertical wall bar, measured from the centreline of the cage.
+// let getVerticalBarXCoordinate face (verticalBarSize: BarSize) spaceBetweenHorizontalBars =
+// let halfWidth = unwrapClearSpace spaceBetweenHorizontalBars / 2.0
+// match face with
+// | LeftFace -> (- halfWidth) + (verticalBarSize.diameter / 2.0)
+// | RightFace -> halfWidth - (verticalBarSize.diameter / 2.0)
+
+
+// []
+// module EndZone =
+
+// /// Returns x- and y-coordinates for end zone bars, for a single end zone and wall face.
+// let findEndZoneBarCoords zone face barSize (OuterDepth endZoneDepth) cageDims horizBarSize =
+// let _, (Depth cageDepth), _ = cageDims
+// let ezDepth = endZoneDepth
+// let ezWidth = HorizontalBars.getClearSpaceBetweenHorizontalBars cageDims horizBarSize
+// let xCoord = getVerticalBarXCoordinate face barSize ezWidth
+// let Φ = barSize.diameter
+// let spacingBetweenBarCentres =
+// 2.0 * unwrapClearSpace (getMinimumClearBarSpacing barSize)
+// match zone with
+// | Top ->
+// let yStart = (cageDepth - Φ) / 2.0
+// let yEnd = yStart - ezDepth + (Φ / 2.0)
+// [for y in yStart .. -spacingBetweenBarCentres .. yEnd -> (xCoord, y)]
+// | Bottom ->
+// let yStart = (Φ - cageDepth) / 2.0
+// let yEnd = yStart + ezDepth - (Φ / 2.0)
+// [for y in yStart .. spacingBetweenBarCentres .. yEnd -> (xCoord, y)]
+
+// /// Returns depth of single end zone, including clear space between bars.
+// /// Measured to the outermost bar dimensions.
+// let getEndZoneDepth (NumberOfBarLayersInEndZone layers) endZoneBarSize =
+// let numOfLayers = layers |> float |> AddUnitsToFloat
+// let clearBarSpace =
+// getMinimumClearBarSpacing endZoneBarSize
+// |> unwrapClearSpace
+// |> (*) (numOfLayers - 1.0)
+// endZoneBarSize.diameter
+// |> (*) numOfLayers
+// |> (+) clearBarSpace
+// |> OuterDepth
+
+// /// Returns list of straight steel bars for both end zones and wall faces.
+// let create barSize endZoneDepth cageDims horizBarSize steel =
+// let _, _, (Length cageLength) = cageDims
+// [ Top, LeftFace
+// Top, RightFace
+// Bottom, LeftFace
+// Bottom, RightFace ]
+// |> List.collect (fun (zone, face) ->
+// findEndZoneBarCoords zone face barSize endZoneDepth cageDims horizBarSize)
+// |> List.map (fun (x, y) -> stripUnitsFromFloat x, stripUnitsFromFloat y)
+// |> List.map (fun (x, y) ->
+// StraightBar.create
+// barSize
+// steel
+// (Point3D (x, y, 0.0))
+// (Point3D (x, y, stripUnitsFromFloat cageLength)))
+
+
+// []
+// module CentralZone =
+
+// /// Returns x- and y-coordinates for central zone bars, for a single end zone and wall face.
+// let findCentralZoneBarCoords face barSize spacing (OuterDepth centralZoneDepth) cageDims horizBarSize =
+// let czWidth = HorizontalBars.getClearSpaceBetweenHorizontalBars cageDims horizBarSize
+// let czDepth = centralZoneDepth
+// let xCoord = getVerticalBarXCoordinate face barSize czWidth
+// let Φ = barSize.diameter
+// let yStart = (-czDepth / 2.0) + (Φ / 2.0)
+// let yEnd = (czDepth / 2.0) - (Φ / 2.0)
+// let length = (yEnd - yStart) |> Length
+// let numberOfBars = getNumberOfIntermediateBars spacing length
+// let updatedSpacing = getIntermediateBarSpacing numberOfBars length |> unwrapBarSpacing
+// [ for y in yStart .. updatedSpacing .. yEnd -> xCoord, y ]
+
+// /// Returns depth of central wall zone. Calculated using the outermost depth
+// /// of both end zones; adds additional clear spacing between the end zone edges
+// /// and the outermost bars in the central zone.
+// let getCentralZoneDepth cageDims endZoneOuterDepth endZoneClearBarSpacing =
+// let _, (Depth cageDepth), _ = cageDims
+// let depthOfBothEndZones =
+// match endZoneOuterDepth with OuterDepth d -> d
+// |> (*) 2.0
+// let clearSpaceForBothEndZones =
+// endZoneClearBarSpacing
+// |> unwrapClearSpace
+// |> (*) 2.0
+// (cageDepth - depthOfBothEndZones - clearSpaceForBothEndZones)
+// |> OuterDepth
+
+// /// Returns list of straight steel bars for both faces of central zone.
+// let create barSize spacing centralZoneDepth horizBarSize cageDims steel =
+// let _, _, (Length cageLength) = cageDims
+// [ LeftFace; RightFace ]
+// |> List.collect (fun face ->
+// findCentralZoneBarCoords face barSize spacing centralZoneDepth cageDims horizBarSize)
+// |> List.map (fun (x, y) -> stripUnitsFromFloat x, stripUnitsFromFloat y)
+// |> List.map (fun (x, y) ->
+// StraightBar.create
+// barSize
+// steel
+// (Point3D (x, y, 0.0))
+// (Point3D (x, y, stripUnitsFromFloat cageLength)))
+
+
+// /// Returns fully configured wall cage.
+// let create (spec: Specification) steel (dimensions: CageDimensions) =
+// let dimsAsTuple =
+// match dimensions with
+// | RectangularCage (w, d, l) -> (w, d, l)
+
+// let horizBarSize = spec.HorizontalBarSpec.BarSize
+// let ezLayers = spec.EndZoneBarSpec.NumberOfLayers
+// let ezBarSize = spec.EndZoneBarSpec.BarSize
+// let ezClearBarSpacing = getMinimumClearBarSpacing ezBarSize
+// let ezDepth = VerticalBars.EndZone.getEndZoneDepth ezLayers ezBarSize
+// let czBarSize = spec.CentralZoneBarSpec.BarSize
+// let czDepth = VerticalBars.CentralZone.getCentralZoneDepth dimsAsTuple ezDepth ezClearBarSpacing
+// let czSpacing = spec.CentralZoneBarSpec.Spacing
+// let horizBarSpacing = spec.HorizontalBarSpec.Spacing
+
+// let ezBars = VerticalBars.EndZone.create ezBarSize ezDepth dimsAsTuple horizBarSize steel
+// let czBars = VerticalBars.CentralZone.create czBarSize czSpacing czDepth horizBarSize dimsAsTuple steel
+// let horizBars = HorizontalBars.create dimensions horizBarSpacing horizBarSize steel
+
+// { Dimensions = dimensions
+// HorizontalBars = horizBars
+// EndZoneBars = ezBars
+// CentralZoneBars = czBars }
diff --git a/src/concrete/Concrete.fs b/src/concrete/Concrete.fs
new file mode 100644
index 0000000..43052ee
--- /dev/null
+++ b/src/concrete/Concrete.fs
@@ -0,0 +1,322 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Structures
+
+open Gazelle.Units
+open FSharp.Data.UnitSystems.SI.UnitSymbols
+
+type UKConcreteGrade =
+ | Fck12
+ | Fck16
+ | Fck20
+ | Fck25
+ | Fck30
+ | Fck35
+ | Fck40
+ | Fck45
+ | Fck50
+ | Fck55
+ | Fck60
+ | Fck70
+ | Fck80
+ | Fck90
+
+type CylinderStrength = UK of UKConcreteGrade
+
+type Aggregate =
+ | Basalt
+ | Limestone
+ | Sandstone
+ | Quartzite
+
+type Cement =
+ | ClassR
+ | ClassN
+ | ClassS
+
+type WeightClass = NormalWeight
+type Age = Age of int
+
+type Concrete =
+ { Age: Age
+ Aggregate: Aggregate
+ Cement: Cement
+ Grade: CylinderStrength
+ WeightClass: WeightClass }
+
+[]
+module Concrete =
+
+ let private validateAge (age: int) =
+ match age with
+ | age when age <= 0 -> invalidArg $"{nameof (age)}" "Concrete age <= 0 days."
+ | age when age <= 3 -> invalidArg $"{nameof (age)}" "In-situ strength tests required for age <= 3 days."
+ | _ -> Age age
+
+ let tryCreate (fck: CylinderStrength) (agg: Aggregate) (cem: Cement) (w: WeightClass) (age: int) =
+ let validAge = validateAge age
+
+ { Age = validAge
+ Aggregate = agg
+ Cement = cem
+ Grade = fck
+ WeightClass = w }
+
+ []
+ module BasicProperties =
+
+ []
+ module Density =
+
+ let unreinforced (w: WeightClass) =
+ match w with
+ | NormalWeight -> Density 2400.0
+
+ let reinforced (w: WeightClass) =
+ match w with
+ | NormalWeight -> Density 2500.0
+
+ []
+ module Strength =
+
+ let fck (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 12.0
+ | UK Fck16 -> 16.0
+ | UK Fck20 -> 20.0
+ | UK Fck25 -> 25.0
+ | UK Fck30 -> 30.0
+ | UK Fck35 -> 35.0
+ | UK Fck40 -> 40.0
+ | UK Fck45 -> 45.0
+ | UK Fck50 -> 50.0
+ | UK Fck55 -> 55.0
+ | UK Fck60 -> 60.0
+ | UK Fck70 -> 70.0
+ | UK Fck80 -> 80.0
+ | UK Fck90 -> 90.0
+
+ let fcm (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 20.0
+ | UK Fck16 -> 24.0
+ | UK Fck20 -> 28.0
+ | UK Fck25 -> 33.0
+ | UK Fck30 -> 38.0
+ | UK Fck35 -> 43.0
+ | UK Fck40 -> 48.0
+ | UK Fck45 -> 53.0
+ | UK Fck50 -> 58.0
+ | UK Fck55 -> 63.0
+ | UK Fck60 -> 68.0
+ | UK Fck70 -> 78.0
+ | UK Fck80 -> 88.0
+ | UK Fck90 -> 98.0
+
+ let fctm (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 1.6
+ | UK Fck16 -> 1.9
+ | UK Fck20 -> 2.2
+ | UK Fck25 -> 2.6
+ | UK Fck30 -> 2.9
+ | UK Fck35 -> 3.2
+ | UK Fck40 -> 3.5
+ | UK Fck45 -> 3.8
+ | UK Fck50 -> 4.1
+ | UK Fck55 -> 4.2
+ | UK Fck60 -> 4.4
+ | UK Fck70 -> 4.6
+ | UK Fck80 -> 4.8
+ | UK Fck90 -> 5.0
+
+ []
+ module ElasticModulus =
+
+ let Ecm (fck: CylinderStrength) (agg: Aggregate) =
+ let Ecm =
+ match fck with
+ | UK Fck12 -> 27_000.0
+ | UK Fck16 -> 29_000.0
+ | UK Fck20 -> 30_000.0
+ | UK Fck25 -> 31_000.0
+ | UK Fck30 -> 33_000.0
+ | UK Fck35 -> 34_000.0
+ | UK Fck40 -> 35_000.0
+ | UK Fck45 -> 36_000.0
+ | UK Fck50 -> 37_000.0
+ | UK Fck55 -> 38_000.0
+ | UK Fck60 -> 39_000.0
+ | UK Fck70 -> 41_000.0
+ | UK Fck80 -> 42_000.0
+ | UK Fck90 -> 44_000.0
+
+ match agg with
+ | Quartzite -> Ecm * 1.0
+ | Limestone -> Ecm * 0.9
+ | Sandstone -> Ecm * 0.7
+ | Basalt -> Ecm * 1.2
+
+ []
+ module Strain =
+
+ let c1 (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 0.0018
+ | UK Fck16 -> 0.0019
+ | UK Fck20 -> 0.0020
+ | UK Fck25 -> 0.0021
+ | UK Fck30 -> 0.0022
+ | UK Fck35 -> 0.00225
+ | UK Fck40 -> 0.0023
+ | UK Fck45 -> 0.0024
+ | UK Fck50 -> 0.00245
+ | UK Fck55 -> 0.0025
+ | UK Fck60 -> 0.0026
+ | UK Fck70 -> 0.0027
+ | UK Fck80 -> 0.0028
+ | UK Fck90 -> 0.0028
+
+ let cu1 (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 0.0035
+ | UK Fck16 -> 0.0035
+ | UK Fck20 -> 0.0035
+ | UK Fck25 -> 0.0035
+ | UK Fck30 -> 0.0035
+ | UK Fck35 -> 0.0035
+ | UK Fck40 -> 0.0035
+ | UK Fck45 -> 0.0035
+ | UK Fck50 -> 0.0035
+ | UK Fck55 -> 0.0032
+ | UK Fck60 -> 0.0030
+ | UK Fck70 -> 0.0028
+ | UK Fck80 -> 0.0028
+ | UK Fck90 -> 0.0028
+
+ let c2 (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 0.0020
+ | UK Fck16 -> 0.0020
+ | UK Fck20 -> 0.0020
+ | UK Fck25 -> 0.0020
+ | UK Fck30 -> 0.0020
+ | UK Fck35 -> 0.0020
+ | UK Fck40 -> 0.0020
+ | UK Fck45 -> 0.0020
+ | UK Fck50 -> 0.0020
+ | UK Fck55 -> 0.0022
+ | UK Fck60 -> 0.0023
+ | UK Fck70 -> 0.0024
+ | UK Fck80 -> 0.0025
+ | UK Fck90 -> 0.0026
+
+ let cu2 (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 0.0035
+ | UK Fck16 -> 0.0035
+ | UK Fck20 -> 0.0035
+ | UK Fck25 -> 0.0035
+ | UK Fck30 -> 0.0035
+ | UK Fck35 -> 0.0035
+ | UK Fck40 -> 0.0035
+ | UK Fck45 -> 0.0035
+ | UK Fck50 -> 0.0035
+ | UK Fck55 -> 0.0031
+ | UK Fck60 -> 0.0029
+ | UK Fck70 -> 0.0027
+ | UK Fck80 -> 0.0026
+ | UK Fck90 -> 0.0026
+
+ let c3 (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 0.00175
+ | UK Fck16 -> 0.00175
+ | UK Fck20 -> 0.00175
+ | UK Fck25 -> 0.00175
+ | UK Fck30 -> 0.00175
+ | UK Fck35 -> 0.00175
+ | UK Fck40 -> 0.00175
+ | UK Fck45 -> 0.00175
+ | UK Fck50 -> 0.00175
+ | UK Fck55 -> 0.0018
+ | UK Fck60 -> 0.0019
+ | UK Fck70 -> 0.0020
+ | UK Fck80 -> 0.0022
+ | UK Fck90 -> 0.0023
+
+ let cu3 (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 0.0035
+ | UK Fck16 -> 0.0035
+ | UK Fck20 -> 0.0035
+ | UK Fck25 -> 0.0035
+ | UK Fck30 -> 0.0035
+ | UK Fck35 -> 0.0035
+ | UK Fck40 -> 0.0035
+ | UK Fck45 -> 0.0035
+ | UK Fck50 -> 0.0035
+ | UK Fck55 -> 0.0031
+ | UK Fck60 -> 0.0029
+ | UK Fck70 -> 0.0027
+ | UK Fck80 -> 0.0026
+ | UK Fck90 -> 0.0026
+
+ let n (fck: CylinderStrength) =
+ match fck with
+ | UK Fck12 -> 2.0
+ | UK Fck16 -> 2.0
+ | UK Fck20 -> 2.0
+ | UK Fck25 -> 2.0
+ | UK Fck30 -> 2.0
+ | UK Fck35 -> 2.0
+ | UK Fck40 -> 2.0
+ | UK Fck45 -> 2.0
+ | UK Fck50 -> 2.0
+ | UK Fck55 -> 1.75
+ | UK Fck60 -> 1.6
+ | UK Fck70 -> 1.45
+ | UK Fck80 -> 1.4
+ | UK Fck90 -> 1.4
+
+ []
+ module TimeDependentProperties =
+
+ let private s cement =
+ match cement with
+ | ClassN -> 0.25
+ | ClassR -> 0.20
+ | ClassS -> 0.38
+
+ let private α age =
+ match age with
+ | (Age age) when age < 28 -> 1.0
+ | _ -> (2.0 / 3.0)
+
+ let private βcc_t cement age =
+ let (Age age) = age
+ s cement |> exp |> (*) (1.0 - sqrt (28.0 / float age))
+
+ []
+ module Strength =
+
+ let fcm_t (fck: CylinderStrength) (cem: Cement) (age: Age) =
+ let βcc_t = βcc_t cem age
+ let fcm_basic = BasicProperties.Strength.fcm fck
+ βcc_t * fcm_basic
+
+ let fctm_t (fck: CylinderStrength) (cem: Cement) (age: Age) =
+ let βcc_t = βcc_t cem age
+ let fctm_basic = BasicProperties.Strength.fctm fck
+ (βcc_t ** (α age)) * fctm_basic
+
+ []
+ module ElasticModulus =
+
+ let Ecm_t (fck: CylinderStrength) (cem: Cement) (agg: Aggregate) (age: Age) =
+ let fcm = BasicProperties.Strength.fcm fck
+ let fcm_timeDep = Strength.fcm_t fck cem age
+ let Ecm = BasicProperties.ElasticModulus.Ecm fck agg
+ ((fcm_timeDep / fcm) ** 0.3) * Ecm
diff --git a/src/concrete/ConcreteSerializer.fs b/src/concrete/ConcreteSerializer.fs
new file mode 100644
index 0000000..336ca68
--- /dev/null
+++ b/src/concrete/ConcreteSerializer.fs
@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+// module ConcreteSerializer =
+
+// module CylinderStrength =
+
+// let deserialize (region: string) (fck: string) =
+// match region.ToUpperInvariant() with
+// | "UK" ->
+// match fck.ToUpperInvariant() with
+// | "C12/15" -> UKConcrete Fck12
+// | "C16/20" -> UKConcrete Fck16
+// | "C20/25" -> UKConcrete Fck20
+// | "C25/30" -> UKConcrete Fck25
+// | "C30/37" -> UKConcrete Fck30
+// | "C35/45" -> UKConcrete Fck35
+// | "C40/50" -> UKConcrete Fck40
+// | "C45/55" -> UKConcrete Fck45
+// | "C50/60" -> UKConcrete Fck50
+// | "C55/67" -> UKConcrete Fck55
+// | "C60/75" -> UKConcrete Fck60
+// | "C70/85" -> UKConcrete Fck70
+// | "C80/95" -> UKConcrete Fck80
+// | "C90/105" -> UKConcrete Fck90
+// | _ -> invalidArg $"{nameof(fck)}" "No matching cylinder strength."
+// | _ -> invalidArg $"{nameof(region)}" "No matching Eurocode region."
+
+// let serialize (fck: CylinderStrength) =
+// match fck with
+// | UKConcrete Fck12 -> "C12/15"
+// | UKConcrete Fck16 -> "C16/20"
+// | UKConcrete Fck20 -> "C20/25"
+// | UKConcrete Fck25 -> "C25/30"
+// | UKConcrete Fck30 -> "C30/37"
+// | UKConcrete Fck35 -> "C35/45"
+// | UKConcrete Fck40 -> "C40/50"
+// | UKConcrete Fck45 -> "C45/55"
+// | UKConcrete Fck50 -> "C50/60"
+// | UKConcrete Fck55 -> "C55/67"
+// | UKConcrete Fck60 -> "C60/75"
+// | UKConcrete Fck70 -> "C70/85"
+// | UKConcrete Fck80 -> "C80/95"
+// | UKConcrete Fck90 -> "C90/105"
+
+// module Aggregate =
+
+// let deserialize (agg: string) =
+// match agg.ToLowerInvariant() with
+// | "basalt" -> Basalt
+// | "limestone" -> Limestone
+// | "quartzite" -> Quartzite
+// | "sandstone" -> Sandstone
+// | _ -> invalidArg $"{nameof(agg)}" "No matching aggregate type."
+
+// let serialize (aggregate: Aggregate) =
+// match aggregate with
+// | Basalt -> "Basalt"
+// | Limestone -> "Limestone"
+// | Quartzite -> "Quartzite"
+// | Sandstone -> "Sandstone"
+
+// module Cement =
+
+// let deserialize (cem: string) =
+// match cem.ToLowerInvariant() with
+// | "classn" -> ClassN
+// | "classs" -> ClassS
+// | "classr" -> ClassR
+// | _ -> invalidArg $"{nameof(cem)}" "No matching cement type."
+
+// let serialize (cem: Cement) =
+// match cem with
+// | ClassN -> "ClassN"
+// | ClassR -> "ClassR"
+// | ClassS -> "ClassS"
+
+// module MixProperties =
+
+// type MixPropertiesDTO =
+// { Aggregate: string
+// Cement: string
+// Grade: string
+// Region: string }
+
+// let deserialize (concreteMix: MixPropertiesDTO) : MixProperties =
+// match concreteMix with
+// | { Aggregate = agg
+// Cement = cem
+// Grade = fck
+// Region = region } ->
+// { Aggregate = Aggregate.deserialize agg
+// Cement = Cement.deserialize cem
+// Grade = CylinderStrength.deserialize region fck }
+
+// let serialize (mix: MixProperties) : MixPropertiesDTO =
+// let region =
+// match mix.Grade with
+// | UKConcrete _ -> "UK"
+// { Aggregate = Aggregate.serialize mix.Aggregate
+// Cement = Cement.serialize mix.Cement
+// Grade = CylinderStrength.serialize mix.Grade
+// Region = region }
+
+// module Strengths =
+
+// type StrengthsDTO =
+// { fck: float
+// fcm: float
+// fctm: float
+// fcm_t: float
+// fctm_t: float }
+
+// let deserialize (strengths: StrengthsDTO) : Strengths =
+// match strengths with
+// | { fck = _fck
+// fcm = _fcm
+// fctm = _fctm
+// fcm_t = _fcm_t
+// fctm_t = _fctm_t } ->
+// { fck = AddUnitsToFloat _fck
+// fcm = AddUnitsToFloat _fcm
+// fctm = AddUnitsToFloat _fctm
+// fcm_t = AddUnitsToFloat _fcm_t
+// fctm_t = AddUnitsToFloat _fctm_t }
+
+// let serialize (strengths: Strengths) : StrengthsDTO =
+// { fck = stripUnitsFromFloat strengths.fck
+// fcm = stripUnitsFromFloat strengths.fcm
+// fctm = stripUnitsFromFloat strengths.fctm
+// fcm_t = stripUnitsFromFloat strengths.fcm_t
+// fctm_t = stripUnitsFromFloat strengths.fctm_t }
+
+// module ElasticModuli =
+
+// type ElasticModuliDTO =
+// { Ecm: float
+// Ecm_t: float }
+
+// let deserialize (moduli: ElasticModuliDTO) : ElasticModuli =
+// { Ecm = AddUnitsToFloat moduli.Ecm
+// Ecm_t = AddUnitsToFloat moduli.Ecm_t }
+
+// let serialize (moduli: ElasticModuli) : ElasticModuliDTO =
+// { Ecm = stripUnitsFromFloat moduli.Ecm
+// Ecm_t = stripUnitsFromFloat moduli.Ecm_t }
+
+// module MechanicalProperties =
+
+// open Strengths
+// open ElasticModuli
+
+// type MechanicalPropertiesDTO =
+// { Strengths: StrengthsDTO
+// Strains: Strains
+// ElasticModuli: ElasticModuliDTO }
+
+// let deserialize (props: MechanicalPropertiesDTO) : MechanicalProperties =
+// match props with
+// | { Strengths = strengths
+// Strains = strains
+// ElasticModuli = moduli } ->
+// { Strengths = Strengths.deserialize strengths
+// Strains = strains
+// ElasticModuli = ElasticModuli.deserialize moduli }
+
+// let serialize (props: MechanicalProperties) : MechanicalPropertiesDTO =
+// { Strengths = Strengths.serialize props.Strengths
+// Strains = props.Strains
+// ElasticModuli = ElasticModuli.serialize props.ElasticModuli }
+
+// module Concrete =
+
+// open MixProperties
+// open MechanicalProperties
+
+// type ConcreteDTO =
+// { Age: int
+// Density: float
+// MixProperties: MixPropertiesDTO
+// MechanicalProperties: MechanicalPropertiesDTO }
+
+// let deserialize (concrete: ConcreteDTO) : Concrete =
+// match concrete with
+// | { Age = age
+// Density = density
+// MixProperties = mix
+// MechanicalProperties = props } ->
+// { Age = CurrentAge (AddUnitsToInteger32 age)
+// Density = AddUnitsToFloat density
+// MixProperties = MixProperties.deserialize mix
+// MechanicalProperties = MechanicalProperties.deserialize props }
+
+// let serialize (concrete: Concrete) : ConcreteDTO =
+// let unwrapAge (CurrentAge age) = age
+// { Age = stripUnitsFromInteger (unwrapAge concrete.Age)
+// Density = stripUnitsFromFloat concrete.Density
+// MixProperties = MixProperties.serialize concrete.MixProperties
+// MechanicalProperties = MechanicalProperties.serialize concrete.MechanicalProperties }
diff --git a/src/concrete/Creep.fs b/src/concrete/Creep.fs
new file mode 100644
index 0000000..dff7a29
--- /dev/null
+++ b/src/concrete/Creep.fs
@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Structures
+
+// open type System.Math
+// open FSharp.Data.UnitSystems.SI.UnitSymbols
+
+// module Creep =
+
+//type RelativeHumidity = RelativeHumidity of float
+//type LoadingAge = LoadingAge of int
+//type NotionalSize = NotionalSize of h0:float
+
+/// Convenient collection of Creep and Shrinkage related parameters.
+//type CreepShrinkage =
+// { RelativeHumidity: RelativeHumidity
+// LoadingAge: LoadingAge
+// NotionalSize: NotionalSize
+// CreepCoefficient: float
+// ShrinkageStrain: float }
+
+// // Partially applied functions to calculate Creep α factors.
+// let private α (power: float) (fcm: float) = (35.0 / fcm) ** power
+
+// let α1 = α 0.7
+// let α2 = α 0.2
+// let α3 = α 0.5
+
+// /// β_fcm is a factor to allow for the effect of concrete strength on the
+// /// notional creep coefficient.
+// let βfcm (fcm: float) =
+// let fcm = float fcm
+// 16.8 / sqrt fcm
+
+// /// β_t0 is a factor to allow for the effect of concrete age at loading
+// /// on the notional creep coefficient.
+// let βt0 (LoadingAge loadingAge) =
+// 1.0 / (0.1 + (float loadingAge ** 0.2))
+
+// /// βH is a coefficient depending on the relative humidity (RH in %).
+// let βH (RelativeHumidity rh) (NotionalSize h0) (fcm: float) =
+// let h0 = float h0
+// if fcm <= 35.0 then
+// let βh =
+// 0.012 * (rh ** 18.0)
+// |> (+) 1.0
+// |> (*) 1.5
+// |> (*) h0
+// |> (+) 250.0
+// if βh <= 1500.0 then βh else 1500.0
+// else
+// let βh =
+// 0.012 * (rh ** 18.0)
+// |> (+) 1.0
+// |> (*) 1.5
+// |> (*) h0
+// |> (+) (250.0 * α3 fcm)
+// if βh <= (1500.0 * α3 fcm) then βh else (1500.0 * α3 fcm)
+
+// /// β_ctt0 is a coefficient to describe the development of creep with time after loading.
+// let βctt0 (LoadingAge loadingAge) (CurrentAge currentAge) (βH: float) =
+// let t = (float)currentAge
+// let t0 = (float)loadingAge
+// let numerator = t - t0
+// let denominator = βH + t - t0
+// let quotient = numerator / denominator
+// quotient ** 0.3
+
+// /// Returns the Notional Creep Coefficient (RH) to EC2.
+// let ψRH (RelativeHumidity rh) (NotionalSize h0) (fcm: float) =
+// let h0 = (float)h0
+// if fcm <= 35.0 then
+// let numerator = 1.0 - rh
+// let denominator = 0.1 * Cbrt(h0)
+// let quotient = numerator / denominator
+// 1.0 + quotient
+// else
+// let numerator = 1.0 - rh
+// let denominator = 0.1 * Cbrt(h0) * (α1 fcm)
+// let quotient = numerator / denominator
+// (1.0 + quotient) * (α2 fcm)
+
+// /// Returns the Creep Coefficient to EC2.
+// let ψtt0 (βctt0: float) (βfcm: float) (βt0: float) (ψRH: float) =
+// ψRH * βfcm * βt0 * βctt0
+
+// /// Calculates notional geometric size of member.
+// let private getNotionalSize (section: Shape3D) =
+// let area = getSectionalArea section
+// let perimeter = getPerimeter section
+// 2.0 * area / perimeter |> NotionalSize
+
+// let private validateLoadingAge (loadingAge: int) (CurrentAge concreteAge) =
+// if loadingAge <= 0 || loadingAge > concreteAge then
+// invalidArg $"nameof{loadingAge}" "Illegal concrete loading age."
+// else LoadingAge loadingAge
+
+// let private validateRelativeHumidity (rh: float) =
+// if (rh <= 0.0 || rh >= 1.0) then
+// invalidArg $"{nameof(rh)}" "Relative Humidity outside allowable range."
+// else RelativeHumidity rh
+
+// /// Calculates the Creep Coefficient, ψtt0, to EC2.
+// let getCreepCoefficient (section: Shape3D) (loadingAge: int) (relativeHumidity: float) concrete =
+// let h0 = getNotionalSize section
+// let rh = validateRelativeHumidity relativeHumidity
+// let loadingAge = validateLoadingAge loadingAge concrete.Age
+// let fcm = concrete.MechanicalProperties.Strengths.fcm
+// let currentAge = concrete.Age
+// let βfcm = βfcm fcm
+// let βt0 = βt0 loadingAge
+// let βH = βH rh h0 fcm
+// let βctt0 = βctt0 loadingAge currentAge βH
+// let ψRH = ψRH rh h0 fcm
+// ψtt0 βctt0 βfcm βt0 ψRH
diff --git a/src/concrete/DesignCodes.fs b/src/concrete/DesignCodes.fs
new file mode 100644
index 0000000..d3f7098
--- /dev/null
+++ b/src/concrete/DesignCodes.fs
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+/// Supported eurocodes (EC).
+type Eurocode = | UK
+
+/// Supported global design codes.
+type DesignCode = Eurocode of Eurocode
diff --git a/src/concrete/Detailing.fs b/src/concrete/Detailing.fs
new file mode 100644
index 0000000..fd5ec35
--- /dev/null
+++ b/src/concrete/Detailing.fs
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Structures
+
+//open System
+
+///// Centre-to-centre spacing between adjacent bars/links.
+//type BarSpacing = BarSpacing of float
+
+///// Space between bars, measured between the outermost bar dimensions.
+//type ClearSpace = ClearSpace of float
+
+///// Total number of reinforcing bars.
+//type NumberOfBars = NumberOfBars of int
+
+//// Lightweight records specifying rebar configuration.
+//type NumberOfBarsAndBarSize = { NumberOfBars: NumberOfBars; BarSize: BarSize }
+//type LinkSpacingAndLinkSize = { LinkSpacing: BarSpacing; LinkSize: BarSize }
+
+//module Detailing =
+
+// /// Returns no. of intermediate bars along a given length at the prescribed spacing.
+// /// This does NOT consider bars at the start or end (i.e. at x = 0 and x = L).
+// /// For a 1000mm length at 250mm spacing, the equations returns 3 intermediate bars.
+// let getNumberOfIntermediateBars (BarSpacing sp) (Length l) =
+// int (Math.Ceiling (l / sp)) - 1
+// |> AddUnitsToInteger32
+// |> NumberOfBars
+
+// /// Calculates bar spacing for intermediate bars along a given length.
+// /// This does NOT consider bars at the start or end (i.e. at x = 0 and x = L).
+// /// For a 1000mm length with 3 inner bars and 2 end bars (5 bars in total),
+// /// the equation ignores the end bars and returns a spacing of 250mm.
+// let getIntermediateBarSpacing (NumberOfBars n) (Length l) = BarSpacing (l / (float n + 1.0))
+
+// /// Returns the minimum permissible clear spacing between adjacent bars.
+// let getMinimumClearBarSpacing (barSize: BarSize) = ClearSpace barSize.diameter
+
+// /// Calculates clear spacing between two straight bars by comparing the pythagorean
+// /// distance between their respective cross-section centroids in 3D space.
+// let clearSpaceBetweenBars (bar1: StraightBar) (bar2: StraightBar) =
+// let p1 = bar1.Geometry.StartPoint
+// let p2 = bar2.Geometry.StartPoint
+// let halfΦ1 = (bar1.BarSize.diameter / 2.0) |> UnitAnnotation.stripUnitsFromFloat
+// let halfΦ2 = (bar2.BarSize.diameter / 2.0) |> UnitAnnotation.stripUnitsFromFloat
+// (distanceBetweenPoints p1 p2) - (halfΦ1 + halfΦ2)
+// |> AddUnitsToFloat
+
+// // Union type helper methods.
+// let internal unwrapBarSpacing (BarSpacing sp) = sp
+// let internal unwrapClearSpace (ClearSpace sp) = sp
diff --git a/src/concrete/DomainError.fs b/src/concrete/DomainError.fs
new file mode 100644
index 0000000..8c0badf
--- /dev/null
+++ b/src/concrete/DomainError.fs
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+type ConcreteError = InvalidAge of message: string
+
+type ReinforcedConcreteError = ConcreteError of ConcreteError
diff --git a/src/concrete/Durability.fs b/src/concrete/Durability.fs
new file mode 100644
index 0000000..0d4678e
--- /dev/null
+++ b/src/concrete/Durability.fs
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+//module Durability =
+
+// /// Concrete protective cover, measured from the outside face
+// /// of the concrete to the outermost rebar surface.
+// type Cover = Cover of float
+
+// type ConcreteCover =
+// | RectangularSectionCover of Top:Cover * Left:Cover * Right:Cover * Bottom:Cover * End:Cover
+// | CircularSectionCover of Perimeter:Cover * End:CoverearSpace sp) = sp
diff --git a/src/concrete/Geometry.fs b/src/concrete/Geometry.fs
new file mode 100644
index 0000000..ca471bc
--- /dev/null
+++ b/src/concrete/Geometry.fs
@@ -0,0 +1,319 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Structures
+
+open System
+open Gazelle.Units
+
+/// Orthogonal geometric cross-sectional axes.
+type Axis =
+ | X
+ | Y
+
+/// Orthogonal axes about which rotation occurs.
+type RotationalAxis =
+ | XX
+ | YY
+
+// Basic geometric properties.
+type Length<[] 'T> = Length of float<'T>
+type Width<[] 'T> = Width of float<'T>
+type Depth<[] 'T> = Depth of float<'T>
+type Diameter<[] 'T> = Diameter of float<'T>
+type Perimeter<[] 'T> = Perimeter of float<'T>
+type CrossSectionDiameter<[] 'T> = CrossSectionDiameter of float<'T>
+type CrossSectionalArea<[] 'T> = CrossSectionalArea of float<'T^2>
+type SurfaceArea<[] 'T> = SurfaceArea of float<'T^2>
+type Volume<[] 'T> = Volume of float<'T^3>
+type SecondMomentOfArea<[] 'T> = SecondMomentOfArea of float<'T^4>
+
+// Inner/outer specific dimensions.
+type InnerWidth<[] 'T> = InnerWidth of float<'T>
+type OuterWidth<[] 'T> = OuterWidth of float<'T>
+type InnerDepth<[] 'T> = InnerDepth of float<'T>
+type OuterDepth<[] 'T> = OuterDepth of float<'T>
+type InnerDiameter<[] 'T> = InnerDiameter of float<'T>
+type OuterDiameter<[] 'T> = OuterDiameter of float<'T>
+
+/// Defines whether 3D shape end-faces are to be included in surface area calculations.
+type EndFacesFlag =
+ | IncludeEndFaces
+ | ExcludeEndFaces
+
+/// 2D coordinate set.
+type Point2D<[] 'T> = { X: float<'T>; Y: float<'T> }
+
+/// 3D coordinate set.
+type Point3D<[] 'T> =
+ { X: float<'T>
+ Y: float<'T>
+ Z: float<'T> }
+
+/// Cuboidal geometry with centrepoints defined at either end.
+type Cuboid<[] 'T> =
+ { StartPoint: Point3D<'T>
+ EndPoint: Point3D<'T>
+ Width: Width<'T>
+ Depth: Depth<'T>
+ Length: Length<'T> }
+
+/// Cylindrical geometry with centrepoints defined at either end.
+type Cylinder<[] 'T> =
+ { StartPoint: Point3D<'T>
+ EndPoint: Point3D<'T>
+ Diameter: Diameter<'T>
+ Length: Length<'T> }
+
+/// Hoop geometry moulded into a rectangular shape with a circular cross-section.
+type RectangularHoop<[] 'T> =
+ { Centroid: Point3D<'T>
+ Width: Width<'T>
+ Depth: Depth<'T>
+ LegDiameter: CrossSectionDiameter<'T>
+ InnerWidth: InnerWidth<'T>
+ InnerDepth: InnerDepth<'T>
+ OuterWidth: OuterWidth<'T>
+ OuterDepth: OuterDepth<'T> }
+
+/// Hoop geometry moulded into a circular shape with a circular cross-section.
+type CircularHoop<[] 'T> =
+ { Centroid: Point3D<'T>
+ Diameter: Diameter<'T>
+ LegDiameter: CrossSectionDiameter<'T>
+ InnerDiameter: InnerDiameter<'T>
+ OuterDiameter: OuterDiameter<'T> }
+
+/// Unified type for all 3D solid geometries.
+type Solid3D<[] 'T> =
+ | Cuboid of Cuboid<'T>
+ | Cylinder of Cylinder<'T>
+
+/// Unified type for all 3D hoop geometries.
+type Hoop3D<[] 'T> =
+ | RectangularHoop of RectangularHoop<'T>
+ | CircularHoop of CircularHoop<'T>
+
+/// Unified type for all 3D geometries.
+type Shape3D<[] 'T> =
+ | Solid3D of Solid3D<'T>
+ | Hoop3D of Hoop3D<'T>
+
+
+/// Collection of modules/functions to calculate geometric properties for various shapes.
+[]
+module Geometry =
+
+ /// Collection of utility functions to create various geometrical shapes.
+ module Create =
+
+ /// Creates a validated Cuboid instance.
+ ///
+ let cuboid (xyCentroid: Point2D<'T>) width depth length =
+ match width, depth, length with
+ | width, _, _ when width <= 0.0<_> -> invalidArg $"{nameof (width)}" "Width <= 0."
+ | _, depth, _ when depth <= 0.0<_> -> invalidArg $"{nameof (depth)}" "Depth <= 0."
+ | _, _, length when length <= 0.0<_> -> invalidArg $"{nameof (length)}" "Length <= 0."
+ | _ ->
+ { StartPoint =
+ { X = xyCentroid.X
+ Y = xyCentroid.Y
+ Z = 0.0<_> }
+ EndPoint =
+ { X = xyCentroid.X
+ Y = xyCentroid.Y
+ Z = 0.0<_> }
+ Width = Width width
+ Depth = Depth depth
+ Length = Length length }
+
+ /// Creates a validated Cylinder instance.
+ ///
+ let cylinder (xyCentroid: Point2D<'T>) (diameter: float<'T>) (length: float<'T>) =
+ match diameter, length with
+ | diameter, _ when diameter <= 0.0<_> -> invalidArg $"{nameof (diameter)}" "Diameter <= 0."
+ | _, length when length <= 0.0<_> -> invalidArg $"{nameof (length)}" "Length <= 0."
+ | _ ->
+ { StartPoint =
+ { X = xyCentroid.X
+ Y = xyCentroid.Y
+ Z = 0.0<_> }
+ EndPoint =
+ { X = xyCentroid.X
+ Y = xyCentroid.Y
+ Z = 0.0<_> }
+ Diameter = Diameter diameter
+ Length = Length length }
+
+
+ /// Creates a validated RectangularHoop instance.
+ ///
+ let rectangularHoop (centroid: Point3D<'T>) width depth legDiameter =
+ match width, depth, legDiameter with
+ | width, _, _ when width <= 0.0<_> -> invalidArg $"{nameof (width)}" "Width <= 0."
+ | _, depth, _ when depth <= 0.0<_> -> invalidArg $"{nameof (depth)}" "Depth <= 0."
+ | _, _, legDiameter when legDiameter <= 0.0<_> -> invalidArg $"{nameof (legDiameter)}" "Leg Diameter <= 0."
+ | _ ->
+ { Centroid = centroid
+ Width = Width width
+ Depth = Depth depth
+ LegDiameter = CrossSectionDiameter legDiameter
+ InnerWidth = InnerWidth(width - legDiameter)
+ InnerDepth = InnerDepth(depth - legDiameter)
+ OuterWidth = OuterWidth(width + legDiameter)
+ OuterDepth = OuterDepth(depth + legDiameter) }
+
+ /// Creates a validated CircularHoop instance.
+ ///
+ let circularHoop (centroid: Point3D<'T>) diameter legDiameter =
+ match diameter, legDiameter with
+ | diameter, _ when diameter <= 0.0<_> -> invalidArg $"{nameof (diameter)}" "Diameter <= 0."
+ | _, legDiameter when legDiameter <= 0.0<_> -> invalidArg $"{nameof (legDiameter)}" "Leg Diameter <= 0."
+ | _ ->
+ { Centroid = centroid
+ Diameter = Diameter diameter
+ LegDiameter = CrossSectionDiameter legDiameter
+ InnerDiameter = InnerDiameter(diameter - legDiameter)
+ OuterDiameter = OuterDiameter(diameter + legDiameter) }
+
+
+ /// Collection of utility functions to query geometric properties of shapes.
+ []
+ module Query =
+
+ /// Unified type for all 3D solid geometries.
+ []
+ []
+ module Solid3D =
+
+ /// Returns the start coordinate of a given 3D solid geometry.
+ let getStartPoint (s: Solid3D<'T>) =
+ match s with
+ | Cuboid c -> c.StartPoint
+ | Cylinder c -> c.StartPoint
+
+ /// Returns the end coordinate of a given 3D solid geometry.
+ let getEndPoint (s: Solid3D<'T>) =
+ match s with
+ | Cuboid c -> c.EndPoint
+ | Cylinder c -> c.EndPoint
+
+ /// Returns the length of a given 3D solid geometry.
+ let getLength (s: Solid3D<'T>) =
+ match s with
+ | Cuboid c -> c.Length
+ | Cylinder c -> c.Length
+
+ /// Returns the cross-sectional perimeter of a given 3D solid geometry.
+ let getPerimeter (s: Solid3D<'T>) =
+ match s with
+ | Cuboid { Width = Width w; Depth = Depth d } -> Perimeter((2.0 * w) + (2.0 * d))
+ | Cylinder { Diameter = Diameter d } -> Perimeter(Math.PI * d)
+
+ /// Returns the cross-sectional area of a given 3D solid geometry.
+ let getCrossSectionalArea (s: Solid3D<'T>) =
+ match s with
+ | Cuboid { Width = Width w; Depth = Depth d } -> CrossSectionalArea(w * d)
+ | Cylinder { Diameter = Diameter d } -> CrossSectionalArea(Math.PI * (Math.pow2 d) / 4.0)
+
+ /// Returns the volume of a given 3D solid geometry.
+ let getVolume (s: Solid3D<'T>) =
+ let (CrossSectionalArea a) = getCrossSectionalArea s
+ let (Length l) = getLength s
+
+ match s with
+ | _ -> Volume(a * l)
+
+ /// Returns the surface area of a given 3D solid geometry. Optionally include the end faces.
+ let getSurfaceArea (f: EndFacesFlag) (s: Solid3D<'T>) =
+ let (Perimeter p) = getPerimeter s
+ let (Length l) = getLength s
+ let (CrossSectionalArea a) = getCrossSectionalArea s
+
+ match s with
+ | _ ->
+ match f with
+ | IncludeEndFaces -> SurfaceArea((2.0 * a) + (p * l))
+ | ExcludeEndFaces -> SurfaceArea(p * l)
+
+ /// Returns the second moment of area of a given 3D solid geometry about either the XX or YY axes.
+ let getSecondMomentOfArea (axis: RotationalAxis) (s: Solid3D<'T>) =
+ match s with
+ | Cuboid { Width = Width w; Depth = Depth d } ->
+ match axis with
+ | XX -> SecondMomentOfArea(w * Math.pow3 d / 12.0)
+ | YY -> SecondMomentOfArea(d * Math.pow3 w / 12.0)
+ | Cylinder { Diameter = Diameter d } ->
+ match axis with
+ | _ -> SecondMomentOfArea(Math.PI * Math.pow4 d / 64.0)
+
+ /// Unified type for all 3D hoop geometries.
+ []
+ []
+ module Hoop3D =
+
+ /// Returns the centroid coordinates of a given hoop geometry.
+ let getCentroid (h: Hoop3D<'T>) =
+ match h with
+ | RectangularHoop rh -> rh.Centroid
+ | CircularHoop ch -> ch.Centroid
+
+ /// Returns the hoop length.
+ let getLength (h: Hoop3D<'T>) =
+ match h with
+ | RectangularHoop { Width = Width w; Depth = Depth d } -> Length((2.0 * w) + (2.0 * d))
+ | CircularHoop { Diameter = Diameter d } -> Length(Math.PI * d)
+
+ /// Returns the cross-sectional area of the hoop leg.
+ let getCrossSectionalAreaOfLeg (h: Hoop3D<'T>) =
+ match h with
+ | RectangularHoop { LegDiameter = CrossSectionDiameter sd }
+ | CircularHoop { LegDiameter = CrossSectionDiameter sd } ->
+ CrossSectionalArea(Math.PI * Math.pow2 sd / 4.0)
+
+ /// Returns the hoop volume.
+ let getVolume (h: Hoop3D<'T>) =
+ let (Length l) = getLength h
+ let (CrossSectionalArea a) = getCrossSectionalAreaOfLeg h
+
+ match h with
+ | _ -> Volume(l * a)
+
+ /// Unified type for all 3D geometries.
+ []
+ []
+ module Shape3D =
+
+ /// Returns the 3D shape volume.
+ let getVolume (shape: Shape3D<'T>) =
+ match shape with
+ | Solid3D s -> Solid3D.getVolume s
+ | Hoop3D h -> Hoop3D.getVolume h
+
+ /// Returns the 3D shape length.
+ let getLength (shape: Shape3D<'T>) =
+ match shape with
+ | Solid3D s -> Solid3D.getLength s
+ | Hoop3D h -> Hoop3D.getLength h
+
+ /// Collection of convenient utility functions to unwrap single-case union types.
+ []
+ module Unwrap =
+
+ let width (Width w) = w
+ let depth (Depth d) = d
+ let diameter (Diameter d) = d
+ let crossSectionDiameter (CrossSectionDiameter sd) = sd
+ let length (Length l) = l
+ let perimeter (Perimeter p) = p
+ let crossSectionalArea (CrossSectionalArea a) = a
+ let volume (Volume v) = v
+ let surfaceArea (SurfaceArea sa) = sa
+ let secondMomentOfArea (SecondMomentOfArea sm) = sm
+ let innerWidth (InnerWidth iw) = iw
+ let innerDepth (InnerDepth id) = id
+ let innerDiameter (InnerDiameter id) = id
+ let outerWidth (OuterWidth ow) = ow
+ let outerDepth (OuterDepth od) = od
+ let outerDiameter (OuterDiameter od) = od
diff --git a/src/concrete/LimitStates.fs b/src/concrete/LimitStates.fs
new file mode 100644
index 0000000..9138682
--- /dev/null
+++ b/src/concrete/LimitStates.fs
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+type DesignSituation =
+ | Persistent
+ | Transient
+ | Accidental
+
+type LimitState =
+ | ULS of DesignSituation
+ | SLS
diff --git a/src/concrete/RCColumn.fs b/src/concrete/RCColumn.fs
new file mode 100644
index 0000000..243afb4
--- /dev/null
+++ b/src/concrete/RCColumn.fs
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+//open System
+
+//type RebarCageSpec =
+// | WallCageSpec of WallCage.Specification
+// | ColumnCageSpec of ColumnCage.Specification
+
+//type RebarCage =
+// | WallCage of WallCage.Cage
+// | ColumnCage of ColumnCage.Cage
+
+//type RCSection =
+// { Geometry: Solid3D
+// Concrete: Concrete
+// Cover: ConcreteCover
+// RebarCage: RebarCage }
+
+//[]
+//module RCSection =
+
+// /// Returns a fully configured RC Section with concrete/steel properties and
+// /// reinforcement cage. Method leverages complex union types to switch between
+// /// different categories of section (e.g. column vs. wall) and to allow
+// /// for alternative cross-sectional geometries.
+// let create concrete steel sectionGeometry cover (cageSpecification: RebarCageSpec) =
+// let cage =
+// match cageSpecification with
+// | WallCageSpec wallCageSpec ->
+// match sectionGeometry, cover with
+// | Cuboid (Centreline cl, Width w, Depth d),
+// RectangularSectionCover (Cover top, Cover left, Cover right, Cover bottom, Cover ends) ->
+// let horizBarSize = wallCageSpec.HorizontalBarSpec.BarSize.diameter
+// let cageWidth = w - left - right - horizBarSize
+// let cageDepth = d - top - bottom
+// let cageLength = (AddUnitsToFloat cl.Length) - (2.0 * ends)
+// RectangularCage (Width cageWidth, Depth cageDepth, Length cageLength)
+// |> WallCage.create wallCageSpec steel |> WallCage
+// | Cylinder _, _ | _, CircularSectionCover _ ->
+// invalidArg $"{nameof(sectionGeometry)}" "Walls do not support cylindrical geometries."
+
+// | ColumnCageSpec columnCageSpec ->
+// match sectionGeometry, cover with
+// | Cuboid (Centreline cl, Width w, Depth d),
+// RectangularSectionCover (Cover top, Cover left, Cover right, Cover bottom, Cover ends) ->
+// match columnCageSpec with
+// | ColumnCage.RectangularCageSpec rectColCageSpec ->
+// let linkSize = rectColCageSpec.Links.LinkSize.diameter
+// let cageWidth = w - left - right - linkSize
+// let cageDepth = d - top - bottom - linkSize
+// let cageLength = (AddUnitsToFloat cl.Length) - (2.0 * ends)
+// RectangularCage (Width cageWidth, Depth cageDepth, Length cageLength)
+// |> ColumnCage.create columnCageSpec steel |> ColumnCage
+// | Cylinder _, _ | _, CircularSectionCover _ ->
+// raise (NotImplementedException "Circular geometries not yet supported.")
+
+// { Geometry = sectionGeometry
+// Concrete = concrete
+// Cover = cover
+// RebarCage = cage }
diff --git a/src/concrete/RCPier.fs b/src/concrete/RCPier.fs
new file mode 100644
index 0000000..5d5e588
--- /dev/null
+++ b/src/concrete/RCPier.fs
@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Concrete
+
+//[]
+//module RCPier =
+
+// type RCPier =
+// { Section: RCSection
+// PierDefinition: Pier }
+
+// let create pierDef rcSection =
+// { Section = rcSection
+// PierDefinition = pierDef }
diff --git a/src/concrete/Rebar.fs b/src/concrete/Rebar.fs
new file mode 100644
index 0000000..042c058
--- /dev/null
+++ b/src/concrete/Rebar.fs
@@ -0,0 +1,101 @@
+namespace Gazelle.Concrete
+
+open Gazelle.Units
+open FSharp.Data.UnitSystems.SI.UnitSymbols
+
+type UKSteelGrade =
+ | B400
+ | B500
+ | B600
+ | C400
+ | C500
+ | C600
+
+type SteelGrade = UKSteelGrade of UKSteelGrade
+
+type UKBarSize =
+ | H6
+ | H8
+ | H10
+ | H12
+ | H16
+ | H20
+ | H25
+ | H32
+ | H40
+ | H50
+
+type BarSize = UKBarSize of UKBarSize
+
+type StraightBar =
+ { SteelGrade: SteelGrade
+ BarSize: BarSize
+ Geometry: Cylinder }
+
+type RectangularLink =
+ { SteelGrade: SteelGrade
+ BarSize: BarSize
+ Geometry: RectangularHoop }
+
+[]
+module Rebar =
+
+ let getDiameter (b: BarSize) =
+ match b with
+ | UKBarSize size ->
+ match size with
+ | H6 -> 6.0
+ | H8 -> 8.0
+ | H10 -> 10.0
+ | H12 -> 12.0
+ | H16 -> 16.0
+ | H20 -> 20.0
+ | H25 -> 25.0
+ | H32 -> 32.0
+ | H40 -> 40.0
+ | H50 -> 50.0
+
+ []
+ module Steel =
+
+ let getYieldStrength (s: SteelGrade) =
+ match s with
+ | UKSteelGrade grade ->
+ match grade with
+ | B400 -> 400.0
+ | B500 -> 500.0
+ | B600 -> 600.0
+ | C400 -> 400.0
+ | C500 -> 500.0
+ | C600 -> 600.0
+
+ let getDensity (s: SteelGrade) =
+ match s with
+ | UKSteelGrade _ -> Density 7850.0
+
+ let getElasticModulus (s: SteelGrade) =
+ match s with
+ | UKSteelGrade _ -> 200_000.0
+
+ []
+ module StraightBar =
+
+ let tryCreate (s: SteelGrade) (b: BarSize) (centroid: Point2D) (l: Length) : StraightBar =
+ let (Length length) = l
+ let diameter = getDiameter b
+
+ { SteelGrade = s
+ BarSize = b
+ Geometry = Geometry.Create.cylinder centroid diameter length }
+
+ []
+ module RectangularLink =
+
+ let tryCreate (s: SteelGrade) (b: BarSize) (centroid: Point3D) (w: Width) (d: Depth) =
+ let (Width width) = w
+ let (Depth depth) = d
+ let legDiameter = getDiameter b
+
+ { SteelGrade = s
+ BarSize = b
+ Geometry = Geometry.Create.rectangularHoop centroid width depth legDiameter }
diff --git a/src/units/Annotation.fs b/src/units/Annotation.fs
new file mode 100644
index 0000000..f2ca505
--- /dev/null
+++ b/src/units/Annotation.fs
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Units
+
+///
+/// Remove unit-of-measure type annotations from various types.
+///
+[]
+module RemoveUnits =
+
+ ///
+ /// Remove unit-of-measure type annotation from 32-bit integer.
+ ///
+ /// Annotated 32-bit integer.
+ /// Value without unit-of-measure.
+ let fromInt32 (value: int<'T>) : int = int value
+
+ ///
+ /// Remove unit-of-measure type annotation from float.
+ ///
+ /// Annotated float.
+ /// Value without unit-of-measure.
+ let fromFloat (value: float<'T>) : float = float value
+
+///
+/// Add unit-of-measure type annotations to various types.
+///
+[]
+module AddUnits =
+
+ ///
+ /// Add unit-of-measure type annotation to 32-bit integer.
+ ///
+ /// Primitive 32-bit integer.
+ /// Value annotated with unit-of-measure.
+ let toInt32 (value: int) : int<'T> =
+ LanguagePrimitives.Int32WithMeasure value
+
+ ///
+ /// Add unit-of-measure type annotation to float.
+ ///
+ /// Primitive float.
+ /// Value annotated with unit-of-measure.
+ let toFloat (value: float) : float<'T> =
+ LanguagePrimitives.FloatWithMeasure value
diff --git a/src/units/Conversion.fs b/src/units/Conversion.fs
new file mode 100644
index 0000000..27a8786
--- /dev/null
+++ b/src/units/Conversion.fs
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Units
+
+open FSharp.Data.UnitSystems.SI.UnitSymbols
+
+///
+/// Functions to convert unit-of-measure type annotations.
+///
+[]
+module Convert =
+
+ ///
+ /// Converts millimetres to metres.
+ ///
+ /// Value in millimetres.
+ /// Value in metres.
+ let millimetresToMetres (n: float) : float = n / 1000.0
+
+ ///
+ /// Converts metres to millimetres.
+ ///
+ /// Value in metres.
+ /// Value in millimetres.
+ let metresToMillimetres (n: float) : float = n * 1000.0
+
+ ///
+ /// Converts Newtons to Kilonewtons.
+ ///
+ /// Value in Newtons.
+ /// Value in Kilonewtons.
+ let newtonsToKilonewtons (x: float) : float = x / 1000.0
+
+ ///
+ /// Converts Kilonewtons to Newtons.
+ ///
+ /// Value in Kilonewtons.
+ /// Value in Newtons.
+ let kilonewtonsToNewtons (x: float) : float = x * 1000.0
diff --git a/src/units/Math.fs b/src/units/Math.fs
new file mode 100644
index 0000000..129ad99
--- /dev/null
+++ b/src/units/Math.fs
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Units
+
+///
+/// Collection of mathematical modules that operate on
+/// values with unit-of-measure type annotations.
+///
+[]
+module Math =
+
+ ///
+ /// Unit safe exponentiation function raised to the power of 2.
+ ///
+ /// Original value.
+ /// Value raised to the power of 2.
+ let pow2 (n: float<'T>) : float<'T^2> = n * n
+
+ ///
+ /// Unit safe exponentiation function raised to the power of 3.
+ ///
+ /// Original value.
+ /// Value raised to the power of 3.
+ let pow3 (n: float<'T>) : float<'T^3> = n * n * n
+
+ ///
+ /// Unit safe exponentiation function raised to the power of 4.
+ ///
+ /// Original value.
+ /// Value raised to the power of 4.
+ let pow4 (n: float<'T>) : float<'T^4> = n * n * n * n
diff --git a/src/units/Units.fs b/src/units/Units.fs
new file mode 100644
index 0000000..1a8d205
--- /dev/null
+++ b/src/units/Units.fs
@@ -0,0 +1,58 @@
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// Gazelle: a fast, cross-platform engine for structural analysis & design.
+// Copyright (C) 2024 James S. Bayley
+
+namespace Gazelle.Units
+
+open FSharp.Data.UnitSystems.SI.UnitSymbols
+
+///
+/// Time in days.
+///
+[]
+type days
+
+///
+/// Length in millimetres.
+///
+[]
+type mm
+
+///
+/// Force in Kilonewtons.
+///
+[]
+type kN
+
+///
+/// Moment or Torque in Kilonewton-Metres.
+///
+[]
+type kNm = kN * m
+
+///
+/// Number of layers.
+///
+[]
+type layers
+
+///
+/// Number of bars.
+///
+[]
+type bars
+
+///
+/// Unit of density.
+///
+type Density<[] 'TMass, [] 'TLength> = Density of float<'TMass / 'TLength^3>
+
+///
+/// Unit of mechanical stress.
+///
+type Stress<[] 'TForce, [] 'TLength> = Stress of float<'TForce / 'TLength^2>
+
+///
+/// Unit of mechanical pressure.
+///
+type Pressure<[] 'TForce, [] 'TLength> = Stress<'TForce, 'TLength>
diff --git a/tests/Gazelle.Tests.fsproj b/tests/Gazelle.Tests.fsproj
new file mode 100644
index 0000000..fd94ee8
--- /dev/null
+++ b/tests/Gazelle.Tests.fsproj
@@ -0,0 +1,31 @@
+
+
+
+ James S. Bayley
+ GazelleKit
+ net8.0
+ https://github.com/gazellekit/gazelle
+ false
+ true
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/tests/Geometry.Tests.fs b/tests/Geometry.Tests.fs
new file mode 100644
index 0000000..f2baeff
--- /dev/null
+++ b/tests/Geometry.Tests.fs
@@ -0,0 +1,489 @@
+namespace Gazelle.Geometry.Tests
+
+open System
+open Xunit
+
+module GeometryTests =
+
+ []
+ let ``Mock Test`` () = Assert.True(true)
+
+// module CuboidTests =
+
+// []
+// let ``Cuboid with width and depth of 200mm and length greater than 0mm has perimeter of 800mm`` () =
+// let c =
+// Geometry.Create.cuboid { X = 0.0; Y = 0.0 } 200.0 200.0 1000.0
+
+// let expected = 800.0
+
+// let actual =
+// Geometry.Query.Solid3D.getPerimeter (Cuboid c) |> Geometry.Unwrap.perimeter
+
+// Assert.Equal(expected, actual)
+
+// []
+// let ``Cuboid with width and depth of 200mm and length greater than 0mm has area of 40_000mm^2`` () =
+// let c =
+// Geometry.Create.cuboid { X = 0.0; Y = 0.0 } 200.0 200.0 1000.0
+
+// let expected = 40_000.0
+
+// let actual =
+// Geometry.Query.Solid3D.getCrossSectionalArea (Cuboid c)
+// |> Geometry.Unwrap.crossSectionalArea
+
+// Assert.Equal(expected, actual)
+
+// []
+// let ``Cuboid with width and depth of 200mm and length of 1000mm has volume of 40_000_000mm^3`` () =
+// let c =
+// Geometry.Create.cuboid { X = 0.0; Y = 0.0 } 200.0 200.0 1000.0
+
+// let expected = 40_000_000.0
+// let actual = Geometry.Query.Solid3D.getVolume (Cuboid c) |> Geometry.Unwrap.volume
+// Assert.Equal(expected, actual)
+
+// []
+// let ``Cuboid with width and depth of 200mm and length of 1000mm has 2nd-moment about XX of 133_333_333.3mm^4``
+// ()
+// =
+// let c =
+// Geometry.Create.cuboid { X = 0.0; Y = 0.0 } 200.0 200.0 1000.0
+
+// let expected = 133_333_333.3
+
+// let actual =
+// Geometry.Query.Solid3D.getSecondMomentOfArea XX (Cuboid c)
+// |> Geometry.Unwrap.secondMomentOfArea
+// |> Units.Annotate.stripUnitsFromFloat
+
+// Assert.Equal(expected, actual, 1)
+
+// []
+// let ``Cuboid with width and depth of 200mm and length of 1000mm has 2nd-moment about YY of 133_333_333.3mm^4``
+// ()
+// =
+// let c =
+// Geometry.Create.cuboid { X = 0.0; Y = 0.0 } 200.0