lythossettle 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ """
2
+ Lythos Settle — settlement analysis of shallow foundations, driven from a browser.
3
+
4
+ The program works out how much, and how fast, a shallow foundation settles on
5
+ a layered soil profile:
6
+
7
+ 1. Stresses — in-situ σ'v0 and σ'p; the stress increase beneath the
8
+ foundation by Boussinesq (rectangle, strip, circle) or 2:1
9
+ 2. Immediate — layered elastic (Steinbrenner) or Schmertmann (1978)
10
+ 3. Consolidation — primary settlement of the clay layers from Cc, Cr, e0
11
+ and σ'p, and secondary compression from Cα
12
+ 4. Time — Terzaghi's one-dimensional consolidation, per layer
13
+ 5. Checks — total settlement and angular distortion
14
+
15
+ On top of it, a parametric or reliability study sweeps any input (a range, or
16
+ a distribution) and reports sensitivities and the probability of exceeding
17
+ the allowable settlement.
18
+
19
+ The interface is a local web server driven from the browser (standard library
20
+ only), so the program also runs over a remote session or inside a container,
21
+ where a desktop toolkit would need a display it does not have.
22
+
23
+ Package layout
24
+ --------------
25
+ lythossettle.config app identity, defaults, themes, palette
26
+ lythossettle.i18n every text of the program, English and Turkish
27
+ lythossettle.stress Boussinesq, 2:1 and Steinbrenner solutions
28
+ lythossettle.consolidation Terzaghi time factor, compression of clay
29
+ lythossettle.engine the settlement analysis
30
+ lythossettle.study parametric / reliability studies
31
+ lythossettle.plotting analysis figures
32
+ lythossettle.study_plots study figures
33
+ lythossettle.report calculation report: HTML, PDF, DOCX
34
+ lythossettle.forms input schema and readers (interface-independent)
35
+ lythossettle.web local web server and the browser interface
36
+
37
+ Run it: lythos-settle (or python -m lythossettle)
38
+ """
39
+
40
+ __version__ = "0.1.0"
41
+
42
+ APP_NAME = "Lythos Settle"
43
+ ORG = "Lythos"
44
+
45
+ __all__ = ["__version__", "APP_NAME", "ORG"]
@@ -0,0 +1,6 @@
1
+ """`python -m lythossettle` opens the interface in a browser."""
2
+ import sys
3
+
4
+ from .cli import main
5
+
6
+ sys.exit(main())
lythossettle/cli.py ADDED
@@ -0,0 +1,137 @@
1
+ """Command line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+
9
+ from . import APP_NAME, __version__
10
+
11
+ #: Default port of the local interface (the family counts up from 8777)
12
+ PORT = 8780
13
+
14
+
15
+ def main(argv=None) -> int:
16
+ parser = argparse.ArgumentParser(
17
+ prog="lythos-settle",
18
+ description="Settlement analysis of shallow foundations: stress distribution, "
19
+ "immediate (elastic / Schmertmann), consolidation and secondary "
20
+ "settlement, consolidation time, and parametric / reliability studies.")
21
+ parser.add_argument("--version", action="version", version=f"{APP_NAME} {__version__}")
22
+ # Running the program with no subcommand means "web", so the top level
23
+ # carries that subcommand's defaults: without them the bare `lythos-settle`
24
+ # would reach serve() with a Namespace that has no host, port or language.
25
+ parser.set_defaults(host="127.0.0.1", port=PORT, lang="en", no_browser=False)
26
+ sub = parser.add_subparsers(dest="command")
27
+
28
+ web = sub.add_parser("web", help="start the interface in a browser")
29
+ web.add_argument("--port", type=int, default=PORT)
30
+ web.add_argument("--host", default="127.0.0.1")
31
+ web.add_argument("--lang", default="en", choices=["en", "tr"])
32
+ web.add_argument("--no-browser", action="store_true")
33
+
34
+ run = sub.add_parser("run", help="analyse a project file and print the results")
35
+ run.add_argument("project", help="path to a .settle / .json project file")
36
+ run.add_argument("-o", "--out", default=None,
37
+ help="write a report here (.pdf / .html / .docx)")
38
+ run.add_argument("--lang", default="en", choices=["en", "tr"])
39
+
40
+ study = sub.add_parser("study", help="run the study defined in a project file")
41
+ study.add_argument("project", help="path to a .settle / .json project file")
42
+ study.add_argument("-o", "--out", default=None, help="write the samples here (.csv / .xlsx)")
43
+ study.add_argument("--lang", default="en", choices=["en", "tr"])
44
+
45
+ example = sub.add_parser("example", help="write a starter project file")
46
+ example.add_argument("-o", "--out", default="project.settle")
47
+
48
+ args = parser.parse_args(argv)
49
+ command = args.command or "web"
50
+ try:
51
+ return _dispatch(command, args)
52
+ except (ValueError, RuntimeError, OSError) as exc:
53
+ # The analysis refuses impossible input with a sentence worth reading
54
+ # (a foundation below the profile, a layer without a modulus, an
55
+ # unreadable project file). A traceback would bury it, so only
56
+ # unexpected failures keep theirs.
57
+ print(f"{APP_NAME}: {exc}", file=sys.stderr)
58
+ return 1
59
+
60
+
61
+ def _example_study(values: dict) -> list:
62
+ """Two study variables for the starter project, so `study` has something to do."""
63
+ return [
64
+ {"path": "foundation.q", "label": "Foundation · q", "mode": "dist", "dist": "normal",
65
+ "mean": values["q"], "cov": 0.10, "min": 0, "max": 0, "n_points": 5},
66
+ {"path": "soil_profile.2.Cc", "label": "Soft clay · Cc", "mode": "dist",
67
+ "dist": "lognormal", "mean": values["soil_profile"][2]["Cc"], "cov": 0.25,
68
+ "min": 0, "max": 0, "n_points": 5},
69
+ ]
70
+
71
+
72
+ def _dispatch(command: str, args) -> int:
73
+ """Runs one command; raises on anything that goes wrong."""
74
+ if command == "web":
75
+ from .web.server import serve
76
+ serve(host=args.host, port=args.port, open_browser=not args.no_browser,
77
+ lang=args.lang)
78
+ return 0
79
+
80
+ from . import forms
81
+
82
+ if command == "example":
83
+ values = forms.defaults()
84
+ values["study_variables"] = _example_study(values)
85
+ with open(args.out, "w", encoding="utf-8") as fh:
86
+ json.dump(forms.project_file(values), fh, indent=2, ensure_ascii=False)
87
+ print(args.out)
88
+ return 0
89
+
90
+ with open(args.project, encoding="utf-8") as fh:
91
+ data = json.load(fh)
92
+
93
+ from .web.session import Session
94
+ session = Session(lang=args.lang)
95
+ values = session.load_project(data)["values"]
96
+
97
+ if command == "run":
98
+ result = session.analyse(values)
99
+ print(result["text"])
100
+ if args.out:
101
+ fmt = args.out.lower().rsplit(".", 1)[-1]
102
+ print(session.report(fmt if fmt in ("pdf", "html", "docx") else "pdf", args.out))
103
+ return 0
104
+
105
+ # A study: analyse the foundation first, so the report and the figures
106
+ # have something to sit beside, then sample.
107
+ session.analyse(values)
108
+ started = session.start_study(values)
109
+ if not started["ok"]:
110
+ print(started["error"], file=sys.stderr)
111
+ return 1
112
+ import time
113
+ last = -1
114
+ while session.state()["job"] == "running":
115
+ state = session.state()
116
+ if state["total"] and state["done"] != last:
117
+ last = state["done"]
118
+ print(f"\r{state['done']} / {state['total']}", end="", file=sys.stderr, flush=True)
119
+ time.sleep(0.2)
120
+ print("", file=sys.stderr)
121
+ state = session.state()
122
+ if state["job"] == "error":
123
+ print(state["error"], file=sys.stderr)
124
+ return 1
125
+ payload = session.study_payload()
126
+ if not payload["ok"]:
127
+ print(payload["error"], file=sys.stderr)
128
+ return 1
129
+ print(payload["text"])
130
+ if args.out:
131
+ kind = "xlsx" if args.out.lower().endswith(".xlsx") else "csv"
132
+ print(session.export_study(kind, args.out))
133
+ return 0
134
+
135
+
136
+ if __name__ == "__main__":
137
+ sys.exit(main())
lythossettle/config.py ADDED
@@ -0,0 +1,116 @@
1
+ """
2
+ Configuration for Lythos Settle: the default project, the choice lists, and
3
+ the theme and plot palette shared by the page and the figures.
4
+
5
+ The app name and version live in the package's ``__init__`` so that there is
6
+ one copy of each; the translations live in `lythossettle.i18n`.
7
+ """
8
+
9
+ from . import APP_NAME
10
+ from . import __version__ as APP_VERSION
11
+
12
+ __all__ = ["APP_NAME", "APP_VERSION", "DEFAULT_CONFIG", "THEMES", "PLOT_PALETTE",
13
+ "SOIL_FILL", "SHAPES", "STRESS_METHODS", "IMMEDIATE_METHODS", "RIGIDITY",
14
+ "BEHAVIOURS", "DRAINAGE", "ACCENT"]
15
+
16
+ MM_PER_M = 1000.0
17
+
18
+ # --- Choice lists (the first entry is the default where one is needed) -----
19
+ SHAPES = ["rectangle", "strip", "circle", "embankment"]
20
+ STRESS_METHODS = ["boussinesq", "two_to_one"]
21
+ IMMEDIATE_METHODS = ["elastic", "schmertmann"]
22
+ RIGIDITY = ["flexible", "rigid"]
23
+ BEHAVIOURS = ["granular", "cohesive"]
24
+ DRAINAGE = ["double", "single"]
25
+
26
+ # --- Interface theme (web/static/style.css) and plot palette ---------------
27
+ # --- kept together so the figures always match the page they are shown on. -
28
+ ACCENT = "#C6613F"
29
+
30
+ THEMES = {
31
+ "dark": dict(bg="#262624", panel="#30302E", input_bg="#262624", fg="#F5F4ED",
32
+ fg_dim="#A6A39A", border="#4A4944", hover="#3A3A37", btn="#3A3A37",
33
+ muted="#6B6A64", accent="#D97757", accent_hover="#E08B6E"),
34
+ "light": dict(bg="#F5F4ED", panel="#FAF9F5", input_bg="#FFFFFF", fg="#141413",
35
+ fg_dim="#73726C", border="#E3E0D5", hover="#F0EEE6", btn="#F0EEE6",
36
+ muted="#B7B4AA", accent=ACCENT, accent_hover="#B0532F"),
37
+ }
38
+ # The report's figures: the light palette on white paper.
39
+ THEMES["paper"] = dict(THEMES["light"], bg="#FFFFFF", panel="#FFFFFF")
40
+
41
+ # Semantic colours: the same quantity is the same colour in every figure. Warm and
42
+ # muted, to sit on the paper-coloured page; terracotta marks the total.
43
+ PLOT_PALETTE = dict(
44
+ immediate="#5B8DB8", consolidation="#8C6BB1", secondary="#D9A55B",
45
+ total="#C6613F", stress="#4E9A8A", overburden="#73726C", preconsolidation="#5E8C4A",
46
+ limit="#A26A12", water="#6FA8C7", footing="#8A8680",
47
+ center="#C6613F", char="#8C6BB1", edge="#D9A55B", corner="#4E9A8A",
48
+ shoulder="#8C6BB1", midslope="#D9A55B", toe="#4E9A8A", fill="#C9A876",
49
+ allowable="#B0413E",
50
+ )
51
+
52
+ # Fill colours of the two soil behaviours in the schematic (theme-dependent,
53
+ # since a light sandy tone reads poorly on a dark background).
54
+ SOIL_FILL = {
55
+ "light": {"granular": "#E3C98F", "cohesive": "#A7B8A0"},
56
+ "dark": {"granular": "#8A7250", "cohesive": "#5E6E58"},
57
+ }
58
+ SOIL_FILL["paper"] = SOIL_FILL["light"]
59
+
60
+ DEFAULT_CONFIG = {
61
+ "project_info": {
62
+ "title": "Project: Raft on soft clay",
63
+ "analyst": "",
64
+ },
65
+ "foundation": {
66
+ "shape": "rectangle",
67
+ "B": 8.0, # width, or diameter of a circle [m]
68
+ "L": 16.0, # length of a rectangle [m]
69
+ "Df": 1.5, # depth of the foundation base [m]
70
+ "q": 100.0, # applied (gross) bearing pressure [kPa]
71
+ "net_pressure": True, # deduct the overburden removed by the excavation
72
+ },
73
+ # An embankment (shape "embankment"): a long trapezoidal fill on the ground
74
+ # surface, loading it with γ·H under the crest and less under the slopes.
75
+ "embankment": {
76
+ "crest": 12.0, # crest width [m]
77
+ "height": 4.0, # [m]
78
+ "slope_left": 26.57, # slope angles from the horizontal [°] (1V:2H)
79
+ "slope_right": 26.57,
80
+ "gamma": 20.0, # unit weight of the fill [kN/m³]
81
+ },
82
+ "groundwater": {
83
+ "depth": 2.0, # below ground surface [m]
84
+ "gamma_water": 9.81, # [kN/m³]
85
+ },
86
+ # Layers from the ground surface down. E [MPa] is the drained modulus of
87
+ # a granular layer and the undrained modulus of a cohesive one; cv in
88
+ # m²/year; OCR = σ'p / σ'v0.
89
+ "soil_profile": [
90
+ {"name": "Fill", "thickness": 1.5, "behaviour": "granular", "gamma": 18.0,
91
+ "gamma_sat": 19.0, "E": 10.0, "nu": 0.30, "Cc": 0.0, "Cr": 0.0, "e0": 0.6,
92
+ "OCR": 1.0, "cv": 0.0, "Calpha": 0.0, "drainage": "double"},
93
+ {"name": "Medium dense sand", "thickness": 3.5, "behaviour": "granular",
94
+ "gamma": 18.5, "gamma_sat": 20.0, "E": 25.0, "nu": 0.30, "Cc": 0.0, "Cr": 0.0,
95
+ "e0": 0.6, "OCR": 1.0, "cv": 0.0, "Calpha": 0.0, "drainage": "double"},
96
+ {"name": "Soft clay", "thickness": 6.0, "behaviour": "cohesive", "gamma": 17.0,
97
+ "gamma_sat": 17.5, "E": 6.0, "nu": 0.50, "Cc": 0.32, "Cr": 0.05, "e0": 1.05,
98
+ "OCR": 1.3, "cv": 1.5, "Calpha": 0.010, "drainage": "double"},
99
+ {"name": "Dense sand", "thickness": 10.0, "behaviour": "granular", "gamma": 19.5,
100
+ "gamma_sat": 21.0, "E": 60.0, "nu": 0.30, "Cc": 0.0, "Cr": 0.0, "e0": 0.5,
101
+ "OCR": 1.0, "cv": 0.0, "Calpha": 0.0, "drainage": "double"},
102
+ ],
103
+ "options": {
104
+ "stress_method": "boussinesq",
105
+ "immediate_method": "elastic",
106
+ "rigidity": "flexible",
107
+ "sublayer": 0.25, # thickness of the calculation sublayers [m]
108
+ "depth_ratio": 0.10, # influence depth where Δσ = ratio · σ'v0 (0: whole profile)
109
+ "design_life": 50.0, # years, for secondary compression and creep
110
+ "creep": True, # Schmertmann's time factor C2
111
+ },
112
+ "criteria": {
113
+ "s_allow": 150.0, # allowable total settlement [mm] (0: no check)
114
+ "distortion_allow": 500.0, # allowable angular distortion 1 / x (0: no check)
115
+ },
116
+ }
@@ -0,0 +1,100 @@
1
+ """
2
+ One-dimensional consolidation: Terzaghi's theory, and the settlement of a
3
+ clay sublayer from its compression indices.
4
+
5
+ Time
6
+ U(Tv) is the average degree of consolidation for a uniform initial excess
7
+ pore pressure: the Fourier series, with Terzaghi's √(4Tv/π) where the
8
+ series would need too many terms. Tv(U) is its inverse, found by
9
+ bisection so that U(Tv(U)) = U to the precision of the series, rather
10
+ than by the usual two-branch approximation.
11
+
12
+ Magnitude
13
+ Δe from Cc and Cr about the preconsolidation pressure σ'p: the
14
+ recompression line up to σ'p and the virgin line beyond it.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import numpy as np
20
+
21
+ #: Terms of the Fourier series (plenty for Tv ≥ 1e-3)
22
+ TERMS = 200
23
+
24
+ #: Below this Tv the series is replaced by √(4Tv/π) (identical there to 1e-9)
25
+ TV_SMALL = 1e-3
26
+
27
+ #: Degree of consolidation taken as the end of primary consolidation, from
28
+ #: which secondary compression is counted
29
+ U_END_OF_PRIMARY = 0.95
30
+
31
+
32
+ def degree(Tv) -> np.ndarray:
33
+ """Average degree of consolidation U for time factor(s) Tv."""
34
+ Tv = np.atleast_1d(np.asarray(Tv, dtype=float))
35
+ Tv = np.maximum(Tv, 0.0)
36
+ m = np.arange(TERMS)
37
+ M = np.pi * (2 * m + 1) / 2.0
38
+ series = 1.0 - np.sum(2.0 / M ** 2 * np.exp(-np.outer(np.maximum(Tv, TV_SMALL), M ** 2)),
39
+ axis=1)
40
+ small = np.sqrt(4.0 * Tv / np.pi)
41
+ return np.clip(np.where(Tv < TV_SMALL, small, series), 0.0, 1.0)
42
+
43
+
44
+ def time_factor(U: float) -> float:
45
+ """Time factor Tv at which the average degree of consolidation is U."""
46
+ if not 0.0 < U < 1.0:
47
+ raise ValueError("the degree of consolidation must be between 0 and 1")
48
+ lo, hi = 0.0, 10.0
49
+ for _ in range(80):
50
+ mid = 0.5 * (lo + hi)
51
+ if degree(mid)[0] < U:
52
+ lo = mid
53
+ else:
54
+ hi = mid
55
+ return 0.5 * (lo + hi)
56
+
57
+
58
+ def drainage_path(thickness: float, drainage: str) -> float:
59
+ """Longest drainage path: half the layer when both faces drain."""
60
+ return thickness / 2.0 if drainage == "double" else thickness
61
+
62
+
63
+ def time_for(U: float, cv: float, h_dr: float) -> float:
64
+ """Time (in the units of cv) to reach degree of consolidation U."""
65
+ if cv <= 0:
66
+ return float("nan")
67
+ return time_factor(U) * h_dr * h_dr / cv
68
+
69
+
70
+ def degree_at(t, cv: float, h_dr: float) -> np.ndarray:
71
+ """U at time(s) t for a layer with coefficient cv and drainage path h_dr.
72
+
73
+ A layer without cv is taken as consolidating at once (U = 1)."""
74
+ t = np.atleast_1d(np.asarray(t, dtype=float))
75
+ if cv <= 0 or h_dr <= 0:
76
+ return np.ones_like(t)
77
+ return degree(cv * t / (h_dr * h_dr))
78
+
79
+
80
+ def primary_strain(sigma0, dsigma, sigma_p, Cc: float, Cr: float, e0: float) -> np.ndarray:
81
+ """Vertical strain Δe / (1 + e0) of clay loaded from σ'0 by Δσ.
82
+
83
+ σ'0 ≥ σ'p is normally consolidated; below it the recompression line is
84
+ followed up to σ'p (or the final stress, whichever is lower) and the
85
+ virgin line beyond.
86
+ """
87
+ s0 = np.maximum(np.atleast_1d(np.asarray(sigma0, dtype=float)), 1e-6)
88
+ sf = np.maximum(s0 + np.maximum(np.asarray(dsigma, dtype=float), 0.0), s0)
89
+ sp = np.maximum(np.asarray(sigma_p, dtype=float), s0)
90
+ recompression = Cr * np.log10(np.minimum(sf, sp) / s0)
91
+ virgin = Cc * np.log10(np.maximum(sf, sp) / sp)
92
+ return (recompression + virgin) / (1.0 + e0)
93
+
94
+
95
+ def secondary_strain(t, t_p: float, Calpha: float, e0: float) -> np.ndarray:
96
+ """Secondary compression strain Cα/(1 + e0)·log10(t / t_p), zero before t_p."""
97
+ t = np.atleast_1d(np.asarray(t, dtype=float))
98
+ if Calpha <= 0 or not np.isfinite(t_p) or t_p <= 0:
99
+ return np.zeros_like(t)
100
+ return Calpha / (1.0 + e0) * np.log10(np.maximum(t / t_p, 1.0))