ConstitutiveLaw.preprocess_strains_with_limits snaps strains that fall within a
small tolerance of an ultimate-strain limit onto that exact limit. It runs at the
top of every constitutive law's get_stress/get_tangent, so the integrators
call it every time they evaluate stress or tangential stiffness — for the fiber
integrator, once per fiber on each iteration of the equilibrium solver (typically
tens of thousands of strain values per moment curvature calculation).
On every call it does more work than needed: it runs np.isclose twice and
allocates two np.zeros_like(eps) arrays, even though it is only comparing each
strain against the limit.
idxs = np.isclose(eps, np.zeros_like(eps) + eps_max, atol=1e-6)
eps[idxs] = eps_max
idxs = np.isclose(eps, np.zeros_like(eps) + eps_min, atol=1e-6)
eps[idxs] = eps_min
preprocess_strains_with_limits reads the limits once per call via
get_ultimate_strain(), so within a call the closeness threshold is a single
scalar shared by every value in eps. np.isclose(a, b) tests
|a - b| <= atol + rtol * |b|
(numpy docs);
here b is the limit, atol=1e-6 is passed and rtol keeps its default 1e-5,
so the threshold is just 1e-6 + 1e-5 * |limit|. Precomputing that scalar once
per call and comparing with a plain abs(...) <= tol gives bit-identical masks
for every law — it removes only the per-element np.isclose machinery and the
zeros_like allocations, not any behaviour.
Fiber benefits most — every fiber goes through this method. For Marin it usually
only benefits the rebars, except when Marin falls back to discretizing a
constitutive law whose stress–strain curve isn't a polynomial.
Example: 40 randomly generated sections, fiber integrator
Each section is randomly rectangular or circular (50/50), with parameters drawn
from:
| parameter |
rectangular |
circular |
| outer size [mm] |
width 200–500, height 300–800 |
diameter 300–800 |
| cover [mm] |
30–50 |
30–50 |
| bar diameter [mm] |
16 / 20 / 25 / 32 |
16 / 20 / 25 / 32 |
| number of bars |
2–5 per layer (top + bottom) |
6–12 around perimeter |
| concrete class |
C25–C45 |
C25–C45 |
Each section runs bending capacity + moment–curvature + N–M interaction domain,
with the current np.isclose implementation timed against the
precomputed-threshold version (identical results on every section):
| metric |
per-section speedup |
| calculation time |
median 1.82×, range 1.56–2.15× (p10 1.69, p90 2.04) |
So roughly a ~1.8× faster fiber calculation.
ConstitutiveLaw.preprocess_strains_with_limitssnaps strains that fall within asmall tolerance of an ultimate-strain limit onto that exact limit. It runs at the
top of every constitutive law's
get_stress/get_tangent, so the integratorscall it every time they evaluate stress or tangential stiffness — for the fiber
integrator, once per fiber on each iteration of the equilibrium solver (typically
tens of thousands of strain values per moment curvature calculation).
On every call it does more work than needed: it runs
np.isclosetwice andallocates two
np.zeros_like(eps)arrays, even though it is only comparing eachstrain against the limit.
preprocess_strains_with_limitsreads the limits once per call viaget_ultimate_strain(), so within a call the closeness threshold is a singlescalar shared by every value in
eps.np.isclose(a, b)tests|a - b| <= atol + rtol * |b|(numpy docs);
here
bis the limit,atol=1e-6is passed andrtolkeeps its default1e-5,so the threshold is just
1e-6 + 1e-5 * |limit|. Precomputing that scalar onceper call and comparing with a plain
abs(...) <= tolgives bit-identical masksfor every law — it removes only the per-element
np.isclosemachinery and thezeros_likeallocations, not any behaviour.Fiber benefits most — every fiber goes through this method. For Marin it usually
only benefits the rebars, except when Marin falls back to discretizing a
constitutive law whose stress–strain curve isn't a polynomial.
Example: 40 randomly generated sections, fiber integrator
Each section is randomly rectangular or circular (50/50), with parameters drawn
from:
Each section runs bending capacity + moment–curvature + N–M interaction domain,
with the current
np.iscloseimplementation timed against theprecomputed-threshold version (identical results on every section):
So roughly a ~1.8× faster fiber calculation.