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.
Files changed (68) hide show
  1. springcalc-0.1.0/.claude/settings.local.json +9 -0
  2. springcalc-0.1.0/.gitignore +18 -0
  3. springcalc-0.1.0/.python-version +1 -0
  4. springcalc-0.1.0/.vscode/launch.json +17 -0
  5. springcalc-0.1.0/LICENSE +21 -0
  6. springcalc-0.1.0/PKG-INFO +517 -0
  7. springcalc-0.1.0/README.md +478 -0
  8. springcalc-0.1.0/docs/Spring calculation all types.xlsm +0 -0
  9. springcalc-0.1.0/pyproject.toml +69 -0
  10. springcalc-0.1.0/scripts/regresiones/factor_f/factor__f_vs_RMa.csv +30 -0
  11. springcalc-0.1.0/scripts/regresiones/factor_f/factor_f.py +74 -0
  12. springcalc-0.1.0/scripts/regresiones/wahl_factor/Wahl factor vs index.csv +53 -0
  13. springcalc-0.1.0/scripts/regresiones/wahl_factor/Wahl_factor torsion.jpg +0 -0
  14. springcalc-0.1.0/scripts/regresiones/wahl_factor/factor_wahl.py +64 -0
  15. springcalc-0.1.0/scripts/regresiones/wahl_factor/factor_wahl2.py +123 -0
  16. springcalc-0.1.0/scripts/regresiones/wahl_factor/modelo_factor_f.pkl +0 -0
  17. springcalc-0.1.0/scripts/regresiones/wahl_factor/parametros_popt.pkl +0 -0
  18. springcalc-0.1.0/scripts/regresiones/wahl_factor/usar_modelo_factor_f.py +71 -0
  19. springcalc-0.1.0/src/springcalc/__init__.py +58 -0
  20. springcalc-0.1.0/src/springcalc/lineal/__init__.py +0 -0
  21. springcalc-0.1.0/src/springcalc/lineal/animation.py +84 -0
  22. springcalc-0.1.0/src/springcalc/lineal/compresion.py +540 -0
  23. springcalc-0.1.0/src/springcalc/lineal/constants.py +28 -0
  24. springcalc-0.1.0/src/springcalc/lineal/extension.py +579 -0
  25. springcalc-0.1.0/src/springcalc/lineal/generic_compression.py +472 -0
  26. springcalc-0.1.0/src/springcalc/lineal/generic_lineal.py +408 -0
  27. springcalc-0.1.0/src/springcalc/lineal/goodman.py +262 -0
  28. springcalc-0.1.0/src/springcalc/lineal/lineal.py +225 -0
  29. springcalc-0.1.0/src/springcalc/lineal/plotting.py +39 -0
  30. springcalc-0.1.0/src/springcalc/lineal/torsion.py +694 -0
  31. springcalc-0.1.0/src/springcalc/material/DH_RMa.csv +90 -0
  32. springcalc-0.1.0/src/springcalc/material/DIAMETRO_TOLERANCIAS.csv +90 -0
  33. springcalc-0.1.0/src/springcalc/material/DM_RMa.csv +75 -0
  34. springcalc-0.1.0/src/springcalc/material/SH_RMa.csv +75 -0
  35. springcalc-0.1.0/src/springcalc/material/SL_RMa.csv +42 -0
  36. springcalc-0.1.0/src/springcalc/material/SM_RMa.csv +75 -0
  37. springcalc-0.1.0/src/springcalc/material/__init__.py +0 -0
  38. springcalc-0.1.0/src/springcalc/material/materials.csv +22 -0
  39. springcalc-0.1.0/src/springcalc/plots/__init__.py +3 -0
  40. springcalc-0.1.0/src/springcalc/plots/goodman_diagram.py +57 -0
  41. springcalc-0.1.0/src/springcalc/pymodels/__init__.py +0 -0
  42. springcalc-0.1.0/src/springcalc/pymodels/material.py +210 -0
  43. springcalc-0.1.0/src/springcalc/pymodels/positions.py +260 -0
  44. springcalc-0.1.0/src/springcalc/pymodels/units.py +3 -0
  45. springcalc-0.1.0/src/springcalc/pymodels/wire_characteristics.py +149 -0
  46. springcalc-0.1.0/src/springcalc/regresiones/__init__.py +0 -0
  47. springcalc-0.1.0/src/springcalc/regresiones/factor_f/__init__.py +0 -0
  48. springcalc-0.1.0/src/springcalc/regresiones/factor_f/factor_f_coeffs.json +10 -0
  49. springcalc-0.1.0/src/springcalc/regresiones/factor_f/usar_modelo_factor_f.py +73 -0
  50. springcalc-0.1.0/src/springcalc/report/__init__.py +3 -0
  51. springcalc-0.1.0/src/springcalc/report/pdf_report.py +240 -0
  52. springcalc-0.1.0/tests/__init__.py +0 -0
  53. springcalc-0.1.0/tests/test_diameter_pitch_cuadratic_variable.py +106 -0
  54. springcalc-0.1.0/tests/test_diameter_pitch_linear_variable copy.py +90 -0
  55. springcalc-0.1.0/tests/test_diameter_variable.py +134 -0
  56. springcalc-0.1.0/tests/test_factor_f.py +49 -0
  57. springcalc-0.1.0/tests/test_goodman.py +151 -0
  58. springcalc-0.1.0/tests/test_lineal.py +46 -0
  59. springcalc-0.1.0/tests/test_lineal_compression.py +59 -0
  60. springcalc-0.1.0/tests/test_lineal_extension.py +85 -0
  61. springcalc-0.1.0/tests/test_material.py +61 -0
  62. springcalc-0.1.0/tests/test_pdf_report.py +62 -0
  63. springcalc-0.1.0/tests/test_pitch_variable.py +185 -0
  64. springcalc-0.1.0/tests/test_progressive_animation.py +78 -0
  65. springcalc-0.1.0/tests/test_set_geometry.py +42 -0
  66. springcalc-0.1.0/tests/test_torsion.py +32 -0
  67. springcalc-0.1.0/tests/test_wahl_factor_category.py +24 -0
  68. springcalc-0.1.0/uv.lock +1743 -0
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run *)",
5
+ "Bash(uv run python -c ' *)",
6
+ "Bash(python -c \"import springcalc\")"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,18 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ # Tooling caches
13
+ .pytest_cache/
14
+ .ruff_cache/
15
+
16
+ *.pdf
17
+ *.png
18
+ *.gif
@@ -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
+ }
@@ -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
+ ```