# -*- coding: utf-8 -*- """The PCI Design Aid 15.2.3 strand power curve and the strain-compatibility flexure it feeds -- docs/eriksson-wall-algorithms.md sec.4.2. ADDITIVE. `pci.fps_approximate_ksi` (the ACI approximate f_ps that the shared `capacity_at`/`phi_mn_lbft` and Panel Lift Check keep) is untouched; these pin the more exact law the reference tool (Eriksson Wall) uses and that the additive `handling_flexure_at` (Panel Analysis) now prefers. """ import pytest from panellift import capacity, pci from panellift.model import Panel, Prestress, Stage, Strand # ------------------------------------------------------- the pure power curve def test_fps_power_curve_elastic_branch(): """eps_ps <= 0.0085 is elastic: f_ps = Ep*eps_ps. 0.005 * 28500 = 142.5 ksi. At the limit itself, 0.0085 * 28500 = 242.25. """ assert pci.fps_power_curve_ksi(0.005, 270.0) == pytest.approx(142.5) assert pci.fps_power_curve_ksi(0.0085, 270.0) == pytest.approx(242.25) def test_fps_power_curve_power_branch(): """eps_ps > 0.0085: f_ps = f_pu - 0.04/(eps_ps - 0.007). At 0.010: 270 - 0.04/(0.010 - 0.007) = 270 - 13.3333 = 256.6667 ksi. At 0.020: 270 - 0.04/0.013 = 270 - 3.0769 = 266.9231 ksi. """ assert pci.fps_power_curve_ksi(0.010, 270.0) == pytest.approx(256.6667, abs=1e-3) assert pci.fps_power_curve_ksi(0.020, 270.0) == pytest.approx(266.9231, abs=1e-3) def test_fps_power_curve_is_nearly_continuous_at_the_knee(): """The two branches meet within ~1 ksi at eps_ps = 0.0085 -- one smooth law, not a step. Elastic 242.25; power 270 - 0.04/0.0015 = 243.33.""" elastic = pci.fps_power_curve_ksi(0.0085, 270.0) power = 270.0 - 0.04 / (0.0085 - 0.007) assert abs(power - elastic) < 1.1 def test_fps_power_curve_caps_at_fpu_and_floors_at_zero(): # The power branch approaches f_pu from below but never reaches it: at # eps = 1.0, 270 - 0.04/0.993 = 269.96 ksi, under f_pu. assert pci.fps_power_curve_ksi(1.0, 270.0) == pytest.approx(269.96, abs=0.01) assert pci.fps_power_curve_ksi(1.0, 270.0) < 270.0 # The cap DOES bind if the elastic branch is pushed past f_pu (an out-of- # range Ep): 0.005 * 100000 = 500 ksi -> capped to f_pu = 270. assert pci.fps_power_curve_ksi(0.005, 270.0, ep_ksi=100000.0) \ == pytest.approx(270.0) # Non-positive strain is no stress, not a negative one. assert pci.fps_power_curve_ksi(0.0, 270.0) == 0.0 assert pci.fps_power_curve_ksi(-0.001, 270.0) == 0.0 # --------------------------------------- strain-compat Mn on a solid panel def _solid_prestressed_panel(): """A plain solid panel, one bottom strand row, no openings -- so the power-curve solve has a single fully-developed tension layer and can be reproduced by an independent bisection in the test.""" strands = [Strand(y=2.0, z_in=1.5, area_in2=0.153, initial_pull_lb=28900.0), Strand(y=6.0, z_in=1.5, area_in2=0.153, initial_pull_lb=28900.0)] pre = Prestress(strands=strands, loss_pct=0.0, diameter_in=0.5, fpu_ksi=270.0) return Panel(mark='S', length_ft=24.0, width_ft=8.0, thickness_in=8.0, unit_weight_pcf=150.0, fci_psi=3500.0, fc_psi=6000.0, prestress=pre) def test_power_curve_mn_matches_an_independent_strain_compat_solve(): """`capacity_at` stores a power-curve M_n; reproduce it from the same law. Midspan, fully developed: every strand sees the full power-curve f_ps at its own strain, and equilibrium 0.85 f'ci beta1 c b = sum(A f_ps) fixes c. The test re-solves that balance independently and checks the stored M_n. """ panel = _solid_prestressed_panel() stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0) x = 12.0 cap = capacity.capacity_at(panel, stage, x, moment_lbft=1.0, shear_lb=0.0) assert cap.mn_powercurve_lbft is not None # Independent solve, same inputs: two identical strands at d = 8 - 1.5 = # 6.5 in, f_se = 28900/0.153/1000 = 188.9 ksi, eps_se = f_se/28500. import math fc = 3500.0 b = panel.thickness_in and (panel.width_ft * 12.0) # solid, full width beta1 = pci.beta1(fc) ep = pci.STRAND_ELASTIC_MODULUS_KSI d = 8.0 - 1.5 area = 0.153 * 2 fse = 28900.0 / 0.153 / 1000.0 eps_se = fse / ep def tension(c): eps = eps_se + 0.003 * (d - c) / c return area * pci.fps_power_curve_ksi(eps, 270.0, ep) * 1000.0 lo, hi = 1e-4, d for _ in range(80): mid = 0.5 * (lo + hi) if 0.85 * fc * beta1 * mid * b - tension(mid) > 0.0: hi = mid else: lo = mid c = 0.5 * (lo + hi) a = beta1 * c mn = tension(c) * (d - a / 2.0) / 12.0 assert cap.mn_powercurve_lbft == pytest.approx(mn, rel=1e-3) def test_handling_flexure_prefers_the_power_curve_mn(): """handling_flexure_at reads the power-curve M_n, not the ACI-approximate one, when the point carries it -- and applies the strain-based phi to it.""" panel = _solid_prestressed_panel() stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0) x = 12.0 cap = capacity.capacity_at(panel, stage, x, moment_lbft=4000.0, shear_lb=0.0) phi, phi_mn, ratio = capacity.handling_flexure_at(cap, 4000.0) # Fully developed, low rho -> tension-controlled -> phi 0.90, and phi*M_n # is 0.90 times the stored power-curve M_n, NOT the approximate phi_mn_lbft. assert phi == pytest.approx(0.90) assert phi_mn == pytest.approx(0.90 * cap.mn_powercurve_lbft) assert phi_mn != pytest.approx(cap.phi_mn_lbft) # differs from approximate assert ratio == pytest.approx(4000.0 / phi_mn) def test_power_curve_mn_is_none_without_developed_strand(): """A free end has no developed area, so no power-curve M_n is stored.""" panel = _solid_prestressed_panel() stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0) cap = capacity.capacity_at(panel, stage, x=0.0, moment_lbft=1.0, shear_lb=0.0) assert cap.mn_powercurve_lbft is None def test_power_curve_free_end_flexure_is_not_a_full_phi_mn(): """The development cap keeps a station near a free end from claiming the full midspan phi*M_n: at x just inside the transfer length the strand has only begun to develop, so phi*M_n there is well below the midspan value.""" panel = _solid_prestressed_panel() stage = Stage(name='strip', multiplier=1.4, fc_psi=3500.0) mid = capacity.capacity_at(panel, stage, 12.0, 1.0, 0.0) near = capacity.capacity_at(panel, stage, 0.5, 1.0, 0.0) assert mid.mn_powercurve_lbft is not None if near.mn_powercurve_lbft is not None: assert near.mn_powercurve_lbft < mid.mn_powercurve_lbft