springcalc 0.1.0__tar.gz
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.
- springcalc-0.1.0/.claude/settings.local.json +9 -0
- springcalc-0.1.0/.gitignore +18 -0
- springcalc-0.1.0/.python-version +1 -0
- springcalc-0.1.0/.vscode/launch.json +17 -0
- springcalc-0.1.0/LICENSE +21 -0
- springcalc-0.1.0/PKG-INFO +517 -0
- springcalc-0.1.0/README.md +478 -0
- springcalc-0.1.0/docs/Spring calculation all types.xlsm +0 -0
- springcalc-0.1.0/pyproject.toml +69 -0
- springcalc-0.1.0/scripts/regresiones/factor_f/factor__f_vs_RMa.csv +30 -0
- springcalc-0.1.0/scripts/regresiones/factor_f/factor_f.py +74 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/Wahl factor vs index.csv +53 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/Wahl_factor torsion.jpg +0 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/factor_wahl.py +64 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/factor_wahl2.py +123 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/modelo_factor_f.pkl +0 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/parametros_popt.pkl +0 -0
- springcalc-0.1.0/scripts/regresiones/wahl_factor/usar_modelo_factor_f.py +71 -0
- springcalc-0.1.0/src/springcalc/__init__.py +58 -0
- springcalc-0.1.0/src/springcalc/lineal/__init__.py +0 -0
- springcalc-0.1.0/src/springcalc/lineal/animation.py +84 -0
- springcalc-0.1.0/src/springcalc/lineal/compresion.py +540 -0
- springcalc-0.1.0/src/springcalc/lineal/constants.py +28 -0
- springcalc-0.1.0/src/springcalc/lineal/extension.py +579 -0
- springcalc-0.1.0/src/springcalc/lineal/generic_compression.py +472 -0
- springcalc-0.1.0/src/springcalc/lineal/generic_lineal.py +408 -0
- springcalc-0.1.0/src/springcalc/lineal/goodman.py +262 -0
- springcalc-0.1.0/src/springcalc/lineal/lineal.py +225 -0
- springcalc-0.1.0/src/springcalc/lineal/plotting.py +39 -0
- springcalc-0.1.0/src/springcalc/lineal/torsion.py +694 -0
- springcalc-0.1.0/src/springcalc/material/DH_RMa.csv +90 -0
- springcalc-0.1.0/src/springcalc/material/DIAMETRO_TOLERANCIAS.csv +90 -0
- springcalc-0.1.0/src/springcalc/material/DM_RMa.csv +75 -0
- springcalc-0.1.0/src/springcalc/material/SH_RMa.csv +75 -0
- springcalc-0.1.0/src/springcalc/material/SL_RMa.csv +42 -0
- springcalc-0.1.0/src/springcalc/material/SM_RMa.csv +75 -0
- springcalc-0.1.0/src/springcalc/material/__init__.py +0 -0
- springcalc-0.1.0/src/springcalc/material/materials.csv +22 -0
- springcalc-0.1.0/src/springcalc/plots/__init__.py +3 -0
- springcalc-0.1.0/src/springcalc/plots/goodman_diagram.py +57 -0
- springcalc-0.1.0/src/springcalc/pymodels/__init__.py +0 -0
- springcalc-0.1.0/src/springcalc/pymodels/material.py +210 -0
- springcalc-0.1.0/src/springcalc/pymodels/positions.py +260 -0
- springcalc-0.1.0/src/springcalc/pymodels/units.py +3 -0
- springcalc-0.1.0/src/springcalc/pymodels/wire_characteristics.py +149 -0
- springcalc-0.1.0/src/springcalc/regresiones/__init__.py +0 -0
- springcalc-0.1.0/src/springcalc/regresiones/factor_f/__init__.py +0 -0
- springcalc-0.1.0/src/springcalc/regresiones/factor_f/factor_f_coeffs.json +10 -0
- springcalc-0.1.0/src/springcalc/regresiones/factor_f/usar_modelo_factor_f.py +73 -0
- springcalc-0.1.0/src/springcalc/report/__init__.py +3 -0
- springcalc-0.1.0/src/springcalc/report/pdf_report.py +240 -0
- springcalc-0.1.0/tests/__init__.py +0 -0
- springcalc-0.1.0/tests/test_diameter_pitch_cuadratic_variable.py +106 -0
- springcalc-0.1.0/tests/test_diameter_pitch_linear_variable copy.py +90 -0
- springcalc-0.1.0/tests/test_diameter_variable.py +134 -0
- springcalc-0.1.0/tests/test_factor_f.py +49 -0
- springcalc-0.1.0/tests/test_goodman.py +151 -0
- springcalc-0.1.0/tests/test_lineal.py +46 -0
- springcalc-0.1.0/tests/test_lineal_compression.py +59 -0
- springcalc-0.1.0/tests/test_lineal_extension.py +85 -0
- springcalc-0.1.0/tests/test_material.py +61 -0
- springcalc-0.1.0/tests/test_pdf_report.py +62 -0
- springcalc-0.1.0/tests/test_pitch_variable.py +185 -0
- springcalc-0.1.0/tests/test_progressive_animation.py +78 -0
- springcalc-0.1.0/tests/test_set_geometry.py +42 -0
- springcalc-0.1.0/tests/test_torsion.py +32 -0
- springcalc-0.1.0/tests/test_wahl_factor_category.py +24 -0
- springcalc-0.1.0/uv.lock +1743 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Use IntelliSense to learn about possible attributes.
|
|
3
|
+
// Hover to view descriptions of existing attributes.
|
|
4
|
+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
5
|
+
"version": "0.2.0",
|
|
6
|
+
"configurations": [
|
|
7
|
+
{
|
|
8
|
+
"name": "Python Debugger: Current File",
|
|
9
|
+
"type": "debugpy",
|
|
10
|
+
"request": "launch",
|
|
11
|
+
"python": "${workspaceFolder}/.venv/bin/python",
|
|
12
|
+
"program": "./tests/test_generic_compression.py",
|
|
13
|
+
"console": "integratedTerminal",
|
|
14
|
+
"stopOnEntry": true
|
|
15
|
+
},
|
|
16
|
+
]
|
|
17
|
+
}
|
springcalc-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ernesto Avedillo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: springcalc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Spring calculation library: compression, extension, and torsion.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ErnestoAvedillo/pysprings
|
|
6
|
+
Project-URL: Repository, https://github.com/ErnestoAvedillo/pysprings
|
|
7
|
+
Project-URL: Issues, https://github.com/ErnestoAvedillo/pysprings/issues
|
|
8
|
+
Author-email: Ernesto Avedillo <eavedillo@yahoo.es>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: fatiga,goodman,ingeniería,muelles,resortes,shigley,springs
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: build>=1.5.0
|
|
21
|
+
Requires-Dist: matplotlib>=3.11.1
|
|
22
|
+
Requires-Dist: numpy>=2.0
|
|
23
|
+
Requires-Dist: pandas>=2.2
|
|
24
|
+
Requires-Dist: pint>=0.24
|
|
25
|
+
Requires-Dist: pydantic>=2.13.4
|
|
26
|
+
Requires-Dist: reportlab>=4.0
|
|
27
|
+
Requires-Dist: scipy>=1.14
|
|
28
|
+
Requires-Dist: twine>=6.2.0
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: flake8-pyproject>=1.2; extra == 'dev'
|
|
31
|
+
Requires-Dist: flake8>=7.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Provides-Extra: publish
|
|
34
|
+
Requires-Dist: build>=1.5.0; extra == 'publish'
|
|
35
|
+
Requires-Dist: twine>=6.2.0; extra == 'publish'
|
|
36
|
+
Provides-Extra: training
|
|
37
|
+
Requires-Dist: scikit-learn>=1.5; extra == 'training'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# springcalc
|
|
41
|
+
|
|
42
|
+
A Python library for **spring calculations**: compression, extension, and
|
|
43
|
+
torsion springs. Includes material data, wire characteristic calculations,
|
|
44
|
+
and fatigue analysis via the Goodman diagram.
|
|
45
|
+
|
|
46
|
+
Visit the library in my guithub https://github.com/ErnestoAvedillo/springcalc and clone my repository using:
|
|
47
|
+
|
|
48
|
+
git clone git@github.com:ErnestoAvedillo/springcalc.git
|
|
49
|
+
|
|
50
|
+
## Structure
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
src/springcalc/ Library package
|
|
54
|
+
├── lineal/ Calculation engine (compression, extension, torsion, Goodman,
|
|
55
|
+
│ 3D visualization, progressive-compression animation)
|
|
56
|
+
├── pymodels/ Data models (pydantic): material, units, wire, positions
|
|
57
|
+
├── material/ Material and tolerance tables (CSV, package data)
|
|
58
|
+
├── regresiones/ Fitted models loaded at runtime
|
|
59
|
+
│ └── factor_f/ Shigley's factor f (plain JSON coefficients + loader)
|
|
60
|
+
├── plots/ Goodman diagram generation
|
|
61
|
+
└── report/ PDF report generation (SpringPDFReport)
|
|
62
|
+
|
|
63
|
+
tests/ Tests (pytest)
|
|
64
|
+
scripts/regresiones/ Training scripts that regenerate the JSON coefficients (not runtime)
|
|
65
|
+
docs/ Reference material (spreadsheet, figures)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
I recommend to use uv to install the library (https://docs.astral.sh/uv/):
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# create the environment and install dependencies
|
|
74
|
+
uv init
|
|
75
|
+
uv add springcalc
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Usage
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from springcalc import Material, CompressionSpring
|
|
82
|
+
|
|
83
|
+
material = Material(material_name="SH")
|
|
84
|
+
spring = CompressionSpring(material=material, wire_diameter=1.0)
|
|
85
|
+
spring.set_geometry(outer_diameter=10.0, free_length=50.0, nr_coils=10)
|
|
86
|
+
properties = compression_spring.get_spring_data()
|
|
87
|
+
for key, value in properties.items():
|
|
88
|
+
print(f"{key}: {value}") )
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Generating a PDF report (spring data, load/travel/diameter curves, and the
|
|
92
|
+
Goodman fatigue diagram):
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from springcalc.report import SpringPDFReport
|
|
96
|
+
|
|
97
|
+
report = SpringPDFReport(spring, title="Spring XYZ-123")
|
|
98
|
+
report.build("spring_report.pdf")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## API Reference
|
|
102
|
+
|
|
103
|
+
- [Material data](#material-data) — `Material`, `get_available_materials()`
|
|
104
|
+
- [Wire characteristics](#wire-characteristics) — `WireCharacteristics`
|
|
105
|
+
- [Compression springs](#compression-springs) — `CompressionSpring`
|
|
106
|
+
- [Extension springs](#extension-springs) — `ExtensionSpring`
|
|
107
|
+
- [Torsion springs](#torsion-springs) — `TorsionSpring`
|
|
108
|
+
- [Fatigue analysis (Goodman diagram)](#fatigue-analysis-goodman-diagram) — `GoodmanData`, `GoodmanAnalyzer`, `Goodman`, `generate_goodman_diagram()`
|
|
109
|
+
- [Position tables](#position-tables) — `LinearPositionsTable`, `AngularPositionsTable`
|
|
110
|
+
- [PDF reports](#pdf-reports) — `SpringPDFReport`
|
|
111
|
+
- [Advanced: variable-geometry springs](#advanced-variable-geometry-springs) — `VariableLinealSpring`, `CompressionSpringGeneral`
|
|
112
|
+
- [Animating progressive compression](#animating-progressive-compression) — `CompressionAnimator`
|
|
113
|
+
|
|
114
|
+
All physical quantities are [`pint`](https://pint.readthedocs.io/) `Quantity`
|
|
115
|
+
objects (a number with a unit, e.g. `20.0 millimeter`). Plain numbers passed
|
|
116
|
+
into a field are usually interpreted in that field's default unit (mm, N,
|
|
117
|
+
degrees...), but a few methods require an explicit `Quantity` — those are
|
|
118
|
+
called out below. Import the shared unit registry with:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from springcalc.pymodels.units import ureg
|
|
122
|
+
|
|
123
|
+
length = 20 * ureg.mm
|
|
124
|
+
angle = 90 * ureg.degree
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Material data
|
|
128
|
+
|
|
129
|
+
`Material` (`springcalc.pymodels.material.Material`) is a pydantic model that
|
|
130
|
+
looks up a named material's mechanical properties from `material/materials.csv`
|
|
131
|
+
and auto-fills any field you don't pass explicitly.
|
|
132
|
+
|
|
133
|
+
| Member | Description |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `Material(material_name, young_modulus=None, shear_modulus=None, elastic_limit_factor=None, poisson_coef=None, RMa_file=None)` | Construct a material by name; unset fields are auto-filled from `materials.csv`. |
|
|
136
|
+
| `.young_modulus` | Young's modulus E, as a `Quantity` in MPa. |
|
|
137
|
+
| `.shear_modulus` | Shear modulus G, as a `Quantity` in MPa. |
|
|
138
|
+
| `.elastic_limit_factor` | Dimensionless factor used to derive the fatigue limit. |
|
|
139
|
+
| `.poisson_coef` | Poisson's ratio (dimensionless). |
|
|
140
|
+
| `.RMa_file` | Name of the CSV (in `material/`) with tensile-strength ranges by wire diameter. |
|
|
141
|
+
| `get_available_materials()` | Module-level function: list of valid `material_name` values. |
|
|
142
|
+
| `Material.material_exists(material_name)` | Classmethod: whether a material is already registered. |
|
|
143
|
+
| `Material.create_material(material_name, young_modulus, shear_modulus, elastic_limit_factor, poisson_coef, description="", RMa_file=None, RMa_data=None, overwrite=False)` | Classmethod: register a new material in `materials.csv` (and optionally its `RMa_file` table) and return it. Raises `ValueError` if the name already exists, unless `overwrite=True`. |
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from springcalc import Material, get_available_materials
|
|
147
|
+
|
|
148
|
+
print(get_available_materials())
|
|
149
|
+
# ['SL', 'SM', 'DM', 'SH', 'DH', 'TDC', 'TDCrV', ...]
|
|
150
|
+
|
|
151
|
+
material = Material(material_name="SH")
|
|
152
|
+
print(material.young_modulus) # 206000.0 megapascal
|
|
153
|
+
print(material.shear_modulus) # 81500.0 megapascal
|
|
154
|
+
print(material.poisson_coef) # 0.3 dimensionless
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Adding a new material — `young_modulus`/`shear_modulus` accept a unit string, a
|
|
158
|
+
plain number (assumed MPa), or a `Quantity`; `elastic_limit_factor` and
|
|
159
|
+
`poisson_coef` are dimensionless. Once created, the material is loadable by
|
|
160
|
+
name like any built-in one:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from springcalc import Material
|
|
164
|
+
|
|
165
|
+
custom = Material.create_material(
|
|
166
|
+
material_name="CustomSteel",
|
|
167
|
+
young_modulus="210000 MPa",
|
|
168
|
+
shear_modulus=81000,
|
|
169
|
+
elastic_limit_factor=0.5,
|
|
170
|
+
poisson_coef=0.3,
|
|
171
|
+
description="Custom steel for a special order",
|
|
172
|
+
RMa_data=[(1.0, 1600, 1800), (2.0, 1500, 1700)], # (diameter mm, RMa_min, RMa_max)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# From now on it behaves like any other material
|
|
176
|
+
same_material = Material(material_name="CustomSteel")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Wire characteristics
|
|
180
|
+
|
|
181
|
+
`WireCharacteristics` (`springcalc.pymodels.wire_characteristics.WireCharacteristics`)
|
|
182
|
+
is the base class of every spring type. Given a material and a wire diameter,
|
|
183
|
+
it looks up the wire diameter tolerance and the tensile-strength range (RMa)
|
|
184
|
+
for that diameter.
|
|
185
|
+
|
|
186
|
+
| Member | Description |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `WireCharacteristics(material, wire_diameter)` | `wire_diameter` may be a plain number (interpreted in mm) or a `Quantity`. |
|
|
189
|
+
| `.diameter_tolerance` | Manufacturing tolerance for this diameter (from `DIAMETRO_TOLERANCIAS.csv`). |
|
|
190
|
+
| `.RMa_min` / `.RMa_max` | Tensile-strength range for this material and diameter, from `material.RMa_file`. |
|
|
191
|
+
| `.set_material(material, wire_diameter)` | Re-assign the material/diameter and refresh the derived fields. |
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from springcalc import Material
|
|
195
|
+
from springcalc.pymodels.wire_characteristics import WireCharacteristics
|
|
196
|
+
|
|
197
|
+
material = Material(material_name="SH")
|
|
198
|
+
wire = WireCharacteristics(material=material, wire_diameter=2.0)
|
|
199
|
+
print(wire.diameter_tolerance) # 0.025
|
|
200
|
+
print(wire.RMa_min, wire.RMa_max) # 1980.0 2200.0
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Compression springs
|
|
204
|
+
|
|
205
|
+
`CompressionSpring` (`springcalc.lineal.compresion.CompressionSpring`, also
|
|
206
|
+
exported as `springcalc.CompressionSpring`) is the main entry point for
|
|
207
|
+
helical compression springs. It extends `LinealSpring`
|
|
208
|
+
(`springcalc.lineal.lineal.LinealSpring`), the shared calculation engine also
|
|
209
|
+
used by `ExtensionSpring`.
|
|
210
|
+
|
|
211
|
+
| Method | Description |
|
|
212
|
+
|---|---|
|
|
213
|
+
| `CompressionSpring(material, wire_diameter, **data)` | Create the spring. |
|
|
214
|
+
| `.set_geometry(mean_diameter=None, outer_diameter=None, inner_diameter=None, nr_coils=None, pitch=None, free_length=None, type_of_end=None, type_conforming=None)` | Set the full geometry in one call: exactly one diameter and exactly two of `nr_coils`/`pitch`/`free_length`. `type_of_end` (one of `constants.COMPRESSION_SPRING_END_TYPES`, e.g. `"open_ground"`) and `type_conforming` (one of `constants.FORMING_TYPES`, e.g. `"cold_formed"`) are optional and, if given, override the spring's defaults before the active-coil count is computed. Equivalent to calling `.set_diameter()` followed by `.calculate_spring_properties()`. Returns `.get_spring_data()`. |
|
|
215
|
+
| `.set_diameter(mean_diameter=None, outer_diameter=None, inner_diameter=None)` | Set exactly one of the three diameters; derives the others and the spring index/Wahl factor. |
|
|
216
|
+
| `.calculate_spring_properties(nr_coils=None, pitch=None, free_length=None)` | Provide exactly two of the three; computes coils, active coils, Wahl factor, spring constant, solid length, and wire length. |
|
|
217
|
+
| `.add_load_position(length)` | Record the load/stress/outer-diameter at a given compressed length, for the load-position table and fatigue analysis. |
|
|
218
|
+
| `.empty_tables()` | Clear the recorded load positions. |
|
|
219
|
+
| `.get_spring_data()` | `dict` with all computed spring properties (material, diameters, constants, Wahl factor, etc.). |
|
|
220
|
+
| `.get_data_positions()` / `.get_data_travels()` | List of `LinearLoadPosition` recorded via `add_load_position`. |
|
|
221
|
+
| `.get_forces_vs_position_graph(show=False)` | Load vs. absolute position curve; returns a base64-encoded PNG. |
|
|
222
|
+
| `.get_forces_vs_travel_graph(show=False)` | Load vs. travel (compression from free length) curve; returns a base64 PNG. |
|
|
223
|
+
| `.get_diameter_graph(show=False)` | Outer diameter vs. position curve; returns a base64 PNG. |
|
|
224
|
+
| `.get_diameter_vs_position_graph(show=False)` | Outer diameter curve plus a to-scale cross-section diagram; returns a base64 PNG. |
|
|
225
|
+
| `.get_3d_plot(num_points=200, show=False, isometric=True)` | Renders the coiled wire geometry in 3D (constant mean diameter and pitch); returns a base64 PNG. Defaults to an orthographic isometric view, matching how spring drawings are conventionally presented. |
|
|
226
|
+
| `.create_goodman_diagram(show=False)` | Runs the fatigue (Goodman) analysis from the recorded positions; returns `{"image", "analysis", "stresses"}` or `{"error", "traceback"}`. |
|
|
227
|
+
| `.get_stress_max()` / `.get_stress_min()` | Max/min stress across recorded positions (raises if none are recorded). |
|
|
228
|
+
| `.get_load_max()` / `.get_load_min()` | Max/min load across recorded positions. |
|
|
229
|
+
| `.calculate_solid_length()` | Coils-stacked solid (fully compressed) length. |
|
|
230
|
+
| `.calculate_wire_length()` | Total wire length needed to wind the spring. |
|
|
231
|
+
| `.set_number_cycles(number_cycles)` | Design life, in cycles, used by the fatigue analysis (default 1e6). |
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
from springcalc import Material, CompressionSpring
|
|
235
|
+
|
|
236
|
+
material = Material(material_name="SL")
|
|
237
|
+
spring = CompressionSpring(material=material, wire_diameter=2.5)
|
|
238
|
+
spring.set_geometry(outer_diameter=30, pitch=20, free_length=100) # mm
|
|
239
|
+
|
|
240
|
+
# Equivalent to calling separately:
|
|
241
|
+
# spring.set_diameter(outer_diameter=30)
|
|
242
|
+
# spring.calculate_spring_properties(pitch=20, free_length=100)
|
|
243
|
+
|
|
244
|
+
for length_mm in [30, 40, 50, 60, 70, 80, 90, 100]:
|
|
245
|
+
spring.add_load_position(length=length_mm)
|
|
246
|
+
|
|
247
|
+
data = spring.get_spring_data()
|
|
248
|
+
print(data["spring_constant"]) # ~7.09 N / mm
|
|
249
|
+
print(data["wahl_factor_category"]) # 'green' -> C=11 is in a normal manufacturable range
|
|
250
|
+
|
|
251
|
+
# Graphs (base64 PNGs, ready to embed in HTML or a PDF)
|
|
252
|
+
load_vs_position_png = spring.get_forces_vs_position_graph()
|
|
253
|
+
diameter_png = spring.get_diameter_vs_position_graph()
|
|
254
|
+
|
|
255
|
+
# Fatigue analysis from the recorded positions
|
|
256
|
+
result = spring.create_goodman_diagram()
|
|
257
|
+
print(result["analysis"]["safety_factor"])
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Extension springs
|
|
261
|
+
|
|
262
|
+
`ExtensionSpring` (`springcalc.lineal.extension.ExtensionSpring`, also exported
|
|
263
|
+
as `springcalc.ExtensionSpring`) models helical extension springs. It extends
|
|
264
|
+
`LinealSpring` directly (not `CompressionSpring`) and its diameter/length
|
|
265
|
+
setters require explicit `Quantity` values rather than plain numbers.
|
|
266
|
+
|
|
267
|
+
| Method | Description |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `ExtensionSpring(material, wire_diameter, **data)` | Create the spring. |
|
|
270
|
+
| `.set_diameter(outer_diameter=None, inner_diameter=None, mean_diameter=None)` | Set exactly one diameter. **Must be a `Quantity`** (e.g. `15 * ureg.mm`), not a plain number. |
|
|
271
|
+
| `.calculate_spring_properties(nr_coils=None, pitch=None, free_length=None)` | Provide exactly two of the three; computes active coils, spring constant, and wire length. |
|
|
272
|
+
| `.add_load_position(length)` | Record the load/stress/outer-diameter at a given extended length (must be ≥ free length). |
|
|
273
|
+
| `.calculate_positions_table(step: list)` | Convenience: call `add_load_position` for each length in `step`. |
|
|
274
|
+
| `.empty_tables()` | Clear the recorded load positions. |
|
|
275
|
+
| `.get_spring_data()` | `dict` with all computed spring properties. |
|
|
276
|
+
| `.get_data_positions()` / `.get_data_travels()` | List of recorded `LinearLoadPosition`. |
|
|
277
|
+
| `.get_forces_vs_position_graph(show=False)` / `.get_forces_vs_travel_graph(show=False)` | Load curves; return base64 PNGs. |
|
|
278
|
+
| `.get_diameter_graph()` / `.get_diameter_vs_position_graph()` | Diameter curves (no `show` parameter on this class); return base64 PNGs. |
|
|
279
|
+
| `.create_goodman_diagram()` | Fatigue analysis from the recorded positions; returns `{"image", "analysis", "stresses"}` or `{"error", "traceback"}` (no `show` parameter). |
|
|
280
|
+
| `.get_stress_max()` / `.get_stress_min()` / `.get_load_max()` / `.get_load_min()` | Extremes across recorded positions. |
|
|
281
|
+
| `.set_number_cycles(number_cycles)` | Design life in cycles for the fatigue analysis. |
|
|
282
|
+
| `.set_initial_stress(initial_stress)` | Set the spring's initial tension (pre-load) stress. |
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
from springcalc import Material, ExtensionSpring
|
|
286
|
+
from springcalc.pymodels.units import ureg
|
|
287
|
+
|
|
288
|
+
material = Material(material_name="SH")
|
|
289
|
+
spring = ExtensionSpring(material=material, wire_diameter=1.5)
|
|
290
|
+
spring.set_diameter(outer_diameter=15 * ureg.mm) # note: needs a Quantity, unlike CompressionSpring
|
|
291
|
+
spring.calculate_spring_properties(nr_coils=10, free_length=60)
|
|
292
|
+
|
|
293
|
+
spring.calculate_positions_table([65, 70, 75, 80]) # extend beyond the free length
|
|
294
|
+
|
|
295
|
+
data = spring.get_spring_data()
|
|
296
|
+
print(data["spring_constant"]) # ~2.33 N / mm
|
|
297
|
+
|
|
298
|
+
result = spring.create_goodman_diagram()
|
|
299
|
+
print(result["analysis"]["safety_factor"])
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Torsion springs
|
|
303
|
+
|
|
304
|
+
`TorsionSpring` (`springcalc.lineal.torsion.TorsionSpring`, also exported as
|
|
305
|
+
`springcalc.TorsionSpring`) models helical torsion springs, tracking angular
|
|
306
|
+
position/travel and torque instead of linear load. It extends
|
|
307
|
+
`WireCharacteristics` directly and has no Goodman/fatigue integration.
|
|
308
|
+
|
|
309
|
+
| Method | Description |
|
|
310
|
+
|---|---|
|
|
311
|
+
| `TorsionSpring(material, wire_diameter, **data)` | Create the spring. |
|
|
312
|
+
| `.set_geometry(mean_diameter, nr_coils, pitch, free_angle, fixed_leg_radius, mobile_leg_radius)` | One-call setup: sets geometry and computes every derived property. Returns `.get_spring_properties()`. |
|
|
313
|
+
| `.calculate_spring_properties()` | Re-run the derived-property calculations after changing an input. |
|
|
314
|
+
| `.add_position(angle_travel=None, torque=None)` | Record a working position from either an angular travel or a torque (exactly one). |
|
|
315
|
+
| `.clean_positions()` | Clear the recorded positions. |
|
|
316
|
+
| `.get_positions()` / `.get_data_positions()` / `.get_data_travels()` | List of recorded `AngularLoadPosition`. |
|
|
317
|
+
| `.get_spring_properties()` | `dict` with all computed properties (diameters, angles, leg lengths, spring constant, Wahl factor, etc.). |
|
|
318
|
+
| `.calculate_torque(rotation_angle)` | Torque required for a given rotation angle. |
|
|
319
|
+
| `.calculate_stress(torque)` | Max wire stress for a given torque. |
|
|
320
|
+
| `.get_forces_vs_position_graph(show=False)` / `.get_forces_vs_travel_graph(show=False)` | Torque vs. angular position/travel curves; return base64 PNGs. |
|
|
321
|
+
| `.get_diameter_vs_position_graph(show=False)` | Outer diameter curve plus a cross-section diagram; returns a base64 PNG. |
|
|
322
|
+
| `.set_number_cycles(number_cycles)` / `.set_shot_peening(shot_peening)` | Fatigue-related inputs (stored but not yet used by a Goodman analysis for this class). |
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
from springcalc import Material, TorsionSpring
|
|
326
|
+
from springcalc.pymodels.units import ureg
|
|
327
|
+
|
|
328
|
+
material = Material(material_name="SH")
|
|
329
|
+
spring = TorsionSpring(material=material, wire_diameter=1.0)
|
|
330
|
+
spring.set_geometry(
|
|
331
|
+
mean_diameter=10 * ureg.mm,
|
|
332
|
+
nr_coils=8,
|
|
333
|
+
pitch=1.2 * ureg.mm,
|
|
334
|
+
free_angle=180 * ureg.degree,
|
|
335
|
+
fixed_leg_radius=15 * ureg.mm,
|
|
336
|
+
mobile_leg_radius=15 * ureg.mm,
|
|
337
|
+
)
|
|
338
|
+
print(spring.spring_constant) # ~35 mm*N/rad
|
|
339
|
+
|
|
340
|
+
spring.add_position(angle_travel=30 * ureg.degree)
|
|
341
|
+
for position in spring.get_positions():
|
|
342
|
+
print(position)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### Fatigue analysis (Goodman diagram)
|
|
346
|
+
|
|
347
|
+
`springcalc.lineal.goodman` implements the modified-Goodman fatigue check
|
|
348
|
+
(Shigley, ch. 6/10) for spring wire in torsion, axial, or flexural loading.
|
|
349
|
+
`CompressionSpring.create_goodman_diagram()` and
|
|
350
|
+
`ExtensionSpring.create_goodman_diagram()` use this internally, but it can
|
|
351
|
+
also be used directly.
|
|
352
|
+
|
|
353
|
+
| Member | Description |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `GoodmanData(material, diameter, load_type="axial", cycles=1e6)` | Pydantic input model. `load_type` is `"axial"`, `"torsion"`, or `"flexion"`. |
|
|
356
|
+
| `GoodmanAnalyzer(data, shot_peening=False)` | Computes the Marin correction factors, the corrected endurance limit `Sse` and fatigue strength `Ssf`. |
|
|
357
|
+
| `.calculate_safety_factor(sigma_max, sigma_min)` | Modified-Goodman safety factor for an operating stress cycle. |
|
|
358
|
+
| `.get_analysis_summary(sigma_max, sigma_min)` | `dict` with the correction factors, strengths, operating point, and safety factor. |
|
|
359
|
+
| `.plot_diagram(sigma_max, sigma_min, show_plot=True)` | Returns a matplotlib `Figure` with the Goodman envelope and the operating point plotted. |
|
|
360
|
+
| `.get_diagram_image(sigma_max, sigma_min)` | Same diagram, returned as a base64 PNG string. |
|
|
361
|
+
| `Goodman(material, diameter, load_type="axial", number_cycles=1e6, shot_peening=False)` | Backwards-compatible wrapper around `GoodmanAnalyzer` with the same methods (`plot_goodman_graph`, `get_goodman_graph`, etc.). |
|
|
362
|
+
| `generate_goodman_diagram(spring, initial_length, final_length, shot_peening=False, number_cycles=1e6)` | Module function in `springcalc.plots`: derives max/min load and stress for a spring compressed between two lengths, and returns the same `{"image", "analysis", "stresses"}` dict. **`initial_length`/`final_length` must be `Quantity` values** (they're subtracted directly from `spring.free_length`). |
|
|
363
|
+
|
|
364
|
+
```python
|
|
365
|
+
from springcalc import Material, GoodmanData, GoodmanAnalyzer
|
|
366
|
+
|
|
367
|
+
material = Material(material_name="DH")
|
|
368
|
+
data = GoodmanData(material=material, diameter=1.0, load_type="torsion", cycles=1e5)
|
|
369
|
+
analyzer = GoodmanAnalyzer(data)
|
|
370
|
+
|
|
371
|
+
sigma_max, sigma_min = 400, 100 # MPa
|
|
372
|
+
print(analyzer.calculate_safety_factor(sigma_max, sigma_min))
|
|
373
|
+
summary = analyzer.get_analysis_summary(sigma_max, sigma_min)
|
|
374
|
+
print(summary["strengths"]["Se_MPa"], summary["strengths"]["Sf_MPa"])
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Using the standalone helper directly on a `CompressionSpring` instance (see
|
|
378
|
+
the [Compression springs](#compression-springs) example for how `spring` was
|
|
379
|
+
built), instead of calling `spring.create_goodman_diagram()`:
|
|
380
|
+
|
|
381
|
+
```python
|
|
382
|
+
from springcalc.plots import generate_goodman_diagram
|
|
383
|
+
from springcalc.pymodels.units import ureg
|
|
384
|
+
|
|
385
|
+
result = generate_goodman_diagram(spring, initial_length=100 * ureg.mm, final_length=40 * ureg.mm)
|
|
386
|
+
print(result["analysis"]["safety_factor"])
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Position tables
|
|
390
|
+
|
|
391
|
+
Every spring stores its recorded working positions in a pydantic list model
|
|
392
|
+
under `.positions`, populated via each spring's `add_load_position`/
|
|
393
|
+
`add_position` method rather than built by hand — but they're documented here
|
|
394
|
+
since `.get_data_positions()` returns their contents.
|
|
395
|
+
|
|
396
|
+
| Model | Used by | Fields |
|
|
397
|
+
|---|---|---|
|
|
398
|
+
| `LinearLoadPosition` (`springcalc.pymodels.positions`) | `CompressionSpring`, `ExtensionSpring` | `.position`, `.travel`, `.load` (N), `.stress` (MPa), `.outer_diameter`, `.inner_diameter` — all `Quantity`. |
|
|
399
|
+
| `LinearPositionsTable` | same | `.positions`: `list[LinearLoadPosition]`. `.add_load_position(...)`, `.clear_table()`. |
|
|
400
|
+
| `AngularLoadPosition` | `TorsionSpring` | Same fields as above, with `.position`/`.travel` in degrees and `.load` in N·mm (torque). |
|
|
401
|
+
| `AngularPositionsTable` | `TorsionSpring` | `.positions`: `list[AngularLoadPosition]`. `.add_load_position(...)`, `.clear_table()`. |
|
|
402
|
+
|
|
403
|
+
```python
|
|
404
|
+
# `spring` is any CompressionSpring/ExtensionSpring/TorsionSpring instance
|
|
405
|
+
# with load positions already recorded via add_load_position/add_position.
|
|
406
|
+
for position in spring.get_data_positions():
|
|
407
|
+
print(position.position, position.load, position.stress)
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### PDF reports
|
|
411
|
+
|
|
412
|
+
`SpringPDFReport` (`springcalc.report.SpringPDFReport`, also exported as
|
|
413
|
+
`springcalc.SpringPDFReport`) renders a `CompressionSpring`'s data, a 3D
|
|
414
|
+
isometric view, curves, and the Goodman diagram into a printable PDF using
|
|
415
|
+
`reportlab`.
|
|
416
|
+
|
|
417
|
+
| Method | Description |
|
|
418
|
+
|---|---|
|
|
419
|
+
| `SpringPDFReport(spring, title=None)` | Wrap a `CompressionSpring` (with load positions already added via `add_load_position`, for the fullest report). |
|
|
420
|
+
| `.build(output_path)` | Render the report and write it to `output_path`. Returns the path. Includes the spring data table, a 3D isometric view (from `spring.get_3d_plot()`), the load/geometry curves, the load-position table, and the Goodman diagram. Degrades gracefully (with a placeholder message) if a graph or the Goodman analysis can't be generated, e.g. no load positions recorded yet. |
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from springcalc.report import SpringPDFReport
|
|
424
|
+
|
|
425
|
+
# `spring` is a CompressionSpring instance (see the Compression springs example)
|
|
426
|
+
report = SpringPDFReport(spring, title="Spring XYZ-123")
|
|
427
|
+
report.build("spring_report.pdf")
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Advanced: variable-geometry springs
|
|
431
|
+
|
|
432
|
+
`VariableLinealSpring` and `CompressionSpringGeneral`
|
|
433
|
+
(`springcalc.lineal.generic_lineal` / `springcalc.lineal.generic_compression`)
|
|
434
|
+
model compression springs whose mean diameter and/or pitch vary along their
|
|
435
|
+
length (e.g. conical or barrel springs), by numerically integrating along the
|
|
436
|
+
helix instead of using the constant-geometry closed-form equations. They are
|
|
437
|
+
not exported from the top-level `springcalc` package — import them from their
|
|
438
|
+
modules directly. For a constant-diameter, constant-pitch spring they agree
|
|
439
|
+
with the closed-form `CompressionSpring` results.
|
|
440
|
+
|
|
441
|
+
| Method | Description |
|
|
442
|
+
|---|---|
|
|
443
|
+
| `CompressionSpringGeneral(material, wire_diameter, **data)` | Create the spring. Set `.mean_diameter_init`, `.pitch_constant`, and `.free_length` for a constant-geometry spring, or... |
|
|
444
|
+
| `.establish_geometrical_function(func_D, func_p)` | ...inject custom functions `h -> mean_diameter` and `h -> pitch` (both `Quantity -> Quantity`) for a true variable-geometry spring. |
|
|
445
|
+
| `.set_geometry(func_D, func_p, free_length=None, type_of_end=None, type_conforming=None)` | One-call setup: calls `.establish_geometrical_function(func_D, func_p)`, sets `.free_length`, and optionally `type_of_end` (one of `constants.COMPRESSION_SPRING_END_TYPES`, e.g. `"open_ground"`) and `type_conforming` (one of `constants.FORMING_TYPES`, e.g. `"cold_formed"`) — both default to the spring's current value when omitted. |
|
|
446
|
+
| `.calculate_theta_max()` | Total helix rotation angle (rad) needed to reach `free_length`; also updates `.nr_coils`. |
|
|
447
|
+
| `.calculate_active_coils()` | Number of active coils (`.nr_active_coils`), discounting the ground/squared end coils that don't deform, based on `type_of_end`/`type_conforming` — same formula as `CompressionSpring`. |
|
|
448
|
+
| `.calculate_spring_constant(num_points=500)` | Equivalent stiffness, integrating the local flexibility along the helix over the active coils only (end coils excluded per `.calculate_active_coils()`). |
|
|
449
|
+
| `.calculate_wire_length(num_points=500)` | Total wire length, integrating the 3D arc length along the helix. |
|
|
450
|
+
| `.calculate_solid_length()` | Solid (fully compressed) length, accounting for coil telescoping/nesting when the diameter varies enough. |
|
|
451
|
+
| `.get_3d_plot(num_points=500, show=False, isometric=True)` | Renders the helix centerline in 3D, following the actual `f_mean_diameter`/`f_pitch` functions (so variable geometries show up as a non-uniform helix); returns a base64 PNG. Defaults to an orthographic isometric view. |
|
|
452
|
+
| `.simulate_progressive_compression(max_deflection, steps=100, num_points=500, capture_geometry=False)` | Step-by-step compression simulation that detects coil-to-coil (oblique) contact; returns `(deflection, force, instantaneous_stiffness)` arrays. With `capture_geometry=True`, also returns a 4th value: `{"thetas", "z_history"}`, the instantaneous coil shape at every step (used by `CompressionAnimator`, see [below](#animating-progressive-compression)). |
|
|
453
|
+
|
|
454
|
+
```python
|
|
455
|
+
from springcalc import Material
|
|
456
|
+
from springcalc.pymodels.units import ureg
|
|
457
|
+
from springcalc.lineal.generic_compression import CompressionSpringGeneral
|
|
458
|
+
|
|
459
|
+
material = Material(material_name="SH")
|
|
460
|
+
spring = CompressionSpringGeneral(material=material, wire_diameter=2.0)
|
|
461
|
+
spring.set_geometry(
|
|
462
|
+
func_D=lambda h: 20 * ureg.mm,
|
|
463
|
+
func_p=lambda h: 6 * ureg.mm,
|
|
464
|
+
free_length=60 * ureg.mm,
|
|
465
|
+
type_of_end="open_ground", # optional; this is the default
|
|
466
|
+
type_conforming="cold_formed", # optional; this is the default
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
spring.calculate_theta_max()
|
|
470
|
+
print(spring.calculate_spring_constant()) # matches G*d^4/(8*D^3*n_active) for constant geometry
|
|
471
|
+
print(spring.nr_active_coils) # 7.7 (10 total coils minus the non-deforming ground ends)
|
|
472
|
+
|
|
473
|
+
deflection, force, stiffness = spring.simulate_progressive_compression(max_deflection=20 * ureg.mm, steps=20)
|
|
474
|
+
print(force[-1]) # ~52.92 N
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Animating progressive compression
|
|
478
|
+
|
|
479
|
+
`CompressionAnimator` (`springcalc.lineal.animation.CompressionAnimator`) renders
|
|
480
|
+
`simulate_progressive_compression`'s result as an animated GIF of the coils
|
|
481
|
+
closing up under load, reusing the same helix geometry as `get_3d_plot`. It
|
|
482
|
+
takes any `VariableLinealSpring` (e.g. `CompressionSpringGeneral`). Saving is
|
|
483
|
+
done with matplotlib's `PillowWriter`, so no extra system dependency (like
|
|
484
|
+
`ffmpeg`) is required.
|
|
485
|
+
|
|
486
|
+
| Method | Description |
|
|
487
|
+
|---|---|
|
|
488
|
+
| `CompressionAnimator(spring)` | Wrap a `CompressionSpringGeneral` (or other `VariableLinealSpring`) instance. |
|
|
489
|
+
| `.create_gif(max_deflection, output_path="compression.gif", steps=60, num_points=300, fps=12, isometric=True)` | Runs `simulate_progressive_compression(capture_geometry=True)` internally and writes the resulting animation to `output_path`. Returns `output_path`. Axis limits are fixed from the free-state geometry so the camera doesn't jump between frames. |
|
|
490
|
+
|
|
491
|
+
```python
|
|
492
|
+
from springcalc import Material
|
|
493
|
+
from springcalc.pymodels.units import ureg
|
|
494
|
+
from springcalc.lineal.generic_compression import CompressionSpringGeneral
|
|
495
|
+
from springcalc.lineal.animation import CompressionAnimator
|
|
496
|
+
|
|
497
|
+
material = Material(material_name="SH")
|
|
498
|
+
spring = CompressionSpringGeneral(material=material, wire_diameter=2.0)
|
|
499
|
+
spring.set_geometry(func_D=lambda h: 20 * ureg.mm, func_p=lambda h: 6 * ureg.mm, free_length=60 * ureg.mm)
|
|
500
|
+
|
|
501
|
+
animator = CompressionAnimator(spring)
|
|
502
|
+
animator.create_gif(max_deflection=25 * ureg.mm, output_path="compression.gif")
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Tests
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
uv run pytest
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
## Retraining the regression models
|
|
512
|
+
|
|
513
|
+
The included JSON coefficient files are already fitted. To regenerate them:
|
|
514
|
+
|
|
515
|
+
```bash
|
|
516
|
+
uv run python scripts/regresiones/factor_f/factor_f.py
|
|
517
|
+
```
|