springcalc
A Python library for spring calculations: compression, extension, and torsion springs. Includes material data, wire characteristic calculations, and fatigue analysis via the Goodman diagram.
Want to help? Contact me at eacarretero@gmail.com
Visit the library on my github https://github.com/ErnestoAvedillo/springcalc and clone the repository using:
git clone git@github.com:ErnestoAvedillo/springcalc.git
Structure
src/springcalc/ Library package
├── lineal/ Calculation engine (compression, extension, torsion, Goodman,
│ 3D visualization, progressive-compression animation)
├── pymodels/ Data models (pydantic): material, units, wire, positions
├── material/ Material and tolerance tables (CSV, package data)
├── regresiones/ Fitted models loaded at runtime
│ └── factor_f/ Shigley's factor f (plain JSON coefficients + loader)
├── plots/ Goodman diagram generation
├── report/ PDF report generation (SpringPDFReport)
└── inverse_calc/ Inverse design: find spring geometry from a target rate or curve
tests/ Tests (pytest)
scripts/regresiones/ Training scripts that regenerate the JSON coefficients (not runtime)
docs/ Reference material (spreadsheet, figures)
Installation
I recommend to use uv to install the library (https://docs.astral.sh/uv/):
# create the environment and install dependencies
uv init
uv add springcalc
Usage
from springcalc import Material, CompressionSpring
material = Material(material_name="SH")
spring = CompressionSpring(material=material, wire_diameter=1.0)
spring.set_geometry(outer_diameter=10.0, free_length=50.0, nr_coils=10)
properties = spring.get_spring_data()
for key, value in properties.items():
print(f"{key}: {value}")
Generating a PDF report (spring data, load/travel/diameter curves, and the Goodman fatigue diagram):
from springcalc.report import SpringPDFReport
report = SpringPDFReport(spring, title="Spring XYZ-123")
report.build("spring_report.pdf")
API Reference
- Material data —
Material,get_available_materials() - Wire characteristics —
WireCharacteristics - Compression springs —
CompressionSpring - Extension springs —
ExtensionSpring - Torsion springs —
TorsionSpring - Fatigue analysis (Goodman diagram) —
GoodmanData,GoodmanAnalyzer,Goodman,generate_goodman_diagram() - Position tables —
LinearPositionsTable,AngularPositionsTable - PDF reports —
SpringPDFReport - Advanced: variable-geometry springs —
VariableLinealSpring,CompressionSpringGeneral - Animating progressive compression —
CompressionAnimator - Inverse design —
CompressionSpringInverseDesigner,ConicalCompressionSpringInverseDesigner,ConicalCurveCompressionSpringInverseDesigner,GeneralCompressionSpringInverseDesigner
All physical quantities are pint Quantity
objects (a number with a unit, e.g. 20.0 millimeter). Plain numbers passed
into a field are usually interpreted in that field's default unit (mm, N,
degrees...), but a few methods require an explicit Quantity — those are
called out below. Import the shared unit registry with:
from springcalc.pymodels.units import ureg
length = 20 * ureg.mm
angle = 90 * ureg.degree
Material data
Material (springcalc.pymodels.material.Material) is a pydantic model that
looks up a named material's mechanical properties from material/materials.csv
and auto-fills any field you don't pass explicitly.
| Member | Description |
|---|---|
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. |
.young_modulus |
Young's modulus E, as a Quantity in MPa. |
.shear_modulus |
Shear modulus G, as a Quantity in MPa. |
.elastic_limit_factor |
Dimensionless factor used to derive the fatigue limit. |
.poisson_coef |
Poisson's ratio (dimensionless). |
.RMa_file |
Name of the CSV (in material/) with tensile-strength ranges by wire diameter. |
get_available_materials() |
Module-level function: list of valid material_name values. |
Material.list_available_materials() |
Classmethod: list of valid material_name values (same as get_available_materials()). |
Material.material_exists(material_name) |
Classmethod: whether a material is already registered. |
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. |
from springcalc import Material, get_available_materials
print(get_available_materials())
# ['SL', 'SM', 'DM', 'SH', 'DH', 'TDC', 'TDCrV', ...]
material = Material(material_name="SH")
print(material.young_modulus) # 206000.0 megapascal
print(material.shear_modulus) # 81500.0 megapascal
print(material.poisson_coef) # 0.3 dimensionless
Adding a new material — young_modulus/shear_modulus accept a unit string, a
plain number (assumed MPa), or a Quantity; elastic_limit_factor and
poisson_coef are dimensionless. Once created, the material is loadable by
name like any built-in one:
from springcalc import Material
custom = Material.create_material(
material_name="CustomSteel",
young_modulus="210000 MPa",
shear_modulus=81000,
elastic_limit_factor=0.5,
poisson_coef=0.3,
description="Custom steel for a special order",
RMa_data=[(1.0, 1600, 1800), (2.0, 1500, 1700)], # (diameter mm, RMa_min, RMa_max)
)
# From now on it behaves like any other material
same_material = Material(material_name="CustomSteel")
Wire characteristics
WireCharacteristics (springcalc.pymodels.wire_characteristics.WireCharacteristics)
is the base class of every spring type. Given a material and a wire diameter,
it looks up the wire diameter tolerance and the tensile-strength range (RMa)
for that diameter.
| Member | Description |
|---|---|
WireCharacteristics(material, wire_diameter) |
wire_diameter may be a plain number (interpreted in mm) or a Quantity. |
.diameter_tolerance |
Manufacturing tolerance for this diameter (from DIAMETRO_TOLERANCIAS.csv). |
.RMa_min / .RMa_max |
Tensile-strength range for this material and diameter, from material.RMa_file. |
.set_material(material, wire_diameter) |
Re-assign the material/diameter and refresh the derived fields. |
from springcalc import Material
from springcalc.pymodels.wire_characteristics import WireCharacteristics
material = Material(material_name="SH")
wire = WireCharacteristics(material=material, wire_diameter=2.0)
print(wire.diameter_tolerance) # 0.025
print(wire.RMa_min, wire.RMa_max) # 1980.0 2200.0
Compression springs
CompressionSpring (springcalc.lineal.compresion.CompressionSpring, also
exported as springcalc.CompressionSpring) is the main entry point for
helical compression springs. It extends LinealSpring
(springcalc.lineal.lineal.LinealSpring), the shared calculation engine also
used by ExtensionSpring.
| Method | Description |
|---|---|
CompressionSpring(material, wire_diameter, **data) |
Create the spring. |
.set_geometry(mean_diameter=None, outer_diameter=None, inner_diameter=None, nr_coils=None, pitch=None, free_length=None, type_of_end=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") is optional and, if given, overrides the spring's default before the active-coil count is computed. Equivalent to calling .set_diameter() followed by .calculate_spring_properties(). Returns .get_spring_data(). |
.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. |
.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. |
.add_load_position(length) |
Record the load/stress/outer-diameter at a given compressed length, for the load-position table and fatigue analysis. |
.empty_tables() |
Clear the recorded load positions. |
.get_spring_data() |
dict with all computed spring properties (material, diameters, constants, Wahl factor, etc.). |
.get_data_positions() / .get_data_travels() |
List of LinearLoadPosition recorded via add_load_position. |
.get_forces_vs_position_graph(show=False) |
Load vs. absolute position curve; returns a base64-encoded PNG. |
.get_forces_vs_travel_graph(show=False) |
Load vs. travel (compression from free length) curve; returns a base64 PNG. |
.get_diameter_graph(show=False) |
Outer diameter vs. position curve; returns a base64 PNG. |
.get_diameter_vs_position_graph(show=False) |
Outer diameter curve plus a to-scale cross-section diagram; returns a base64 PNG. |
.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. |
.create_goodman_diagram(show=False) |
Runs the fatigue (Goodman) analysis from the recorded positions; returns {"image", "analysis", "stresses"} or {"error", "traceback"}. |
.get_stress_max() / .get_stress_min() |
Max/min stress across recorded positions (raises if none are recorded). |
.get_load_max() / .get_load_min() |
Max/min load across recorded positions. |
.calculate_solid_length() |
Coils-stacked solid (fully compressed) length. |
.calculate_wire_length() |
Total wire length needed to wind the spring. |
.set_number_cycles(number_cycles) |
Design life, in cycles, used by the fatigue analysis (default 1e6). |
from springcalc import Material, CompressionSpring
material = Material(material_name="SL")
spring = CompressionSpring(material=material, wire_diameter=2.5)
spring.set_geometry(outer_diameter=30, pitch=20, free_length=100) # mm
# Equivalent to calling separately:
# spring.set_diameter(outer_diameter=30)
# spring.calculate_spring_properties(pitch=20, free_length=100)
for length_mm in [30, 40, 50, 60, 70, 80, 90, 100]:
spring.add_load_position(length=length_mm)
data = spring.get_spring_data()
print(data["spring_constant"]) # ~3.60 N / mm
print(data["wahl_factor_category"]) # 'green' -> C=11 is in a normal manufacturable range
# Graphs (base64 PNGs, ready to embed in HTML or a PDF)
load_vs_position_png = spring.get_forces_vs_position_graph()
diameter_png = spring.get_diameter_vs_position_graph()
# Fatigue analysis from the recorded positions
result = spring.create_goodman_diagram()
print(result["analysis"]["safety_factor"])
Extension springs
ExtensionSpring (springcalc.lineal.extension.ExtensionSpring, also exported
as springcalc.ExtensionSpring) models helical extension springs. It extends
LinealSpring directly (not CompressionSpring) and its diameter/length
setters require explicit Quantity values rather than plain numbers.
| Method | Description |
|---|---|
ExtensionSpring(material, wire_diameter, **data) |
Create the spring. |
.set_diameter(mean_diameter=None, outer_diameter=None, inner_diameter=None) |
Set exactly one diameter. Must be a Quantity (e.g. 15 * ureg.mm), not a plain number. |
.calculate_spring_properties(nr_coils=None, pitch=None, free_length=None) |
Pass nr_coils (required); pitch is optional and defaults to the wire diameter (close-wound). free_length is accepted but ignored — it is set to nr_coils × pitch. Computes active coils, spring constant, and wire length. |
.add_load_position(length) |
Record the load/stress/outer-diameter at a given extended length (must be ≥ free length). |
.calculate_positions_table(step: list) |
Convenience: call add_load_position for each length in step. |
.empty_tables() |
Clear the recorded load positions. |
.get_spring_data() |
dict with all computed spring properties. |
.get_data_positions() / .get_data_travels() |
List of recorded LinearLoadPosition. |
.get_forces_vs_position_graph(show=False) / .get_forces_vs_travel_graph(show=False) |
Load curves; return base64 PNGs. |
.get_diameter_graph(show=False) / .get_diameter_vs_position_graph(show=False) |
Diameter curves; return base64 PNGs. |
.create_goodman_diagram(show=False) |
Fatigue analysis from the recorded positions; returns {"image", "analysis", "stresses"} or {"error", "traceback"}. |
.get_stress_max() / .get_stress_min() / .get_load_max() / .get_load_min() |
Extremes across recorded positions. |
.set_number_cycles(number_cycles) |
Design life in cycles for the fatigue analysis. |
.set_initial_stress(initial_stress) |
Set the spring's initial tension (pre-load) stress. |
from springcalc import Material, ExtensionSpring
from springcalc.pymodels.units import ureg
material = Material(material_name="SH")
spring = ExtensionSpring(material=material, wire_diameter=1.5)
spring.set_diameter(outer_diameter=15 * ureg.mm) # note: needs a Quantity, unlike CompressionSpring
spring.calculate_spring_properties(nr_coils=10) # pitch defaults to the wire diameter -> free_length = 10 * 1.5 = 15 mm
spring.calculate_positions_table([20, 25, 30, 35]) # extend beyond the 15 mm free length
data = spring.get_spring_data()
print(data["spring_constant"]) # ~2.33 N / mm
result = spring.create_goodman_diagram()
print(result["analysis"]["safety_factor"])
Torsion springs
TorsionSpring (springcalc.lineal.torsion.TorsionSpring, also exported as
springcalc.TorsionSpring) models helical torsion springs, tracking angular
position/travel and torque instead of linear load. It extends
WireCharacteristics directly and has no Goodman/fatigue integration.
| Method | Description |
|---|---|
TorsionSpring(material, wire_diameter, **data) |
Create the spring. |
.set_geometry(mean_diameter=None, outer_diameter=None, inner_diameter=None, nr_coils=None, pitch=None, free_angle=None, fixed_leg_radius=None, mobile_leg_radius=None) |
One-call setup: pass exactly one of the three diameters plus nr_coils/pitch/free_angle/fixed_leg_radius/mobile_leg_radius. Sets geometry and computes every derived property. Returns .get_spring_properties(). |
.calculate_spring_properties() |
Re-run the derived-property calculations after changing an input. |
.add_position(angle_travel=None, torque=None) |
Record a working position from either an angular travel or a torque (exactly one). |
.clean_positions() |
Clear the recorded positions. |
.get_positions() / .get_data_positions() / .get_data_travels() |
List of recorded AngularLoadPosition. |
.get_spring_properties() |
dict with all computed properties (diameters, angles, leg lengths, spring constant, Wahl factor, etc.). |
.calculate_torque(rotation_angle) |
Torque required for a given rotation angle. |
.calculate_stress(torque) |
Max wire stress for a given torque. |
.get_forces_vs_position_graph(show=False) / .get_forces_vs_travel_graph(show=False) |
Torque vs. angular position/travel curves; return base64 PNGs. |
.get_diameter_vs_position_graph(show=False) |
Outer diameter curve plus a cross-section diagram; returns a base64 PNG. |
.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). |
from springcalc import Material, TorsionSpring
from springcalc.pymodels.units import ureg
material = Material(material_name="SH")
spring = TorsionSpring(material=material, wire_diameter=1.0)
spring.set_geometry(
mean_diameter=10 * ureg.mm,
nr_coils=8,
pitch=1.2 * ureg.mm,
free_angle=180 * ureg.degree,
fixed_leg_radius=15 * ureg.mm,
mobile_leg_radius=15 * ureg.mm,
)
print(spring.spring_constant) # ~35 mm*N/rad
spring.add_position(angle_travel=30 * ureg.degree)
for position in spring.get_positions():
print(position)
Fatigue analysis (Goodman diagram)
springcalc.lineal.goodman implements the modified-Goodman fatigue check
(Shigley, ch. 6/10) for spring wire in torsion, axial, or flexural loading.
CompressionSpring.create_goodman_diagram() and
ExtensionSpring.create_goodman_diagram() use this internally, but it can
also be used directly.
| Member | Description |
|---|---|
GoodmanData(material, wire_diameter, load_type="axial", number_cycles=1e6) |
Pydantic input model. load_type is "axial", "torsion", or "flexion". |
GoodmanAnalyzer(data, shot_peening=False) |
Computes the Marin correction factors, the corrected endurance limit Sse and fatigue strength Ssf. |
.calculate_safety_factor(sigma_max, sigma_min) |
Modified-Goodman safety factor for an operating stress cycle. |
.get_analysis_summary(sigma_max, sigma_min) |
dict with the correction factors, strengths, operating point, and safety factor. |
.plot_diagram(sigma_max, sigma_min, show_plot=True) |
Returns a matplotlib Figure with the Goodman envelope and the operating point plotted. |
.get_diagram_image(sigma_max, sigma_min) |
Same diagram, returned as a base64 PNG string. |
Goodman(material, wire_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.). |
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). |
from springcalc import Material, GoodmanData, GoodmanAnalyzer
material = Material(material_name="DH")
data = GoodmanData(material=material, wire_diameter=1.0, load_type="torsion", number_cycles=1e5)
analyzer = GoodmanAnalyzer(data)
sigma_max, sigma_min = 400, 100 # MPa
print(analyzer.calculate_safety_factor(sigma_max, sigma_min))
summary = analyzer.get_analysis_summary(sigma_max, sigma_min)
print(summary["strengths"]["Se_MPa"], summary["strengths"]["Sf_MPa"])
Using the standalone helper directly on a CompressionSpring instance (see
the Compression springs example for how spring was
built), instead of calling spring.create_goodman_diagram():
from springcalc.plots import generate_goodman_diagram
from springcalc.pymodels.units import ureg
result = generate_goodman_diagram(spring, initial_length=100 * ureg.mm, final_length=40 * ureg.mm)
print(result["analysis"]["safety_factor"])
Position tables
Every spring stores its recorded working positions in a pydantic list model
under .positions, populated via each spring's add_load_position/
add_position method rather than built by hand — but they're documented here
since .get_data_positions() returns their contents.
| Model | Used by | Fields |
|---|---|---|
LinearLoadPosition (springcalc.pymodels.positions) |
CompressionSpring, ExtensionSpring |
.position, .travel, .load (N), .stress (MPa), .outer_diameter, .inner_diameter — all Quantity. |
LinearPositionsTable |
same | .positions: list[LinearLoadPosition]. .add_load_position(...), .clear_table(). |
AngularLoadPosition |
TorsionSpring |
Same fields as above, with .position/.travel in degrees and .load in N·mm (torque). |
AngularPositionsTable |
TorsionSpring |
.positions: list[AngularLoadPosition]. .add_load_position(...), .clear_table(). |
# `spring` is any CompressionSpring/ExtensionSpring/TorsionSpring instance
# with load positions already recorded via add_load_position/add_position.
for position in spring.get_data_positions():
print(position.position, position.load, position.stress)
PDF reports
SpringPDFReport (springcalc.report.SpringPDFReport, also exported as
springcalc.SpringPDFReport) renders a CompressionSpring's data, a 3D
isometric view, curves, and the Goodman diagram into a printable PDF using
reportlab.
| Method | Description |
|---|---|
SpringPDFReport(spring, title=None) |
Wrap a CompressionSpring (with load positions already added via add_load_position, for the fullest report). |
.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. |
from springcalc.report import SpringPDFReport
# `spring` is a CompressionSpring instance (see the Compression springs example)
report = SpringPDFReport(spring, title="Spring XYZ-123")
report.build("spring_report.pdf")
Advanced: variable-geometry springs
VariableLinealSpring and CompressionSpringGeneral
(springcalc.lineal.generic_lineal / springcalc.lineal.generic_compression)
model compression springs whose mean diameter and/or pitch vary along their
length (e.g. conical or barrel springs), by numerically integrating along the
helix instead of using the constant-geometry closed-form equations. They are
not exported from the top-level springcalc package — import them from their
modules directly. Unlike CompressionSpring, this class has no active-coil
correction (no .calculate_active_coils()/.nr_active_coils): .nr_coils
is used directly wherever CompressionSpring would use its (always smaller)
active-coil count, so even for a constant-diameter, constant-pitch spring
its results (e.g. .calculate_spring_constant()) don't numerically match
CompressionSpring's for the same nr_coils — type_of_end is accepted
(and stored) but doesn't otherwise affect the calculation here.
| Method | Description |
|---|---|
CompressionSpringGeneral(material, wire_diameter, **data) |
Create the spring. Set .mean_diameter_init, .pitch_constant, and .free_length for a constant-geometry spring, or... |
.establish_geometrical_function(f_mean_diameter, f_pitch) |
...inject custom functions h -> mean_diameter and h -> pitch (both Quantity -> Quantity) for a true variable-geometry spring. |
.set_geometry(f_mean_diameter, f_pitch, free_length, type_of_end=None) |
One-call setup: calls .establish_geometrical_function(f_mean_diameter, f_pitch), sets .free_length (required, must be a Quantity), and optionally type_of_end (one of constants.COMPRESSION_SPRING_END_TYPES, e.g. "open_ground") — defaults to the spring's current value when omitted. |
.calculate_theta_max() |
Total helix rotation angle (rad) needed to reach free_length; also updates .nr_coils. |
.calculate_spring_constant(num_points=500) |
Equivalent stiffness, integrating the local flexibility along the entire helix (.nr_coils). Unlike CompressionSpring, this class has no active-coil correction — end coils aren't excluded, so results are only comparable to CompressionSpring's for end types where that correction is zero. |
.calculate_wire_length(num_points=500) |
Total wire length, integrating the 3D arc length along the helix. |
.calculate_solid_length() |
Solid (fully compressed) length, accounting for coil telescoping/nesting when the diameter varies enough. |
.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. |
.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). |
from springcalc import Material
from springcalc.pymodels.units import ureg
from springcalc.lineal.generic_compression import CompressionSpringGeneral
material = Material(material_name="SH")
spring = CompressionSpringGeneral(material=material, wire_diameter=2.0)
spring.set_geometry(
f_mean_diameter=lambda h: 20 * ureg.mm,
f_pitch=lambda h: 6 * ureg.mm,
free_length=60 * ureg.mm,
type_of_end="open_ground", # optional; this is the default
)
spring.calculate_theta_max()
print(spring.calculate_spring_constant()) # matches G*d^4/(8*D^3*n) for constant geometry (n = nr_coils, 10 here)
deflection, force, stiffness = spring.simulate_progressive_compression(max_deflection=20 * ureg.mm, steps=20)
print(force[-1]) # ~40.75 N (still in the linear regime at 20mm out of 60mm free length)
Animating progressive compression
CompressionAnimator (springcalc.lineal.animation.CompressionAnimator) renders
simulate_progressive_compression's result as an animated GIF of the coils
closing up under load, reusing the same helix geometry as get_3d_plot. It
takes any VariableLinealSpring (e.g. CompressionSpringGeneral). Saving is
done with matplotlib's PillowWriter, so no extra system dependency (like
ffmpeg) is required.
| Method | Description |
|---|---|
CompressionAnimator(spring) |
Wrap a CompressionSpringGeneral (or other VariableLinealSpring) instance. |
.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. |
from springcalc import Material
from springcalc.pymodels.units import ureg
from springcalc.lineal.generic_compression import CompressionSpringGeneral
from springcalc.lineal.animation import CompressionAnimator
material = Material(material_name="SH")
spring = CompressionSpringGeneral(material=material, wire_diameter=2.0)
spring.set_geometry(f_mean_diameter=lambda h: 20 * ureg.mm, f_pitch=lambda h: 6 * ureg.mm, free_length=60 * ureg.mm)
animator = CompressionAnimator(spring)
animator.create_gif(max_deflection=25 * ureg.mm, output_path="compression.gif")
Inverse design
springcalc.inverse_calc (not exported from the top-level springcalc
package — import from its modules directly) works backwards from a target
spring rate or a target force-displacement curve to a buildable geometry,
instead of computing properties from geometry you already chose. All four
designers search the standard wire diameter series
(get_standard_wire_diameters()), score candidates against a target fatigue
safety factor (via GoodmanAnalyzer), and rebuild/verify the winning design
with the library's normal spring classes before returning it — so the
returned .spring behaves exactly like a spring you built by hand.
Cylindrical spring from two (or more) rate points
CompressionSpringInverseDesigner
(springcalc.inverse_calc.lineal_comp_inv) finds the wire diameter and mean
diameter of a standard (constant-diameter, constant-pitch) CompressionSpring
that reproduces a target rate while landing the safety factor as close as
possible to a target value. The number of coils is always solved so the rate
matches exactly; for each standard wire diameter, the mean diameter that hits
the safety-factor target is found by 1-D root finding.
| Member | Description |
|---|---|
Requirements(material, safety_factor, length1=None, length2=None, force1=None, force2=None, csv_path=None) |
Design brief. Give either length1/length2/force1/force2 (two exact length/force points) or csv_path (a CSV with length,force columns, mm/N, fit by least squares) — not both. Also used by the conical designers below. |
CompressionSpringInverseDesigner(requirements, type_of_end=..., spring_index_bounds=(4.5, 12.0), wire_diameter_bounds=(0.0, inf), min_active_coils=2.0, number_cycles=1_000_000, shot_peening=False) |
Construct the designer. |
.design() |
Runs the search and returns an InverseCompressionDesign. |
InverseCompressionDesign.spring |
The winning, fully built CompressionSpring (load positions already added at the two extreme lengths). |
.wire_diameter / .mean_diameter / .free_length / .nr_coils / .spring_index / .spring_constant |
Resulting geometry, as Quantity/float. |
.safety_factor / .safety_factor_target / .safety_factor_error |
Achieved vs. target safety factor (error is positive when more conservative than requested). |
.candidates |
list[CandidateDesign], one per standard wire diameter tried (valid or not), for traceability. |
from springcalc.inverse_calc.lineal_comp_inv import CompressionSpringInverseDesigner, Requirements
from springcalc.pymodels.material import Material
material = Material(material_name="SL")
requirements = Requirements(
material=material, safety_factor=1.5,
length1=60, force1=200, # more compressed, higher-force point
length2=90, force2=50, # less compressed, lower-force point
)
result = CompressionSpringInverseDesigner(requirements).design()
print(result.wire_diameter, result.mean_diameter, result.nr_coils)
print(result.safety_factor, result.safety_factor_error)
Conical (tapered) spring from two (or more) rate points
ConicalCompressionSpringInverseDesigner
(springcalc.inverse_calc.conical_comp_inv) is the same idea, but for a
linearly-tapered (diameter and pitch) conical spring built on
CompressionSpringGeneral. The rate and safety-factor equations alone leave
the taper shape (tau_D = D_end/D_start, tau_p = p_end/p_start) free, so
the extra degree of freedom is resolved by minimizing solid length — i.e.
preferring the most compact (most telescoping) taper, the usual reason to
choose a conical spring at all. For each standard wire diameter, the taper
shape is searched on a grid and locally polished (Nelder-Mead).
| Member | Description |
|---|---|
ConicalCompressionSpringInverseDesigner(requirements, type_of_end=..., spring_index_bounds=(4.5, 12.0), taper_ratio_bounds=(0.3, 1.0), pitch_ratio_bounds=(0.3, 3.0), wire_diameter_bounds=(0.0, inf), min_coils=2.0, shape_grid_resolution=9, number_cycles=1_000_000, shot_peening=False) |
Construct the designer with the same Requirements used above. |
.design() |
Runs the search and returns a ConicalInverseCompressionDesign. |
ConicalInverseCompressionDesign.spring |
The winning, fully built CompressionSpringGeneral. |
.diameter_start / .diameter_end / .pitch_start / .pitch_end / .free_length / .nr_coils / .solid_length / .spring_constant |
Resulting geometry, as Quantity/float. |
.safety_factor / .safety_factor_target / .safety_factor_error |
Achieved vs. target safety factor. |
.candidates |
list[ShapeCandidate], best taper shape per standard wire diameter tried. |
linear_profile(start_mm, end_mm, free_length_mm) |
Module function: builds a f_mean_diameter/f_pitch-compatible closure for a linearly-varying quantity — handy for feeding the winning geometry into CompressionSpringGeneral.set_geometry() directly. |
from springcalc.inverse_calc.conical_comp_inv import ConicalCompressionSpringInverseDesigner
from springcalc.inverse_calc.lineal_comp_inv import Requirements
from springcalc.pymodels.material import Material
material = Material(material_name="SL")
requirements = Requirements(material=material, safety_factor=1.2,
length1=60, force1=200, length2=90, force2=50)
result = ConicalCompressionSpringInverseDesigner(requirements).design()
print(result.diameter_start, result.diameter_end)
print(result.pitch_start, result.pitch_end)
print(result.solid_length)
Conical (tapered) spring fit to a full force-displacement curve
ConicalCurveCompressionSpringInverseDesigner
(springcalc.inverse_calc.conical_curve_comp_inv) fits a conical spring's
wire diameter, free length, and diameter/pitch taper to an entire target
force-vs-displacement curve (rather than just two points), so it can capture
progressive stiffening from coil-to-coil contact. There's no closed form for
this, so it's a regression: scipy.optimize.differential_evolution searches
the 6-parameter space (wire diameter, free length, D_start, D_end, p_start,
p_end), minimizing curve RMSE plus a soft safety-factor term plus a geometry
penalty, using a fast closed-form contact simulation; the winning wire
diameter is then snapped to the standard series and the rest locally
re-fit before the final design is rebuilt with the real, general
CompressionSpringGeneral machinery.
| Member | Description |
|---|---|
CompressionCurveRequirements(material, safety_factor, csv_path) |
Design brief: csv_path is a CSV with displacement,load columns (mm, N) — displacement is travel from the free length, not an absolute position. |
load_target_curve(csv_path) |
Module function: reads and sorts the target curve; returns (displacement, load) numpy arrays. |
ConicalCurveCompressionSpringInverseDesigner(requirements, type_of_end=..., spring_index_bounds=(4.5, 12.0), wire_diameter_bounds=(0.3, 10.0), diameter_bounds=(3.0, 150.0), pitch_bounds=(0.3, 40.0), free_length_margin=(1.05, 3.0), min_coils=2.0, safety_factor_weight=1.0, penalty_weight=0.05, search_num_points=60, search_steps=60, final_num_points=500, final_steps=500, maxiter=60, popsize=15, seed=None, number_cycles=1_000_000, shot_peening=False) |
Construct the designer. seed makes the regression reproducible. |
.design() |
Runs the regression and returns a ConicalCurveInverseDesign. |
ConicalCurveInverseDesign.spring |
The winning, fully built CompressionSpringGeneral. |
.diameter_start / .diameter_end / .pitch_start / .pitch_end / .free_length / .nr_coils / .solid_length |
Resulting geometry, as Quantity/float. |
.safety_factor / .safety_factor_target / .safety_factor_error |
Achieved vs. target safety factor. |
.curve_rmse / .curve_rmse_relative |
Fit quality: RMSE between the simulated and target curves (Quantity in N), and that RMSE relative to the target curve's typical load magnitude. |
.target_displacement / .target_load / .simulated_displacement / .simulated_load |
Both curves as numpy arrays, ready to plot against each other. |
from springcalc.inverse_calc.conical_curve_comp_inv import (
CompressionCurveRequirements, ConicalCurveCompressionSpringInverseDesigner,
)
from springcalc.pymodels.material import Material
material = Material(material_name="SL")
requirements = CompressionCurveRequirements(
material=material, safety_factor=1.3, csv_path="target_curve.csv", # displacement,load columns (mm, N)
)
result = ConicalCurveCompressionSpringInverseDesigner(requirements, seed=0).design()
print(result.curve_rmse, result.curve_rmse_relative)
print(result.diameter_start, result.diameter_end, result.pitch_start, result.pitch_end)
General (arbitrary-profile) spring fit to a full force-displacement curve
GeneralCompressionSpringInverseDesigner
(springcalc.inverse_calc.general_comp_inv) is the same curve-fitting idea as
ConicalCurveCompressionSpringInverseDesigner, but for a mean-diameter and
pitch profile that isn't restricted to a straight taper: D(h) and p(h) are
each a monotone cubic (PCHIP) spline through num_control_points free
values, able to represent a barrel/hourglass shape or a non-monotonically
varying pitch, not just two tapered ends. A spline profile has no closed-form
winding-angle mapping, so unlike the conical curve designer, every candidate
the search evaluates is simulated with the library's real, general
(root-solve-based) machinery — expect this search to take minutes to hours
rather than seconds, and prefer ConicalCurveCompressionSpringInverseDesigner
whenever a simple taper already fits the target curve.
| Member | Description |
|---|---|
GeneralCompressionSpringInverseDesigner(requirements, type_of_end=..., num_control_points=4, spring_index_bounds=(4.5, 12.0), wire_diameter_bounds=(0.3, 10.0), diameter_bounds=(3.0, 150.0), pitch_bounds=(0.3, 40.0), free_length_margin=(1.05, 3.0), min_coils=2.0, safety_factor_weight=1.0, penalty_weight=0.05, search_num_points=20, search_steps=15, final_num_points=500, final_steps=500, maxiter=15, popsize=6, polish_maxiter=60, seed=None, workers=1, number_cycles=1_000_000, shot_peening=False) |
Construct the designer with the same CompressionCurveRequirements used by the conical curve designer. workers parallelizes the global search across processes (default 1). |
.design() |
Runs the regression and returns a GeneralCurveInverseDesign. |
GeneralCurveInverseDesign.spring |
The winning, fully built CompressionSpringGeneral. |
.diameter_control_points / .pitch_control_points / .control_positions |
The fitted profile's control values (Quantity arrays) and the axial positions (Quantity, mm) they sit at — there's no single start/end pair for a general shape. |
.diameter_at(h) / .pitch_at(h) |
Evaluate the fitted spline at any axial position h (a Quantity). |
.wire_diameter / .free_length / .nr_coils / .solid_length |
Resulting geometry, as Quantity/float. |
.safety_factor / .safety_factor_target / .safety_factor_error |
Achieved vs. target safety factor. |
.curve_rmse / .curve_rmse_relative |
Fit quality, same meaning as the conical curve designer's fields. |
.target_displacement / .target_load / .simulated_displacement / .simulated_load |
Both curves as numpy arrays. |
spline_profile(control_h_mm, control_values_mm, free_length_mm) |
Module function: builds a f_mean_diameter/f_pitch-compatible closure for a PCHIP spline through control points — handy for feeding the winning geometry into CompressionSpringGeneral.set_geometry() directly. |
from springcalc.inverse_calc.conical_curve_comp_inv import CompressionCurveRequirements
from springcalc.inverse_calc.general_comp_inv import GeneralCompressionSpringInverseDesigner
from springcalc.pymodels.material import Material
material = Material(material_name="SL")
requirements = CompressionCurveRequirements(
material=material, safety_factor=1.3, csv_path="target_curve.csv", # displacement,load columns (mm, N)
)
result = GeneralCompressionSpringInverseDesigner(requirements, seed=0).design()
print(result.curve_rmse, result.curve_rmse_relative)
print(result.diameter_control_points, result.pitch_control_points)
print(result.diameter_at(result.free_length / 2))
Tests
uv run pytest
Retraining the regression models
The included JSON coefficient files are already fitted. To regenerate them:
uv run python scripts/regresiones/factor_f/factor_f.py
Metadata
Release files for springcalc 0.1.24
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| springcalc-0.1.24.tar.gz | 2.6 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| springcalc-0.1.24-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 2.7 MB
Release files / springcalc-0.1.24.tar.gz
| Download URL | springcalc-0.1.24.tar.gz |
|---|---|
| Size | 2.6 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7f1f1e6ac7ef2cec4e43c90bdff90f93ab1784804a8f367ec07d06a3448d7bd3
|
|
BLAKE2b-256 checksum How to use checksums |
864bbf1070d08b85813969a3ae7861ba8ab1fcebf6e052533ea69f05fb331e7d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.12
|
Release files / springcalc-0.1.24-py3-none-any.whl
| Download URL | springcalc-0.1.24-py3-none-any.whl |
|---|---|
| Size | 110.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5bcfba4d90d20583dc97aefccd1932cca34488e9ba152419449adb201b43a77f
|
|
BLAKE2b-256 checksum How to use checksums |
a70e7b23f9bf8a6494e065303b3c60cb4c970856254b5436466857f569fc1362
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.12
|