From 8c55490ad91165ee566e5b81e6acc1e507eaa90b Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 23 Jun 2021 17:04:37 +1000 Subject: [PATCH 01/71] remove unused variable from three comp tools --- src/threecomphyd/evolutionary_fitter/three_comp_tools.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index ed4c932..ebd6578 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -241,8 +241,6 @@ def prepare_caen_recovery_ratios(w_p: float, cp: float): :param cp: CP :return SimpleRecMeasures Object """ - # name for returned measures - name = "caen" # estimate intensities p4 = round(cp + w_p / 240, 2) # predicted exhaustion after 4 min p8 = round(cp + w_p / 480, 2) # predicted exhaustion after 8 min From 2bc456371acbfeef8f07f37cb663f3ffc4693adf Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 23 Jun 2021 22:25:19 +1000 Subject: [PATCH 02/71] introduce first differential equation checks using equations by morton --- tests/tte_phase_tests.py | 93 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 tests/tte_phase_tests.py diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py new file mode 100644 index 0000000..6efa706 --- /dev/null +++ b/tests/tte_phase_tests.py @@ -0,0 +1,93 @@ +import logging +import math + +import numpy as np +from scipy import optimize +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + hz = 1000 + + example_conf = [11532.526538727172, + 23240.257042239595, + 249.7641585019016, + 286.26673813946095, + 7.988078323028352, + 0.25486842730772163, + 0.26874299216869681, + 0.2141766056862277 + ] + + a_anf = example_conf[0] + a_ans = example_conf[1] + m_ae = example_conf[2] + m_ans = example_conf[3] + m_anf = example_conf[4] + the = example_conf[5] + gam = example_conf[6] + phi = example_conf[7] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, + m_anf=m_anf, the=the, gam=gam, phi=phi) + + p = 250 + + # end of phase A1 -> the time whe h=theta + t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * the) / (p * (1 - phi)))) + + # simulate with the agent for estimated amount of time steps + agent.reset() + for _ in range(int(t1 * hz)): + agent.set_power(p) + agent.perform_one_step() + + # check if t1 time estimation lines up + assert agent.get_h() - the < 0.01 + + # taken from Equation 11 by Morton 1986 + a = (m_ae * a_ans * (1 - the - gam) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( + a_anf * a_ans * (1 - phi) * (1 - the - gam)) + + b = (m_ae * m_ans) / (a_anf * a_ans * (1 - phi) * (1 - the - gam)) + + c = (m_ans * (p * (1 - phi) - m_ae * the)) / (a_anf * a_ans * (1 - phi) * (1 - the - gam)) + + # use auxillary equation to derive both negative roots r1 and r2 + coeff = [1, a, b] + r1, r2 = np.roots(coeff) + + + def eq_12_and_derivative(p): + c1, c2 = p + return (c1 * math.exp(r1 * t1) + c2 * math.exp(r2 * t1) + c / b, + r1 * c1 * math.exp(r1 * t1) + r2 * c2 * math.exp(r2 * t1)) + + + # solve for c1 and c2 + res = optimize.fsolve(eq_12_and_derivative, x0=np.array([1, 1])) + s_c1 = float(res[0]) + s_c2 = float(res[1]) + + + def eq_9_substituted(t): + k1 = r1 * s_c1 * ((a_ans * (1 - the - gam)) / m_ans) + s_c1 + k2 = r2 * s_c2 * ((a_ans * (1 - the - gam)) / m_ans) + s_c2 + h = k1 * math.exp(r1 * t) + k2 * math.exp(r2 * t) + c / b + the + return h - (1 - gam) + + + t2 = optimize.fsolve(eq_9_substituted, x0=np.array([1]))[0] + + # simulate with the agent for estimated amount of time steps + agent.reset() + for _ in range(int((t2) * hz)): + agent.set_power(p) + agent.perform_one_step() + + # check if t1 time estimation lines up + assert agent.get_h() - (1 - gam) < 0.01 From 5b0de55565aec3ff0b514994c1b67e66f6ea34b6 Mon Sep 17 00:00:00 2001 From: faweigend Date: Sat, 26 Jun 2021 21:23:26 +1000 Subject: [PATCH 03/71] add phase A4 to TTE verification and move previous phases into functions --- tests/tte_phase_tests.py | 162 +++++++++++++++++++++++++++++---------- 1 file changed, 121 insertions(+), 41 deletions(-) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 6efa706..38370b3 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -5,50 +5,24 @@ from scipy import optimize from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - hz = 1000 - - example_conf = [11532.526538727172, - 23240.257042239595, - 249.7641585019016, - 286.26673813946095, - 7.988078323028352, - 0.25486842730772163, - 0.26874299216869681, - 0.2141766056862277 - ] - - a_anf = example_conf[0] - a_ans = example_conf[1] - m_ae = example_conf[2] - m_ans = example_conf[3] - m_anf = example_conf[4] - the = example_conf[5] - gam = example_conf[6] - phi = example_conf[7] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, - m_anf=m_anf, the=the, gam=gam, phi=phi) - - p = 250 +def phase_a1(): # end of phase A1 -> the time whe h=theta - t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * the) / (p * (1 - phi)))) + t_end = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * the) / (p * (1 - phi)))) # simulate with the agent for estimated amount of time steps agent.reset() - for _ in range(int(t1 * hz)): + for _ in range(int(t_end * hz)): agent.set_power(p) agent.perform_one_step() # check if t1 time estimation lines up - assert agent.get_h() - the < 0.01 + assert abs(agent.get_h() - the) < 0.001 + + return t_end + +def phase_a2(): # taken from Equation 11 by Morton 1986 a = (m_ae * a_ans * (1 - the - gam) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( a_anf * a_ans * (1 - phi) * (1 - the - gam)) @@ -61,33 +35,139 @@ coeff = [1, a, b] r1, r2 = np.roots(coeff) - def eq_12_and_derivative(p): c1, c2 = p return (c1 * math.exp(r1 * t1) + c2 * math.exp(r2 * t1) + c / b, r1 * c1 * math.exp(r1 * t1) + r2 * c2 * math.exp(r2 * t1)) - # solve for c1 and c2 res = optimize.fsolve(eq_12_and_derivative, x0=np.array([1, 1])) s_c1 = float(res[0]) s_c2 = float(res[1]) - def eq_9_substituted(t): k1 = r1 * s_c1 * ((a_ans * (1 - the - gam)) / m_ans) + s_c1 k2 = r2 * s_c2 * ((a_ans * (1 - the - gam)) / m_ans) + s_c2 h = k1 * math.exp(r1 * t) + k2 * math.exp(r2 * t) + c / b + the return h - (1 - gam) - - t2 = optimize.fsolve(eq_9_substituted, x0=np.array([1]))[0] + t_end = optimize.fsolve(eq_9_substituted, x0=np.array([1]))[0] # simulate with the agent for estimated amount of time steps agent.reset() - for _ in range(int((t2) * hz)): + for _ in range(int((t_end) * hz)): agent.set_power(p) agent.perform_one_step() # check if t1 time estimation lines up - assert agent.get_h() - (1 - gam) < 0.01 + assert abs(agent.get_h() - (1 - gam)) < 0.001 + + # use t2 to get l(t2) + l_t2 = s_c1 * math.exp(r1 * t_end) + s_c2 * math.exp(r2 * t_end) + c / b + + return l_t2, t_end + + +def phase_a4(l_t2, t_end_t2): + # PHASE A4 + + # FIRST: Determine dynamics of l(t) + def a4_lt(cx, t): + return (1 - the - gam) + cx * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) + + # solve EQ(20) + def eq_20(c3): + # fit to t2 and known l(t2) + l_est_t2 = a4_lt(c3, t_end_t2) + return abs(l_t2 - l_est_t2) + + # find root + s_c3 = optimize.fsolve(eq_20, x0=np.array([1]))[0] + + # check if C3 estimation lines up + t_l0 = optimize.fsolve(lambda t: abs((1 - the - gam - 0.1) - a4_lt(s_c3, t)), x0=np.array([t_end_t2]))[0] + # simulate with the agent for estimated amount of time steps + agent.reset() + for _ in range(int(t_l0 * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_g() - (1 - the - gam - 0.1)) < 0.01 + + # SECOND: Determine dynamics of h(t) + + # as defined for EQ(21) + k = m_ans / ((1 - the - gam) * a_ans) + a = -m_ae / ((1 - phi) * a_anf) + b = (m_ans * s_c3) / ((1 - the - gam) * a_anf) + g = p / a_anf + + def a4_ht(cx, t): + return ((-b * math.exp(-k * t)) / (a + k)) + (cx * math.exp(a * t)) - (g / a) + + def eq_21_v3(c4): + # we know that at t2 h=(1-gamma) + h_act_t2 = (1 - gam) + h_est_t2 = a4_ht(c4, t_end_t2) + return abs(h_act_t2 - h_est_t2) + + s_c4 = optimize.fsolve(eq_21_v3, x0=np.array([1]))[0] + + # double-check with first derivative and time step t2+1 + h_act_t2 = (1 - gam) + dh_act = ( + ((-m_ae * (1 - gam)) / ((1 - phi) * a_anf)) - + ((m_ans * (1 - the - gam - l_t2)) / ((1 - the - gam) * a_anf)) + + (p / a_anf) + ) + h_act_t2p = h_act_t2 + dh_act + assert abs(a4_ht(s_c4, t_end_t2 + 1) - h_act_t2p) < 0.001 + + # find time point where h=(1-phi) + t_end = optimize.fsolve(lambda t: abs((1 - phi) - a4_ht(s_c4, t)), x0=np.array([t_end_t2]))[0] + + # check if t4 time estimation lines up + agent.reset() + for _ in range(int(t_end * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - (1 - phi)) < 0.001 + + # finally return t4 result + return t_end + + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + hz = 40 + + example_conf = [11532.526538727172, + 543240.257042239595, + 249.7641585019016, + 286.26673813946095, + 7.988078323028352, + 0.25486842730772163, + 0.26874299216869681, + 0.2141766056862277] + + a_anf = example_conf[0] + a_ans = example_conf[1] + m_ae = example_conf[2] + m_ans = example_conf[3] + m_anf = example_conf[4] + the = example_conf[5] + gam = example_conf[6] + phi = example_conf[7] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, + m_anf=m_anf, the=the, gam=gam, phi=phi) + + p = 400 + + # all TTE phases + t1 = phase_a1() + lt2, t2 = phase_a2() + t4 = phase_a4(lt2, t2) From 0e3bbe9cab27108fa8bb8a570efb7fba6eb71dc9 Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 29 Jun 2021 18:51:03 +1000 Subject: [PATCH 04/71] add phase A3 to TTE verification --- tests/tte_phase_tests.py | 171 +++++++++++++++++++++++++++++---------- 1 file changed, 127 insertions(+), 44 deletions(-) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 38370b3..c7731e7 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -4,6 +4,7 @@ import numpy as np from scipy import optimize from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation def phase_a1(): @@ -22,7 +23,7 @@ def phase_a1(): return t_end -def phase_a2(): +def phase_a2(t1): # taken from Equation 11 by Morton 1986 a = (m_ae * a_ans * (1 - the - gam) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( a_anf * a_ans * (1 - phi) * (1 - the - gam)) @@ -45,13 +46,18 @@ def eq_12_and_derivative(p): s_c1 = float(res[0]) s_c2 = float(res[1]) - def eq_9_substituted(t): + # substitute into equation for h + def a2_ht(t): k1 = r1 * s_c1 * ((a_ans * (1 - the - gam)) / m_ans) + s_c1 k2 = r2 * s_c2 * ((a_ans * (1 - the - gam)) / m_ans) + s_c2 - h = k1 * math.exp(r1 * t) + k2 * math.exp(r2 * t) + c / b + the - return h - (1 - gam) + return k1 * math.exp(r1 * t) + k2 * math.exp(r2 * t) + c / b + the - t_end = optimize.fsolve(eq_9_substituted, x0=np.array([1]))[0] + if phi < gam: + # the whole of AnS is utilized in phase A2 + t_end = optimize.fsolve(lambda t: abs((1 - gam) - a2_ht(t)), x0=np.array([1]))[0] + else: + # phase A2 transitions into phase A3 before AnS is empty + t_end = optimize.fsolve(lambda t: abs((1 - phi) - a2_ht(t)), x0=np.array([1]))[0] # simulate with the agent for estimated amount of time steps agent.reset() @@ -59,39 +65,82 @@ def eq_9_substituted(t): agent.set_power(p) agent.perform_one_step() - # check if t1 time estimation lines up - assert abs(agent.get_h() - (1 - gam)) < 0.001 + assert abs(agent.get_h() - (1 - max(phi, gam))) < 0.001 # use t2 to get l(t2) l_t2 = s_c1 * math.exp(r1 * t_end) + s_c2 * math.exp(r2 * t_end) + c / b - return l_t2, t_end + return t_end, l_t2 + + +def phase_a3(t2, lt2): + # phase A3 is not applicable if gamma is greater or equal to phi + if gam >= phi: + return t2, lt2 + + a = ((a_anf + a_ans) * m_ans) / (a_anf * a_ans * (1 - the - gam)) + b = ((p - m_ae) * m_ans) / (a_anf * a_ans * (1 - the - gam)) + + def a3_lt(c1, c2, t): + return (((b * t) + (c1 * math.exp(-a * t))) / a) + c2 + + def a3_dlt(c1, c2, t): + return (b / a) - c1 * math.exp(-a * t) + + # derivative l'(t2) can be calculated + dlt2 = (m_ans * ((1 - phi) - lt2 - the)) / (a_ans * (1 - the - gam)) + + def general_l(p): + c1, c2 = p + est_lt2 = a3_lt(c1, c2, t2) + est_dlt2 = a3_dlt(c1, c2, t2 + 1) + # add a small tolerance for derivative estimation + com_dl = int(abs(est_dlt2 - dlt2) > 0.001) + return abs(est_lt2 - lt2), com_dl + + # solve for c1 and c2 + res = optimize.fsolve(general_l, x0=np.array([0, 0])) + s_c1 = float(res[0]) + s_c2 = float(res[1]) + + def a3_ht(t): + return ((a_ans * (1 - the - gam)) / m_ans) * a3_dlt(s_c1, s_c2, t) + a3_lt(s_c1, s_c2, t) + the + + # find end of A3 where h(t3) = (1-gamma) + t3 = optimize.fsolve(lambda t: abs((1 - gam) - a3_ht(t)), x0=np.array([t2]))[0] + + # check if t4 time estimation lines up + agent.reset() + for _ in range(int(t3 * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - (1 - gam)) < 0.001 + # return t3 and l(t3) + return t3, a3_lt(s_c1, s_c2, t3) -def phase_a4(l_t2, t_end_t2): + +def phase_a4(t2, lt2): # PHASE A4 + # phase A4 is not applicable if phi is greater or equal to gamma + if phi >= gam: + return t2, lt2 # FIRST: Determine dynamics of l(t) def a4_lt(cx, t): return (1 - the - gam) + cx * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) - # solve EQ(20) - def eq_20(c3): - # fit to t2 and known l(t2) - l_est_t2 = a4_lt(c3, t_end_t2) - return abs(l_t2 - l_est_t2) - - # find root - s_c3 = optimize.fsolve(eq_20, x0=np.array([1]))[0] + # find c that fits to known l(t2) = l + s_c3 = optimize.fsolve(lambda c: abs(lt2 - a4_lt(c, t2)), x0=np.array([1]))[0] - # check if C3 estimation lines up - t_l0 = optimize.fsolve(lambda t: abs((1 - the - gam - 0.1) - a4_lt(s_c3, t)), x0=np.array([t_end_t2]))[0] + # double-check if C3 estimation lines up + t_l0 = optimize.fsolve(lambda t: abs((1 - the - gam - 0.1) - a4_lt(s_c3, t)), x0=np.array([t2]))[0] # simulate with the agent for estimated amount of time steps agent.reset() for _ in range(int(t_l0 * hz)): agent.set_power(p) agent.perform_one_step() - assert abs(agent.get_g() - (1 - the - gam - 0.1)) < 0.01 + assert abs(agent.get_g() - (1 - the - gam - 0.1)) < 0.001 # SECOND: Determine dynamics of h(t) @@ -104,26 +153,21 @@ def eq_20(c3): def a4_ht(cx, t): return ((-b * math.exp(-k * t)) / (a + k)) + (cx * math.exp(a * t)) - (g / a) - def eq_21_v3(c4): - # we know that at t2 h=(1-gamma) - h_act_t2 = (1 - gam) - h_est_t2 = a4_ht(c4, t_end_t2) - return abs(h_act_t2 - h_est_t2) - - s_c4 = optimize.fsolve(eq_21_v3, x0=np.array([1]))[0] + # find c4 that matches h(t2) = (1-gamma) + s_c4 = optimize.fsolve(lambda c: abs((1 - gam) - a4_ht(c, t2)), x0=np.array([1]))[0] # double-check with first derivative and time step t2+1 - h_act_t2 = (1 - gam) - dh_act = ( - ((-m_ae * (1 - gam)) / ((1 - phi) * a_anf)) - - ((m_ans * (1 - the - gam - l_t2)) / ((1 - the - gam) * a_anf)) + - (p / a_anf) - ) - h_act_t2p = h_act_t2 + dh_act - assert abs(a4_ht(s_c4, t_end_t2 + 1) - h_act_t2p) < 0.001 - - # find time point where h=(1-phi) - t_end = optimize.fsolve(lambda t: abs((1 - phi) - a4_ht(s_c4, t)), x0=np.array([t_end_t2]))[0] + # h_act_t2 = (1 - gam) + # dh_act = ( + # ((-m_ae * (1 - gam)) / ((1 - phi) * a_anf)) - + # ((m_ans * (1 - the - gam - lt2)) / ((1 - the - gam) * a_anf)) + + # (p / a_anf) + # ) + # h_act_t2p = h_act_t2 + dh_act + # assert abs(a4_ht(s_c4, t2 + 1) - h_act_t2p) < 0.0001 + + # find end of phase A4. The time point where h(t4)=(1-phi) + t_end = optimize.fsolve(lambda t: abs((1 - phi) - a4_ht(s_c4, t)), x0=np.array([t2]))[0] # check if t4 time estimation lines up agent.reset() @@ -132,7 +176,40 @@ def eq_21_v3(c4): agent.perform_one_step() assert abs(agent.get_h() - (1 - phi)) < 0.001 + lt_end = a4_lt(s_c3, t_end) # finally return t4 result + return t_end, lt_end + + +def phase_a5(t4, lt4): + # FIRST: Determine dynamics of l(t) + def a5_lt(cx, t): + return (1 - the - gam) + cx * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) + + # find c that fits to known l(t4) = given l + s_cl = optimize.fsolve(lambda c: abs(lt4 - a5_lt(c, t4)), x0=np.array([1]))[0] + + # SECOND: determine dynamics of h(t) + k = m_ans / ((1 - the - gam) * a_ans) + a = (p - m_ae) / a_anf + b = (m_ans * s_cl) / ((1 - the - gam) * a_anf) + + def a5_ht(cx, t): + return (a * t) - ((b * math.exp(-k * t)) / k) + cx + + # find c5 that matches h(t4) = (1-phi) + s_ch = optimize.fsolve(lambda c: abs((1 - phi) - a5_ht(c, t4)), x0=np.array([1]))[0] + + # find end of phase A5. The time point where h(t5)=1 + t_end = optimize.fsolve(lambda t: abs(1 - a5_ht(s_ch, t)) + int(t < t4), x0=np.array([t4]))[0] + + # check if t5 time estimation lines up + agent.reset() + for _ in range(int(t_end * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - 1) < 0.001 + return t_end @@ -141,16 +218,16 @@ def eq_21_v3(c4): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - hz = 40 + hz = 50 example_conf = [11532.526538727172, - 543240.257042239595, + 424324.257042239595, 249.7641585019016, 286.26673813946095, 7.988078323028352, 0.25486842730772163, - 0.26874299216869681, - 0.2141766056862277] + 0.2141766056862277, + 0.2641766056862277] a_anf = example_conf[0] a_ans = example_conf[1] @@ -165,9 +242,15 @@ def eq_21_v3(c4): agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, m_anf=m_anf, the=the, gam=gam, phi=phi) + # ThreeCompVisualisation(agent) + p = 400 # all TTE phases t1 = phase_a1() - lt2, t2 = phase_a2() - t4 = phase_a4(lt2, t2) + t2, lt2 = phase_a2(t1) + t3, lt3 = phase_a3(t2, lt2) + t4, lt4 = phase_a4(t3, lt3) + t5 = phase_a5(t4, lt4) + + print(t5) \ No newline at end of file From f9a8e4d28d5ea62a5856b4efff134cb471a1347c Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 29 Jun 2021 19:51:58 +1000 Subject: [PATCH 05/71] clean differential equation tests up and insert configuration that breaks the 5 phases --- tests/tte_phase_tests.py | 173 ++++++++++++++++++++------------------- 1 file changed, 87 insertions(+), 86 deletions(-) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index c7731e7..d46e53f 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -8,19 +8,9 @@ def phase_a1(): - # end of phase A1 -> the time whe h=theta - t_end = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * the) / (p * (1 - phi)))) - - # simulate with the agent for estimated amount of time steps - agent.reset() - for _ in range(int(t_end * hz)): - agent.set_power(p) - agent.perform_one_step() - - # check if t1 time estimation lines up - assert abs(agent.get_h() - the) < 0.001 - - return t_end + # end of phase A1 -> the time when h(t)=theta + t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * the) / (p * (1 - phi)))) + return t1 def phase_a2(t1): @@ -36,41 +26,32 @@ def phase_a2(t1): coeff = [1, a, b] r1, r2 = np.roots(coeff) - def eq_12_and_derivative(p): - c1, c2 = p - return (c1 * math.exp(r1 * t1) + c2 * math.exp(r2 * t1) + c / b, - r1 * c1 * math.exp(r1 * t1) + r2 * c2 * math.exp(r2 * t1)) + def a2_lt(c1, c2, t): + # the general solution for l(t) + return c1 * math.exp(r1 * t) + c2 * math.exp(r2 * t) + c / b - # solve for c1 and c2 - res = optimize.fsolve(eq_12_and_derivative, x0=np.array([1, 1])) + def a2_dlt(c1, c2, t): + # derivative of general solution + return r1 * c1 * math.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) + + # l(t1) and dl(t1) equal to 0. Find c1 and c2 that satisfy that + res = optimize.fsolve(lambda cx: (a2_lt(cx[0], cx[1], t1), a2_dlt(cx[0], cx[1], t1)), x0=np.array([1, 1])) s_c1 = float(res[0]) s_c2 = float(res[1]) - # substitute into equation for h + # substitute into EQ(9) for h def a2_ht(t): - k1 = r1 * s_c1 * ((a_ans * (1 - the - gam)) / m_ans) + s_c1 - k2 = r2 * s_c2 * ((a_ans * (1 - the - gam)) / m_ans) + s_c2 - return k1 * math.exp(r1 * t) + k2 * math.exp(r2 * t) + c / b + the + return (a_ans * (1 - the - gam)) / m_ans * a2_dlt(s_c1, s_c2, t) + a2_lt(s_c1, s_c2, t) + the if phi < gam: # the whole of AnS is utilized in phase A2 - t_end = optimize.fsolve(lambda t: abs((1 - gam) - a2_ht(t)), x0=np.array([1]))[0] + t2 = optimize.fsolve(lambda t: abs((1 - gam) - a2_ht(t)), x0=np.array([1]))[0] else: # phase A2 transitions into phase A3 before AnS is empty - t_end = optimize.fsolve(lambda t: abs((1 - phi) - a2_ht(t)), x0=np.array([1]))[0] - - # simulate with the agent for estimated amount of time steps - agent.reset() - for _ in range(int((t_end) * hz)): - agent.set_power(p) - agent.perform_one_step() - - assert abs(agent.get_h() - (1 - max(phi, gam))) < 0.001 + t2 = optimize.fsolve(lambda t: abs((1 - phi) - a2_ht(t)), x0=np.array([1]))[0] # use t2 to get l(t2) - l_t2 = s_c1 * math.exp(r1 * t_end) + s_c2 * math.exp(r2 * t_end) + c / b - - return t_end, l_t2 + return t2, a2_lt(s_c1, s_c2, t2) def phase_a3(t2, lt2): @@ -82,20 +63,24 @@ def phase_a3(t2, lt2): b = ((p - m_ae) * m_ans) / (a_anf * a_ans * (1 - the - gam)) def a3_lt(c1, c2, t): + # general solution for l(t) return (((b * t) + (c1 * math.exp(-a * t))) / a) + c2 def a3_dlt(c1, c2, t): + # first derivative l'(t) return (b / a) - c1 * math.exp(-a * t) - # derivative l'(t2) can be calculated + # derivative l'(t2) can be calculated manually dlt2 = (m_ans * ((1 - phi) - lt2 - the)) / (a_ans * (1 - the - gam)) def general_l(p): c1, c2 = p + # l(t2) = previously estimated lt2 est_lt2 = a3_lt(c1, c2, t2) + # l'(t2) = manually estimated derivative est_dlt2 = a3_dlt(c1, c2, t2 + 1) # add a small tolerance for derivative estimation - com_dl = int(abs(est_dlt2 - dlt2) > 0.001) + com_dl = int(abs(est_dlt2 - dlt2) > 0.00001) return abs(est_lt2 - lt2), com_dl # solve for c1 and c2 @@ -104,43 +89,36 @@ def general_l(p): s_c2 = float(res[1]) def a3_ht(t): + # EQ(9) with constants for l(t) and l'(t) return ((a_ans * (1 - the - gam)) / m_ans) * a3_dlt(s_c1, s_c2, t) + a3_lt(s_c1, s_c2, t) + the # find end of A3 where h(t3) = (1-gamma) t3 = optimize.fsolve(lambda t: abs((1 - gam) - a3_ht(t)), x0=np.array([t2]))[0] - # check if t4 time estimation lines up - agent.reset() - for _ in range(int(t3 * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - (1 - gam)) < 0.001 - # return t3 and l(t3) return t3, a3_lt(s_c1, s_c2, t3) def phase_a4(t2, lt2): - # PHASE A4 # phase A4 is not applicable if phi is greater or equal to gamma if phi >= gam: return t2, lt2 # FIRST: Determine dynamics of l(t) - def a4_lt(cx, t): - return (1 - the - gam) + cx * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) + def a4_lt(c, t): + return (1 - the - gam) + c * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) # find c that fits to known l(t2) = l s_c3 = optimize.fsolve(lambda c: abs(lt2 - a4_lt(c, t2)), x0=np.array([1]))[0] - # double-check if C3 estimation lines up - t_l0 = optimize.fsolve(lambda t: abs((1 - the - gam - 0.1) - a4_lt(s_c3, t)), x0=np.array([t2]))[0] - # simulate with the agent for estimated amount of time steps - agent.reset() - for _ in range(int(t_l0 * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_g() - (1 - the - gam - 0.1)) < 0.001 + # # double-check if C3 estimation lines up + # t_l0 = optimize.fsolve(lambda t: abs((1 - the - gam - 0.1) - a4_lt(s_c3, t)), x0=np.array([t2]))[0] + # # simulate with the agent for estimated amount of time steps + # agent.reset() + # for _ in range(int(t_l0 * hz)): + # agent.set_power(p) + # agent.perform_one_step() + # assert abs(agent.get_g() - (1 - the - gam - 0.1)) < 0.001 # SECOND: Determine dynamics of h(t) @@ -150,8 +128,8 @@ def a4_lt(cx, t): b = (m_ans * s_c3) / ((1 - the - gam) * a_anf) g = p / a_anf - def a4_ht(cx, t): - return ((-b * math.exp(-k * t)) / (a + k)) + (cx * math.exp(a * t)) - (g / a) + def a4_ht(c, t): + return ((-b * math.exp(-k * t)) / (a + k)) + (c * math.exp(a * t)) - (g / a) # find c4 that matches h(t2) = (1-gamma) s_c4 = optimize.fsolve(lambda c: abs((1 - gam) - a4_ht(c, t2)), x0=np.array([1]))[0] @@ -167,24 +145,16 @@ def a4_ht(cx, t): # assert abs(a4_ht(s_c4, t2 + 1) - h_act_t2p) < 0.0001 # find end of phase A4. The time point where h(t4)=(1-phi) - t_end = optimize.fsolve(lambda t: abs((1 - phi) - a4_ht(s_c4, t)), x0=np.array([t2]))[0] - - # check if t4 time estimation lines up - agent.reset() - for _ in range(int(t_end * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - (1 - phi)) < 0.001 + t4 = optimize.fsolve(lambda t: abs((1 - phi) - a4_ht(s_c4, t)), x0=np.array([t2]))[0] - lt_end = a4_lt(s_c3, t_end) - # finally return t4 result - return t_end, lt_end + # finally return t4 result and l + return t4, a4_lt(s_c3, t4) def phase_a5(t4, lt4): # FIRST: Determine dynamics of l(t) - def a5_lt(cx, t): - return (1 - the - gam) + cx * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) + def a5_lt(c, t): + return (1 - the - gam) + c * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) # find c that fits to known l(t4) = given l s_cl = optimize.fsolve(lambda c: abs(lt4 - a5_lt(c, t4)), x0=np.array([1]))[0] @@ -201,16 +171,10 @@ def a5_ht(cx, t): s_ch = optimize.fsolve(lambda c: abs((1 - phi) - a5_ht(c, t4)), x0=np.array([1]))[0] # find end of phase A5. The time point where h(t5)=1 - t_end = optimize.fsolve(lambda t: abs(1 - a5_ht(s_ch, t)) + int(t < t4), x0=np.array([t4]))[0] + t5 = optimize.fsolve(lambda t: abs(1 - a5_ht(s_ch, t)) + int(t < t4), x0=np.array([t4]))[0] - # check if t5 time estimation lines up - agent.reset() - for _ in range(int(t_end * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - 1) < 0.001 - - return t_end + # return time to exhaustion and l at exhaustion + return t5, a5_lt(s_cl, t5) if __name__ == "__main__": @@ -218,16 +182,16 @@ def a5_ht(cx, t): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - hz = 50 + hz = 100 example_conf = [11532.526538727172, 424324.257042239595, 249.7641585019016, 286.26673813946095, 7.988078323028352, - 0.25486842730772163, - 0.2141766056862277, - 0.2641766056862277] + 0.45486842730772163, + 0.2641766056862277, + 0.641766056862277] a_anf = example_conf[0] a_ans = example_conf[1] @@ -242,7 +206,7 @@ def a5_ht(cx, t): agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, m_anf=m_anf, the=the, gam=gam, phi=phi) - # ThreeCompVisualisation(agent) + ThreeCompVisualisation(agent) p = 400 @@ -251,6 +215,43 @@ def a5_ht(cx, t): t2, lt2 = phase_a2(t1) t3, lt3 = phase_a3(t2, lt2) t4, lt4 = phase_a4(t3, lt3) - t5 = phase_a5(t4, lt4) + t5, lt5 = phase_a5(t4, lt4) + + # confirm with assert tests + eps = 0.001 + # A1 + agent.reset() + for _ in range(int(t1 * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - the) < eps + # A2 + agent.reset() + for _ in range(int(t2 * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - (1 - max(phi, gam))) < eps + # A3 + agent.reset() + for _ in range(int(t3 * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - (1 - gam)) < eps + # A4 + agent.reset() + for _ in range(int(t4 * hz)): + agent.set_power(p) + agent.perform_one_step() + if phi >= gam: + assert abs(agent.get_h() - (1 - gam)) < eps + else: + # A4 is only different from t3 if phi >= gamma + assert abs(agent.get_h() - (1 - phi)) < eps + # A5 + agent.reset() + for _ in range(int(t5 * hz)): + agent.set_power(p) + agent.perform_one_step() + assert abs(agent.get_h() - 1) < eps print(t5) \ No newline at end of file From 06cd68a0fb321791926a8489bce12c9d72ccc073 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 30 Jun 2021 11:31:56 +1000 Subject: [PATCH 06/71] move data storage directory configuration into example scripts for clarification --- example_scripts/evolutionary_fitting.py | 11 +++++++++-- .../grid_search_evolutionary_parameters.py | 16 +++++++++++++--- example_scripts/pygmo_fittings_report.py | 14 +++++++++++--- src/threecomphyd/config.py | 4 +--- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/example_scripts/evolutionary_fitting.py b/example_scripts/evolutionary_fitting.py index fa98428..7aba112 100644 --- a/example_scripts/evolutionary_fitting.py +++ b/example_scripts/evolutionary_fitting.py @@ -1,14 +1,20 @@ import logging +import os from threecomphyd.evolutionary_fitter.pygmo_three_comp_fitter import PyGMOThreeCompFitter from threecomphyd.evolutionary_fitter.three_comp_tools import prepare_caen_recovery_ratios, \ prepare_standard_tte_measures +import threecomphyd.config as config + if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + # set the path to store the results into + config.paths["data_storage"] = os.path.dirname(os.path.abspath(__file__)) + "/../data-storage/" + # define (W', CP) combination in use. Insert your parameters here. comb = ( 18200, # W' @@ -25,9 +31,10 @@ fitter = PyGMOThreeCompFitter(ttes=ttes, recovery_measures=recovery_measures) - # run 10 fittings + # run 10 fittings and save results to + # config.paths["data_storage"] + "THREE_COMP_PYGMO_FIT" folder for _ in range(10): - # sets parameters accordinig to best fitting determined by grid serach + # sets parameters according to best fitting determined by grid search # see Table 3 in Appendix fitter.fit_with_moead(gen=30, cycles=40, diff --git a/example_scripts/grid_search_evolutionary_parameters.py b/example_scripts/grid_search_evolutionary_parameters.py index c0cc298..9fd643f 100644 --- a/example_scripts/grid_search_evolutionary_parameters.py +++ b/example_scripts/grid_search_evolutionary_parameters.py @@ -1,17 +1,24 @@ import logging +import os from threecomphyd.evolutionary_fitter.pygmo_three_comp_fitter import PyGMOThreeCompFitter -from threecomphyd.evolutionary_fitter.three_comp_tools import prepare_caen_recovery_ratios, prepare_standard_tte_measures +from threecomphyd.evolutionary_fitter.three_comp_tools import prepare_caen_recovery_ratios, \ + prepare_standard_tte_measures + +import threecomphyd.config as config if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + # set the path to store the results into + config.paths["data_storage"] = os.path.dirname(os.path.abspath(__file__)) + "/../data-storage/" + # group averages from Caen et al. 2019 caen = ( - 18200, # W' - 248 # CP + 18200, # W' + 248 # CP ) # define wp cp combination in use @@ -27,6 +34,9 @@ fitter = PyGMOThreeCompFitter(ttes=ttes, recovery_measures=recovery_measures) # Grid search starts here + # islands run the evolutionary algorithm in parallel and exchange solutions. One island = one thread islands = [7, 14, 21] for isl in islands: + # run grid search with given amount of islands and save results to + # config.paths["data_storage"] + "THREE_COMP_PYGMO_FIT" folder fitter.grid_search_algorithm_moead(islands=isl) diff --git a/example_scripts/pygmo_fittings_report.py b/example_scripts/pygmo_fittings_report.py index 0ee85b4..18a8338 100644 --- a/example_scripts/pygmo_fittings_report.py +++ b/example_scripts/pygmo_fittings_report.py @@ -1,16 +1,24 @@ import logging +import os + from threecomphyd.handler.pygmo_fitting_report_creator import PyGMOFittingReportCreator +import threecomphyd.config as config + if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - # Create report creator object and run report creation + # set the path to read the results from. The ReportCreator will look for a THREE_COMP_PYGMO_FIT directory + config.paths["data_storage"] = os.path.dirname(os.path.abspath(__file__)) + "/../data-storage/" + + # Create report creator object src = PyGMOFittingReportCreator() - # saves fitting results in a readable json format to the data-storage folder + # run report creation, which saves fitting results in a readable json format into the + # config.paths["data_storage"] + "PYGMO_FITTING_REPORT_CREATOR" folder src.write_data_report(clear_all=True) - # creates the grid search overview table for the Appendix + # creates the grid search overview table for the Appendix of Weigend et al. 2021 # src.create_latex_table() diff --git a/src/threecomphyd/config.py b/src/threecomphyd/config.py index daa55dd..787fcf2 100644 --- a/src/threecomphyd/config.py +++ b/src/threecomphyd/config.py @@ -1,8 +1,6 @@ -import os - # paths to be used by all scripts that store or access data paths = { - "data_storage": os.path.dirname(os.path.abspath(__file__)) + "/../../data-storage/" + "data_storage": "." } # an additional constraint on the three component hydraulic model that limits the interval for phi From 46a49c5f24938a29c7fba7ff55c8abab4fac586d Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 30 Jun 2021 11:43:39 +1000 Subject: [PATCH 07/71] add more clarification on how to use example scripts --- README.md | 68 +++++++++++++++++++++++++++++++------------------------ 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 79b1937..ad4c1e2 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,23 @@ - ![](./httpdocs/title.gif) # threecomphyd [![PyPI](https://img.shields.io/pypi/v/threecomphyd.svg?style=for-the-badge)](https://pypi.python.org/pypi/threecomphyd) -This package provides tools to model performance of athletes. The name `threecomphyd` stands for -__Three Component Hydraulic__ and describes the performance modeling approach in focus. - -Example scripts in the [GitHub repository](https://github.com/faweigend/three_comp_hyd) serve as instructions on -how to use this package. We provide a simulation agent and example simulations, functionalities for debug -plots, and an evolutionary fitting process to fit a model to individual athletes. +This package provides tools to model performance of athletes. The name `threecomphyd` stands for +__Three Component Hydraulic__ and describes the performance modeling approach in focus. +Example scripts in the [GitHub repository](https://github.com/faweigend/three_comp_hyd) serve as instructions on how to +use this package. We provide a simulation agent and example simulations, functionalities for debug plots, and an +evolutionary fitting process to fit a model to individual athletes. ### Publication -Please see the corresponding paper -[__A New Pathway to Approximate Energy Expenditure and Recovery of an Athlete__](https://arxiv.org/abs/2104.07903) for -further references. If you make use of this project, we would be grateful if -you star the repository and/or cite our paper. +Please see the corresponding paper +[__A New Pathway to Approximate Energy Expenditure and Recovery of an Athlete__](https://arxiv.org/abs/2104.07903) for +further references. If you make use of this project, we would be grateful if you star the repository and/or cite our +paper. + ``` @misc{weigend2021new, title={A New Pathway to Approximate Energy Expenditure and Recovery of an Athlete}, @@ -32,41 +31,52 @@ you star the repository and/or cite our paper. ### Setup -If you simply want to use the package, you may want to install it via `pip3 install threecomphyd` without the need -for a manual download. +If you simply want to use the package, you may want to install it via `pip3 install threecomphyd` without the need for a +manual download. -If you downloaded the source files, e.g., from this [GitHub repository](https://github.com/faweigend/three_comp_hyd), +If you downloaded the source files, e.g., from this [GitHub repository](https://github.com/faweigend/three_comp_hyd), you can install the project with a similar procedure as a package by running `pip3 install -e `. - ### Usage Hints -If you use pycharm professional, please ensure that the `SciView` tab is deactivated when running interactive animations. +Some hints on how to handle provided example applications in the `example_scripts` directory. + +#### SciView in Pycharm-Professional +If you use pycharm professional, please ensure that you deactivate the `SciView` tab when running interactive +animations. + ``` File => Settings => Tools => Python Scientific => Uncheck "show plots in tool window" ``` +#### Set Data-Storage Directory +The scripts `pygmo_fittings_report.py`, `grid_search_evolutionary_parameters.py`, and `evolutionary_fitting.py` read and +write files to the system. They will use the location indicated in `config.py` to write to and read from. By default +this is the location where you run the scripts from, but you can change it by setting the `data_storage` entry. Check in +the example scripts how to do it. + ### Example Applications Following demo applications are available via scripts in the `example_scripts` directory -* `interactive_simulation.py` lets you experiment with an exemplary three component hydraulic agent and -investigate its responses to various power demands. +* `interactive_simulation.py` lets you experiment with an exemplary three component hydraulic agent and investigate its + responses to various power demands. * `model_behaviour_plots.py` recreates the energy expenditure and recovery plots of the results section of the paper. -* `pygmo_fittings_report` Iterates over all evolutionary fitting results stored in the `data-storage` directory and creates - a readable `.json` overview over best fitting results, configurations, algorithm parameters, execution time, etc. - -WARNING! the following scripts require a system with a minimum of 7 CPU cores for the evolutionary fitting, or even 21 +* `pygmo_fittings_report.py` Iterates over all evolutionary fitting results stored in the `data-storage` directory + defined in `config` and creates a readable `.json` overview over best fitting results, configurations, algorithm + parameters, execution time, etc. + +WARNING! the following scripts require a system with a minimum of 7 CPU cores for the evolutionary fitting, or even 21 CPU cores for the grid search approach. -* `grid_search_evolutionary_parameters.py` starts a grid search over described parameter settings for MOEA/D coupled - with the asynchronous island model by Pygmo. One fitting per parameter combination as summarised in Table 3 of the Appendix - are estimated. Results are stored into a `data-storage` folder in the root directory of the project. - -* `evolutionary_fitting` Runs 10 fittings on an athlete using the in the script defined parameters for CP, W' and recovery - rations by Caen et al. The script uses the published set of best fitting parameters (30 generations, - 40 cycles, 64 population size, 7 islands) to fit the model with the outlined evolutionary computation approach +* `grid_search_evolutionary_parameters.py` starts a grid search over described parameter settings for MOEA/D coupled + with the asynchronous island model by Pygmo. One fitting per parameter combination as summarised in Table 3 of the + Appendix are estimated. Results are stored into a `data-storage` folder in the root directory of the project. + +* `evolutionary_fitting.py` Runs 10 fittings on an athlete using the in the script defined parameters for CP, W' and + recovery rations by Caen et al. The script uses the published set of best fitting parameters (30 generations, 40 + cycles, 64 population size, 7 islands) to fit the model with the outlined evolutionary computation approach (see Appendix B and C). Results are stored into a `data-storage` folder in the root directory of the project. From 5a20079985865c8090ea4fd34bddcbe16bb46b53 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 30 Jun 2021 16:27:37 +1000 Subject: [PATCH 08/71] Introduce ODE simulator. reorganise phases and introduce new A2. Fix order in educated guess --- .../evolutionary_fitter/three_comp_tools.py | 2 +- .../simulator/ode_three_comp_hyd_simulator.py | 178 +++++++++++ tests/tte_phase_tests.py | 300 +++++------------- 3 files changed, 261 insertions(+), 219 deletions(-) create mode 100644 src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index ebd6578..46f0ac7 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -57,9 +57,9 @@ def create_educated_initial_guess(self, cp: float = 250.0, w_p: float = 200000.0 np.random.normal(1, 0.4) * cp, # max flow from Ae should be related to CP np.random.normal(1, 0.4) * cp * 10, # max flow from AnS is expected to be high np.random.normal(1, 0.4) * cp * 0.1, # max recovery flow is expected to be low - np.random.normal(1, 0.4) * 0.5, # for a curvelinear expenditure dynamic the pipe has to be halfway or lower np.random.normal(1, 0.4) * 0.25, # AnS needs a considerable height np.random.normal(1, 0.4) * 0.25, # AnS needs a considerable height + np.random.normal(1, 0.4) * 0.5, # for a curvelinear expenditure dynamic the pipe has to be halfway or lower ] # ensure values are within limits for i, i_x_e in enumerate(i_x): diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py new file mode 100644 index 0000000..6e91553 --- /dev/null +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -0,0 +1,178 @@ +import math + +import numpy as np +from scipy import optimize + + +class ODEThreeCompHydSimulator: + """ + Simulates Three Component Hydraulic Model responses using Ordinary Differential Equations + """ + + @staticmethod + def phase_a1(p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> float: + # end of phase A1 -> the time when h(t) = min(theta,1-phi) + t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * min(theta, 1 - phi)) / (p * (1 - phi)))) + return t1 + + @staticmethod + def phase_a2(t1: float, p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> float: + # A2 is only applicable if phi is higher than the top of AnS + if phi <= (1 - theta): + return t1 + # linear utilization + return t1 + ((phi - (1 - theta)) * a_anf) / (p - m_ae) + + @staticmethod + def phase_a3(t2: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, theta: float, + gamma: float, phi: float) -> (float, float): + + # A3 is only applicable if flow from Ae is not at max + if phi > (1 - theta): + return t2, 0 + + # taken from Equation 11 by Morton 1986 + a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( + a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + b = (m_ae * m_ans) / ( + a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + c = (m_ans * (p * (1 - phi) - m_ae * theta)) / ( + a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + # use auxiliary equation to derive both negative roots r1 and r2 + coeff = [1, a, b] + r1, r2 = np.roots(coeff) + + def a3_gt(c1, c2, t): + # the general solution for g(t) + return c1 * math.exp(r1 * t) + c2 * math.exp(r2 * t) + c / b + + def a3_dgt(c1, c2, t): + # derivative of general solution + return r1 * c1 * math.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) + + # Find c1 and c2 that satisfy g(t2) and dg(t2) = 0 + res = optimize.fsolve(lambda cx: (a3_gt(cx[0], cx[1], t2), a3_dgt(cx[0], cx[1], t2)), x0=np.array([1, 1])) + s_c1 = float(res[0]) + s_c2 = float(res[1]) + + # substitute into EQ(9) for h + def a2_ht(t): + return (a_ans * (1 - theta - gamma)) / m_ans * a3_dgt(s_c1, s_c2, t) + a3_gt(s_c1, s_c2, t) + theta + + if phi < gamma: + # the whole of AnS is utilized in phase A3 + t3 = optimize.fsolve(lambda t: abs((1 - gamma) - a2_ht(t)), x0=np.array([1]))[0] + else: + # phase A3 transitions into phase A4 before AnS is empty + t3 = optimize.fsolve(lambda t: abs((1 - phi) - a2_ht(t)), x0=np.array([1]))[0] + + # use t3 to get g(t3) + return t3, a3_gt(s_c1, s_c2, t3) + + @staticmethod + def phase_a4(t3: float, gt3: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, + theta: float, gamma: float, phi: float) -> (float, float): + # phase A3 is not applicable if gamma is greater or equal to phi + if gamma >= phi: + return t3, gt3 + + a = ((a_anf + a_ans) * m_ans) / (a_anf * a_ans * (1 - theta - gamma)) + b = ((p - m_ae) * m_ans) / (a_anf * a_ans * (1 - theta - gamma)) + + def a4_gt(c1, c2, t): + # general solution for g(t) + return (((b * t) + (c1 * math.exp(-a * t))) / a) + c2 + + def a4_dgt(c1, c2, t): + # first derivative g'(t) + return (b / a) - c1 * math.exp(-a * t) + + # derivative g'(t3) can be calculated manually + ht3 = max(theta, 1 - phi) + dgt3 = (m_ans * (ht3 - gt3 - theta)) / (a_ans * (1 - theta - gamma)) + + def general_gt(p): + c1, c2 = p + # g(t3) = previously estimated gt3 + est_gt3 = a4_gt(c1, c2, t3) + # g'(t3) = manually estimated derivative + est_dgt3 = a4_dgt(c1, c2, t3) + return abs(est_gt3 - gt3), abs(est_dgt3 - dgt3) + + # solve for c1 and c2 + res = optimize.fsolve(general_gt, x0=np.array([0, 0])) + s_c1 = float(res[0]) + s_c2 = float(res[1]) + + def a4_ht(t): + # EQ(9) with constants for g(t) and g'(t) + return ((a_ans * (1 - theta - gamma)) / m_ans) * a4_dgt(s_c1, s_c2, t) + a4_gt(s_c1, s_c2, t) + theta + + # find end of A4 where g(t4) = (1-gamma) + t4 = optimize.fsolve(lambda t: abs((1 - gamma) - a4_ht(t)), x0=np.array([t3]))[0] + + # return t4 and g(t4) + return t4, a4_gt(s_c1, s_c2, t4) + + @staticmethod + def phase_a5(t4: float, gt4: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, + theta: float, gamma: float, phi: float) -> (float, float): + + # phase A5 is not applicable if phi is greater or equal to gamma + if phi >= gamma: + return t4, gt4 + + def a5_gt(c, t): + # generalised g(t) for phase A5 + return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + + # find c that fits to known g(t4) = gt4 + s_c1 = optimize.fsolve(lambda c: abs(gt4 - a5_gt(c, t4)), x0=np.array([1]))[0] + + # as defined for EQ(21) + k = m_ans / ((1 - theta - gamma) * a_ans) + a = -m_ae / ((1 - phi) * a_anf) + b = (m_ans * s_c1) / ((1 - theta - gamma) * a_anf) + g = p / a_anf + + def a5_ht(c, t): + return ((-b * math.exp(-k * t)) / (a + k)) + (c * math.exp(a * t)) - (g / a) + + # find c that matches g(t4) = (1-gamma) + s_c2 = optimize.fsolve(lambda c: abs((1 - gamma) - a5_ht(c, t4)), x0=np.array([1]))[0] + + # solve for time point where phase A5 ends h(t5)=(1-phi) + t5 = optimize.fsolve(lambda t: abs((1 - phi) - a5_ht(s_c2, t)), x0=np.array([t4]))[0] + + # return t5 and g(t5) + return t5, a5_gt(s_c1, t5) + + @staticmethod + def phase_a6(t5: float, gt5: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, theta: float, + gamma: float, phi: float) -> (float, float): + + def a6_gt(c, t): + # generalised g(t) for phase A6 + return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + + # find c that fits to known g(t5) = given gt5 + s_cg = optimize.fsolve(lambda c: abs(gt5 - a6_gt(c, t5)), x0=np.array([1]))[0] + + k = m_ans / ((1 - theta - gamma) * a_ans) + a = (p - m_ae) / a_anf + b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) + + def a6_ht(cx, t): + # generalised h(t) for phase A6 + return (a * t) - ((b * math.exp(-k * t)) / k) + cx + + # find ch that matches h(t5) = (1-phi) + s_ch = optimize.fsolve(lambda c: abs((1 - phi) - a6_ht(c, t5)), x0=np.array([1]))[0] + + # find end of phase A6. The time point where h(t6)=1 + # condition t > t5 added + t6 = optimize.fsolve(lambda t: abs(1 - a6_ht(s_ch, t)) + int(t < t5), x0=np.array([t5]))[0] + + # return time to exhaustion (t6) and g(t6) + return t6, a6_gt(s_cg, t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index d46e53f..cc78fb8 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -1,257 +1,121 @@ import logging -import math +import warnings + +warnings.filterwarnings("error") -import numpy as np -from scipy import optimize from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -def phase_a1(): - # end of phase A1 -> the time when h(t)=theta - t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * the) / (p * (1 - phi)))) - return t1 - - -def phase_a2(t1): - # taken from Equation 11 by Morton 1986 - a = (m_ae * a_ans * (1 - the - gam) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( - a_anf * a_ans * (1 - phi) * (1 - the - gam)) - - b = (m_ae * m_ans) / (a_anf * a_ans * (1 - phi) * (1 - the - gam)) - - c = (m_ans * (p * (1 - phi) - m_ae * the)) / (a_anf * a_ans * (1 - phi) * (1 - the - gam)) - - # use auxillary equation to derive both negative roots r1 and r2 - coeff = [1, a, b] - r1, r2 = np.roots(coeff) - - def a2_lt(c1, c2, t): - # the general solution for l(t) - return c1 * math.exp(r1 * t) + c2 * math.exp(r2 * t) + c / b - - def a2_dlt(c1, c2, t): - # derivative of general solution - return r1 * c1 * math.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) - - # l(t1) and dl(t1) equal to 0. Find c1 and c2 that satisfy that - res = optimize.fsolve(lambda cx: (a2_lt(cx[0], cx[1], t1), a2_dlt(cx[0], cx[1], t1)), x0=np.array([1, 1])) - s_c1 = float(res[0]) - s_c2 = float(res[1]) - - # substitute into EQ(9) for h - def a2_ht(t): - return (a_ans * (1 - the - gam)) / m_ans * a2_dlt(s_c1, s_c2, t) + a2_lt(s_c1, s_c2, t) + the - - if phi < gam: - # the whole of AnS is utilized in phase A2 - t2 = optimize.fsolve(lambda t: abs((1 - gam) - a2_ht(t)), x0=np.array([1]))[0] - else: - # phase A2 transitions into phase A3 before AnS is empty - t2 = optimize.fsolve(lambda t: abs((1 - phi) - a2_ht(t)), x0=np.array([1]))[0] - - # use t2 to get l(t2) - return t2, a2_lt(s_c1, s_c2, t2) - - -def phase_a3(t2, lt2): - # phase A3 is not applicable if gamma is greater or equal to phi - if gam >= phi: - return t2, lt2 - - a = ((a_anf + a_ans) * m_ans) / (a_anf * a_ans * (1 - the - gam)) - b = ((p - m_ae) * m_ans) / (a_anf * a_ans * (1 - the - gam)) - - def a3_lt(c1, c2, t): - # general solution for l(t) - return (((b * t) + (c1 * math.exp(-a * t))) / a) + c2 - - def a3_dlt(c1, c2, t): - # first derivative l'(t) - return (b / a) - c1 * math.exp(-a * t) - - # derivative l'(t2) can be calculated manually - dlt2 = (m_ans * ((1 - phi) - lt2 - the)) / (a_ans * (1 - the - gam)) - - def general_l(p): - c1, c2 = p - # l(t2) = previously estimated lt2 - est_lt2 = a3_lt(c1, c2, t2) - # l'(t2) = manually estimated derivative - est_dlt2 = a3_dlt(c1, c2, t2 + 1) - # add a small tolerance for derivative estimation - com_dl = int(abs(est_dlt2 - dlt2) > 0.00001) - return abs(est_lt2 - lt2), com_dl - - # solve for c1 and c2 - res = optimize.fsolve(general_l, x0=np.array([0, 0])) - s_c1 = float(res[0]) - s_c2 = float(res[1]) - - def a3_ht(t): - # EQ(9) with constants for l(t) and l'(t) - return ((a_ans * (1 - the - gam)) / m_ans) * a3_dlt(s_c1, s_c2, t) + a3_lt(s_c1, s_c2, t) + the - - # find end of A3 where h(t3) = (1-gamma) - t3 = optimize.fsolve(lambda t: abs((1 - gam) - a3_ht(t)), x0=np.array([t2]))[0] - - # return t3 and l(t3) - return t3, a3_lt(s_c1, s_c2, t3) - - -def phase_a4(t2, lt2): - # phase A4 is not applicable if phi is greater or equal to gamma - if phi >= gam: - return t2, lt2 - - # FIRST: Determine dynamics of l(t) - def a4_lt(c, t): - return (1 - the - gam) + c * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) - - # find c that fits to known l(t2) = l - s_c3 = optimize.fsolve(lambda c: abs(lt2 - a4_lt(c, t2)), x0=np.array([1]))[0] - - # # double-check if C3 estimation lines up - # t_l0 = optimize.fsolve(lambda t: abs((1 - the - gam - 0.1) - a4_lt(s_c3, t)), x0=np.array([t2]))[0] - # # simulate with the agent for estimated amount of time steps - # agent.reset() - # for _ in range(int(t_l0 * hz)): - # agent.set_power(p) - # agent.perform_one_step() - # assert abs(agent.get_g() - (1 - the - gam - 0.1)) < 0.001 - - # SECOND: Determine dynamics of h(t) - - # as defined for EQ(21) - k = m_ans / ((1 - the - gam) * a_ans) - a = -m_ae / ((1 - phi) * a_anf) - b = (m_ans * s_c3) / ((1 - the - gam) * a_anf) - g = p / a_anf - - def a4_ht(c, t): - return ((-b * math.exp(-k * t)) / (a + k)) + (c * math.exp(a * t)) - (g / a) - - # find c4 that matches h(t2) = (1-gamma) - s_c4 = optimize.fsolve(lambda c: abs((1 - gam) - a4_ht(c, t2)), x0=np.array([1]))[0] - - # double-check with first derivative and time step t2+1 - # h_act_t2 = (1 - gam) - # dh_act = ( - # ((-m_ae * (1 - gam)) / ((1 - phi) * a_anf)) - - # ((m_ans * (1 - the - gam - lt2)) / ((1 - the - gam) * a_anf)) + - # (p / a_anf) - # ) - # h_act_t2p = h_act_t2 + dh_act - # assert abs(a4_ht(s_c4, t2 + 1) - h_act_t2p) < 0.0001 - - # find end of phase A4. The time point where h(t4)=(1-phi) - t4 = optimize.fsolve(lambda t: abs((1 - phi) - a4_ht(s_c4, t)), x0=np.array([t2]))[0] - - # finally return t4 result and l - return t4, a4_lt(s_c3, t4) - - -def phase_a5(t4, lt4): - # FIRST: Determine dynamics of l(t) - def a5_lt(c, t): - return (1 - the - gam) + c * math.exp((-m_ans * t) / ((1 - the - gam) * a_ans)) - - # find c that fits to known l(t4) = given l - s_cl = optimize.fsolve(lambda c: abs(lt4 - a5_lt(c, t4)), x0=np.array([1]))[0] - - # SECOND: determine dynamics of h(t) - k = m_ans / ((1 - the - gam) * a_ans) - a = (p - m_ae) / a_anf - b = (m_ans * s_cl) / ((1 - the - gam) * a_anf) - - def a5_ht(cx, t): - return (a * t) - ((b * math.exp(-k * t)) / k) + cx - - # find c5 that matches h(t4) = (1-phi) - s_ch = optimize.fsolve(lambda c: abs((1 - phi) - a5_ht(c, t4)), x0=np.array([1]))[0] - - # find end of phase A5. The time point where h(t5)=1 - t5 = optimize.fsolve(lambda t: abs(1 - a5_ht(s_ch, t)) + int(t < t4), x0=np.array([t4]))[0] - - # return time to exhaustion and l at exhaustion - return t5, a5_lt(s_cl, t5) - - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - hz = 100 - - example_conf = [11532.526538727172, - 424324.257042239595, - 249.7641585019016, - 286.26673813946095, - 7.988078323028352, - 0.45486842730772163, - 0.2641766056862277, - 0.641766056862277] - - a_anf = example_conf[0] - a_ans = example_conf[1] - m_ae = example_conf[2] - m_ans = example_conf[3] - m_anf = example_conf[4] - the = example_conf[5] - gam = example_conf[6] - phi = example_conf[7] +def test_procedure(hz, eps, conf): + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + the = conf[5] + gam = conf[6] + phi = conf[7] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, m_anf=m_anf, the=the, gam=gam, phi=phi) - ThreeCompVisualisation(agent) + # ThreeCompVisualisation(agent) - p = 400 + p = 300 # all TTE phases - t1 = phase_a1() - t2, lt2 = phase_a2(t1) - t3, lt3 = phase_a3(t2, lt2) - t4, lt4 = phase_a4(t3, lt3) - t5, lt5 = phase_a5(t4, lt4) + try: + t1 = ODEThreeCompHydSimulator.phase_a1(p=p, a_anf=a_anf, m_ae=m_ae, theta=the, phi=phi) + t2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, p=p, a_anf=a_anf, m_ae=m_ae, theta=the, phi=phi) + except ValueError: + logging.info("A1/2 error with {}".format(example_conf)) + return + + try: + t3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + except RuntimeWarning: + logging.info("A3 warning with {}".format(example_conf)) + return + + try: + t4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, gt3=gt3, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + except RuntimeWarning: + logging.info("A4 warning with {}".format(example_conf)) + return + + try: + t5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, gt4=gt4, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + except RuntimeWarning: + logging.info("A5 warning with {}".format(example_conf)) + return + + try: + t6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, gt5=gt5, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + except RuntimeWarning: + logging.info("A6 warning with {}".format(example_conf)) + return - # confirm with assert tests - eps = 0.001 # A1 agent.reset() for _ in range(int(t1 * hz)): agent.set_power(p) agent.perform_one_step() - assert abs(agent.get_h() - the) < eps - # A2 - agent.reset() - for _ in range(int(t2 * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - (1 - max(phi, gam))) < eps + assert abs(agent.get_h() - min(the, 1 - phi)) < eps # A3 agent.reset() for _ in range(int(t3 * hz)): agent.set_power(p) agent.perform_one_step() - assert abs(agent.get_h() - (1 - gam)) < eps + if phi >= (1 - the): + assert abs(agent.get_h() - the) < eps + else: + assert abs(agent.get_h() - (1 - max(phi, gam))) < eps # A4 agent.reset() for _ in range(int(t4 * hz)): agent.set_power(p) agent.perform_one_step() + assert abs(agent.get_h() - (1 - gam)) < eps + # A5 + agent.reset() + for _ in range(int(t5 * hz)): + agent.set_power(p) + agent.perform_one_step() if phi >= gam: assert abs(agent.get_h() - (1 - gam)) < eps else: - # A4 is only different from t3 if phi >= gamma + # A5 is only different from t4 if phi >= gamma assert abs(agent.get_h() - (1 - phi)) < eps - # A5 + # A6 agent.reset() - for _ in range(int(t5 * hz)): + for _ in range(int(t6 * hz)): agent.set_power(p) agent.perform_one_step() assert abs(agent.get_h() - 1) < eps - print(t5) \ No newline at end of file + print(t6) + + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + # estimations per second for discrete agent + hz = 100 + # required precision of discrete to differential agent + eps = 0.001 + + udp = MultiObjectiveThreeCompUDP(None, None) + + example_conf = udp.create_educated_initial_guess() + + test_procedure(hz, eps, example_conf) From aecf8f3db2b1cf14d482bbd11c91051ce2194f61 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 30 Jun 2021 20:57:13 +1000 Subject: [PATCH 09/71] improve test procedure to highlight were fittings go wrong. Add handling of equilibria --- .../simulator/ode_three_comp_hyd_simulator.py | 123 +++++++++++------- tests/tte_phase_tests.py | 84 ++++++------ 2 files changed, 119 insertions(+), 88 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 6e91553..e06b213 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -1,3 +1,4 @@ +import logging import math import numpy as np @@ -9,27 +10,41 @@ class ODEThreeCompHydSimulator: Simulates Three Component Hydraulic Model responses using Ordinary Differential Equations """ + tolerance = 8 + max_steps = 5000 + @staticmethod - def phase_a1(p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> float: + def phase_a1(p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> (float, float, float): + + # check if equilibrium in phase A1 + if ((m_ae * min(theta, 1 - phi)) / (p * (1 - phi))) >= 1: + h = (a_anf * (1 - phi)) / m_ae * (1 - np.exp(-(m_ae * np.inf) / (a_anf * (1 - phi)))) * p / a_anf + return np.inf, h, 0 + # end of phase A1 -> the time when h(t) = min(theta,1-phi) t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * min(theta, 1 - phi)) / (p * (1 - phi)))) - return t1 + + # return t1, h(t1), g(t1) + return t1, min(theta, 1 - phi), 0 @staticmethod - def phase_a2(t1: float, p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> float: + def phase_a2(t1: float, ht1: float, gt1: float, p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> ( + float, float, float): # A2 is only applicable if phi is higher than the top of AnS if phi <= (1 - theta): - return t1 - # linear utilization - return t1 + ((phi - (1 - theta)) * a_anf) / (p - m_ae) + return t1, ht1, gt1 + # linear utilization -> no equilibrium possible + t2 = t1 + ((phi - (1 - theta)) * a_anf) / (p - m_ae) + # return t2, h(t2), g(t2) + return t2, theta, gt1 @staticmethod - def phase_a3(t2: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, theta: float, - gamma: float, phi: float) -> (float, float): + def phase_a3(t2: float, ht2: float, gt2: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, + theta: float, gamma: float, phi: float) -> (float, float, float): # A3 is only applicable if flow from Ae is not at max if phi > (1 - theta): - return t2, 0 + return t2, ht2, gt2 # taken from Equation 11 by Morton 1986 a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( @@ -45,37 +60,42 @@ def phase_a3(t2: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans def a3_gt(c1, c2, t): # the general solution for g(t) - return c1 * math.exp(r1 * t) + c2 * math.exp(r2 * t) + c / b + return c1 * np.exp(r1 * t) + c2 * math.exp(r2 * t) + c / b def a3_dgt(c1, c2, t): # derivative of general solution - return r1 * c1 * math.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) + return r1 * c1 * np.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) # Find c1 and c2 that satisfy g(t2) and dg(t2) = 0 - res = optimize.fsolve(lambda cx: (a3_gt(cx[0], cx[1], t2), a3_dgt(cx[0], cx[1], t2)), x0=np.array([1, 1])) + res = optimize.fsolve(lambda cx: [ + a3_gt(cx[0], cx[1], t2), + a3_dgt(cx[0], cx[1], t2) + ], x0=np.array([1, 1])) + s_c1 = float(res[0]) s_c2 = float(res[1]) # substitute into EQ(9) for h - def a2_ht(t): + def a3_ht(t): return (a_ans * (1 - theta - gamma)) / m_ans * a3_dgt(s_c1, s_c2, t) + a3_gt(s_c1, s_c2, t) + theta - if phi < gamma: - # the whole of AnS is utilized in phase A3 - t3 = optimize.fsolve(lambda t: abs((1 - gamma) - a2_ht(t)), x0=np.array([1]))[0] - else: - # phase A3 transitions into phase A4 before AnS is empty - t3 = optimize.fsolve(lambda t: abs((1 - phi) - a2_ht(t)), x0=np.array([1]))[0] + # if phi > gamma, then phase A3 transitions into phase A4 before AnS is empty + ht3 = 1 - max(phi, gamma) - # use t3 to get g(t3) - return t3, a3_gt(s_c1, s_c2, t3) + # check if equilibrium in this phase + if ht2 <= a3_ht(np.inf) <= ht3: + return np.inf, a3_ht(np.inf), a3_gt(s_c1, s_c2, np.inf) + else: + t3 = optimize.fsolve(lambda t: abs(ht3 - a3_ht(t)), x0=np.array([t2]))[0] + # use t3 to get g(t3) + return t3, ht3, a3_gt(s_c1, s_c2, t3) @staticmethod - def phase_a4(t3: float, gt3: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, + def phase_a4(t3: float, ht3: float, gt3: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, theta: float, gamma: float, phi: float) -> (float, float): # phase A3 is not applicable if gamma is greater or equal to phi if gamma >= phi: - return t3, gt3 + return t3, ht3, gt3 a = ((a_anf + a_ans) * m_ans) / (a_anf * a_ans * (1 - theta - gamma)) b = ((p - m_ae) * m_ans) / (a_anf * a_ans * (1 - theta - gamma)) @@ -84,12 +104,11 @@ def a4_gt(c1, c2, t): # general solution for g(t) return (((b * t) + (c1 * math.exp(-a * t))) / a) + c2 - def a4_dgt(c1, c2, t): + def a4_dgt(c1, _, t): # first derivative g'(t) return (b / a) - c1 * math.exp(-a * t) # derivative g'(t3) can be calculated manually - ht3 = max(theta, 1 - phi) dgt3 = (m_ans * (ht3 - gt3 - theta)) / (a_ans * (1 - theta - gamma)) def general_gt(p): @@ -109,47 +128,54 @@ def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) return ((a_ans * (1 - theta - gamma)) / m_ans) * a4_dgt(s_c1, s_c2, t) + a4_gt(s_c1, s_c2, t) + theta - # find end of A4 where g(t4) = (1-gamma) - t4 = optimize.fsolve(lambda t: abs((1 - gamma) - a4_ht(t)), x0=np.array([t3]))[0] - - # return t4 and g(t4) - return t4, a4_gt(s_c1, s_c2, t4) + ht4 = (1 - gamma) + # check if equilibrium in this phase + if ht3 <= a4_ht(np.inf) <= ht4: + return np.inf, a4_ht(np.inf), a4_gt(s_c1, s_c2, np.inf) + else: + # find end of A4 where g(t4) = (1-gamma) + t4 = optimize.fsolve(lambda t: abs(ht4 - a4_ht(t)), x0=np.array([t3]))[0] + return t4, ht4, a4_gt(s_c1, s_c2, t4) @staticmethod - def phase_a5(t4: float, gt4: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, + def phase_a5(t4: float, ht4: float, gt4: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, theta: float, gamma: float, phi: float) -> (float, float): # phase A5 is not applicable if phi is greater or equal to gamma if phi >= gamma: - return t4, gt4 + return t4, ht4, gt4 def a5_gt(c, t): # generalised g(t) for phase A5 return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) # find c that fits to known g(t4) = gt4 - s_c1 = optimize.fsolve(lambda c: abs(gt4 - a5_gt(c, t4)), x0=np.array([1]))[0] + s_cg = optimize.fsolve(lambda c: abs(gt4 - a5_gt(c, t4)), x0=np.array([1]))[0] # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) a = -m_ae / ((1 - phi) * a_anf) - b = (m_ans * s_c1) / ((1 - theta - gamma) * a_anf) + b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) g = p / a_anf def a5_ht(c, t): return ((-b * math.exp(-k * t)) / (a + k)) + (c * math.exp(a * t)) - (g / a) - # find c that matches g(t4) = (1-gamma) - s_c2 = optimize.fsolve(lambda c: abs((1 - gamma) - a5_ht(c, t4)), x0=np.array([1]))[0] + # find c that matches h(t4) = ht4 + s_ch = optimize.fsolve(lambda c: abs(ht4 - a5_ht(c, t4)), x0=np.array([1]))[0] - # solve for time point where phase A5 ends h(t5)=(1-phi) - t5 = optimize.fsolve(lambda t: abs((1 - phi) - a5_ht(s_c2, t)), x0=np.array([t4]))[0] - - # return t5 and g(t5) - return t5, a5_gt(s_c1, t5) + ht5 = (1 - phi) + # check if equilibrium in this phase + if ht4 <= a5_ht(np.inf, s_ch) <= ht5: + return np.inf, a5_ht(np.inf, s_ch), a5_gt(s_cg, np.inf) + else: + # solve for time point where phase A5 ends h(t5)=(1-phi) + t5 = optimize.fsolve(lambda t: abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))[0] + return t5, ht5, a5_gt(s_cg, t5) @staticmethod - def phase_a6(t5: float, gt5: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, theta: float, + def phase_a6(t5: float, ht5: float, gt5: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, + theta: float, gamma: float, phi: float) -> (float, float): def a6_gt(c, t): @@ -163,16 +189,15 @@ def a6_gt(c, t): a = (p - m_ae) / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) - def a6_ht(cx, t): + def a6_ht(cx, t) -> float: # generalised h(t) for phase A6 return (a * t) - ((b * math.exp(-k * t)) / k) + cx - # find ch that matches h(t5) = (1-phi) - s_ch = optimize.fsolve(lambda c: abs((1 - phi) - a6_ht(c, t5)), x0=np.array([1]))[0] + # find ch that matches h(t5) = ht5 + s_ch = optimize.fsolve(lambda c: np.abs(ht5 - a6_ht(c, t5)), x0=np.array([1]))[0] + ht6 = 1 # find end of phase A6. The time point where h(t6)=1 # condition t > t5 added - t6 = optimize.fsolve(lambda t: abs(1 - a6_ht(s_ch, t)) + int(t < t5), x0=np.array([t5]))[0] - - # return time to exhaustion (t6) and g(t6) - return t6, a6_gt(s_cg, t6) + t6 = optimize.fsolve(lambda t: np.abs(ht6 - a6_ht(s_ch, t)) + int(t < t5), x0=np.array([t5]))[0] + return t6, ht6, a6_gt(s_cg, t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index cc78fb8..ace9c90 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -1,15 +1,17 @@ -import logging -import warnings - -warnings.filterwarnings("error") - from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +import logging +import warnings + +import numpy as np + +warnings.filterwarnings("error") + -def test_procedure(hz, eps, conf): +def test_procedure(hz, eps, conf, agent): a_anf = conf[0] a_ans = conf[1] m_ae = conf[2] @@ -19,49 +21,46 @@ def test_procedure(hz, eps, conf): gam = conf[6] phi = conf[7] - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, m_ans=m_ans, - m_anf=m_anf, the=the, gam=gam, phi=phi) - - # ThreeCompVisualisation(agent) - - p = 300 + p = 350 # all TTE phases - try: - t1 = ODEThreeCompHydSimulator.phase_a1(p=p, a_anf=a_anf, m_ae=m_ae, theta=the, phi=phi) - t2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, p=p, a_anf=a_anf, m_ae=m_ae, theta=the, phi=phi) - except ValueError: - logging.info("A1/2 error with {}".format(example_conf)) + t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p, a_anf=a_anf, m_ae=m_ae, theta=the, phi=phi) + if t1 == np.inf: + logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return + logging.info("A1: {}".format(t1)) - try: - t3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) - except RuntimeWarning: - logging.info("A3 warning with {}".format(example_conf)) + t2, ht2, gt2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, ht1=ht1, gt1=gt1, p=p, a_anf=a_anf, m_ae=m_ae, theta=the, + phi=phi) + logging.info("A2: {}".format(t2)) + + t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, a_anf=a_anf, a_ans=a_ans, + m_ae=m_ae, m_ans=m_ans, theta=the, gamma=gam, phi=phi) + if t3 == np.inf: + logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return + logging.info("A3: {}".format(t3)) - try: - t4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, gt3=gt3, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) - except RuntimeWarning: - logging.info("A4 warning with {}".format(example_conf)) + t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + if t4 == np.inf: + logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return + logging.info("A4: {}".format(t4)) - try: - t5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, gt4=gt4, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) - except RuntimeWarning: - logging.info("A5 warning with {}".format(example_conf)) + t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + if t5 == np.inf: + logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return + logging.info("A5: {}".format(t5)) - try: - t6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, gt5=gt5, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) - except RuntimeWarning: - logging.info("A6 warning with {}".format(example_conf)) + t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, + m_ans=m_ans, theta=the, gamma=gam, phi=phi) + if t6 == np.inf: + logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return + logging.info("A6: {}".format(t6)) # A1 agent.reset() @@ -118,4 +117,11 @@ def test_procedure(hz, eps, conf): example_conf = udp.create_educated_initial_guess() - test_procedure(hz, eps, example_conf) + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) + + ThreeCompVisualisation(agent) + + test_procedure(hz, eps, example_conf, agent) From eb7e21c70c7efdba9d101c2d0c75a21737a13b4e Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 1 Jul 2021 16:42:50 +1000 Subject: [PATCH 10/71] minor discrete dynamics adjustments, fix bounds application in educated guess, apply further experiments with ODE simulator --- .../agents/three_comp_hyd_agent.py | 12 +- .../evolutionary_fitter/three_comp_tools.py | 10 +- .../simulator/ode_three_comp_hyd_simulator.py | 168 +++++++++++++----- tests/tte_phase_tests.py | 95 ++++------ 4 files changed, 165 insertions(+), 120 deletions(-) diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 8496cf3..45ab985 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -111,12 +111,12 @@ def raise_detailed_error_report(): # step 2 a: determine oxygen energy flow (p_{Ae}) # level AnF above pipe exit. Scale contribution according to h level - if 0 <= self.__h <= (1 - self.__phi): - # contribution through R1 scales with maximal flow capacity + if 0 <= self.__h < (1 - self.__phi): + # contribution from Ae scales with maximal flow capacity self.__p_ae = self.__m_ae * self.__h / (1 - self.__phi) # at maximum rate because level h of AnF is below pipe exit of Ae elif (1 - self.__phi) <= self.__h: - # max contribution R1 = m_o + # max contribution R1 = m_ae self.__p_ae = self.__m_ae else: raise_detailed_error_report() @@ -128,7 +128,7 @@ def raise_detailed_error_report(): # [no change] AnS empty and level AnF below pipe exit elif self.__h >= (1 - self.__gamma) and self.__g == self.__height_ans: self.__p_an = 0.0 - # [no change] h at par with g + # [no change] h at equal with g elif self.__h == (self.__g + self.__theta): self.__p_an = 0.0 else: @@ -142,7 +142,7 @@ def raise_detailed_error_report(): elif (self.__g + self.__theta) < self.__h < (1 - self.__gamma): # h is more than g+theta thus the difference is positive self.__p_an = self.__m_ans * ((self.__h - (self.__g + self.__theta)) / self.__height_ans) - # [utilise max] if level AnF below AnS pipe exit and AnS not empty + # [utilise max] if level AnF below or at AnS pipe exit and AnS not empty elif (1 - self.__gamma) <= self.__h and self.__g < self.__height_ans: # the only thing that affects flow is the amount of remaining liquid (pressure) self.__p_an = self.__m_ans * ((self.__height_ans - self.__g) / self.__height_ans) @@ -164,8 +164,10 @@ def raise_detailed_error_report(): self.__p_an = min(self.__p_an, (self.__height_ans - self.__g) * self.__a_ans) # level AnS is adapted to estimated change + # g increases as p_An flows out self.__g += self.__p_an / self.__a_ans / self._hz # refill or deplete AnF according to AnS flow and Power demand + # h decreases as p_Ae and p_An flow in self.__h -= (self.__p_ae + self.__p_an) / self.__a_anf / self._hz # step 3: account for rounding errors and set exhaustion flag diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index 46f0ac7..bc55302 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -43,7 +43,7 @@ def __init__(self, self.__bounds = ([x[0] for x in self.__limits.values()], [x[1] for x in self.__limits.values()]) - def create_educated_initial_guess(self, cp: float = 250.0, w_p: float = 200000.0): + def create_educated_initial_guess(self, cp: float = 250.0, w_p: float = 50000.0): """ creates a suitable initial guess configuration :param cp: critical power that usually corresponds to Ae flow @@ -62,11 +62,9 @@ def create_educated_initial_guess(self, cp: float = 250.0, w_p: float = 200000.0 np.random.normal(1, 0.4) * 0.5, # for a curvelinear expenditure dynamic the pipe has to be halfway or lower ] # ensure values are within limits - for i, i_x_e in enumerate(i_x): - # lower bound - i_x[i] = max(self.__bounds[0][i], i_x_e) - # upper bound - i_x[i] = min(self.__bounds[1][i], i_x_e) + for i in range(len(i_x)): + i_x[i] = max(self.__bounds[0][i], i_x[i]) # lower bound + i_x[i] = min(self.__bounds[1][i], i_x[i]) # upper bound return i_x def fitness(self, x): diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index e06b213..483f663 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -1,4 +1,3 @@ -import logging import math import numpy as np @@ -9,12 +8,15 @@ class ODEThreeCompHydSimulator: """ Simulates Three Component Hydraulic Model responses using Ordinary Differential Equations """ - - tolerance = 8 - max_steps = 5000 + tolerance = 1.49012e-8 @staticmethod - def phase_a1(p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> (float, float, float): + def phase_a1(p: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + m_ae = conf[2] + theta = conf[5] + phi = conf[7] # check if equilibrium in phase A1 if ((m_ae * min(theta, 1 - phi)) / (p * (1 - phi))) >= 1: @@ -28,8 +30,13 @@ def phase_a1(p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> ( return t1, min(theta, 1 - phi), 0 @staticmethod - def phase_a2(t1: float, ht1: float, gt1: float, p: float, a_anf: float, m_ae: float, theta: float, phi: float) -> ( - float, float, float): + def phase_a2(t1: float, ht1: float, gt1: float, p: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + m_ae = conf[2] + theta = conf[5] + phi = conf[7] + # A2 is only applicable if phi is higher than the top of AnS if phi <= (1 - theta): return t1, ht1, gt1 @@ -39,8 +46,15 @@ def phase_a2(t1: float, ht1: float, gt1: float, p: float, a_anf: float, m_ae: fl return t2, theta, gt1 @staticmethod - def phase_a3(t2: float, ht2: float, gt2: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, - theta: float, gamma: float, phi: float) -> (float, float, float): + def phase_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] # A3 is only applicable if flow from Ae is not at max if phi > (1 - theta): @@ -60,24 +74,36 @@ def phase_a3(t2: float, ht2: float, gt2: float, p: float, a_anf: float, a_ans: f def a3_gt(c1, c2, t): # the general solution for g(t) - return c1 * np.exp(r1 * t) + c2 * math.exp(r2 * t) + c / b + return c1 * np.exp(r1 * t) + c2 * np.exp(r2 * t) + c / b def a3_dgt(c1, c2, t): # derivative of general solution return r1 * c1 * np.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) + def a3_gtdgt(c1): + c2 = -(r1 * c1 * np.exp(r1 * t2)) / (r2 * np.exp(r2 * t2)) + return np.abs(c1 * np.exp(r1 * t2) + c2 * np.exp(r2 * t2) + c / b) + # Find c1 and c2 that satisfy g(t2) and dg(t2) = 0 - res = optimize.fsolve(lambda cx: [ - a3_gt(cx[0], cx[1], t2), - a3_dgt(cx[0], cx[1], t2) - ], x0=np.array([1, 1])) + # res = optimize.fsolve(lambda cx: [ + # a3_gt(cx[0], cx[1], t2), + # a3_dgt(cx[0], cx[1], t2) + # ], x0=np.array([0, 0]), xtol=ODEThreeCompHydSimulator.tolerance) + # s_c1 = float(res[0]) + # s_c2 = float(res[1]) - s_c1 = float(res[0]) - s_c2 = float(res[1]) + s_c1 = optimize.fsolve(a3_gtdgt, x0=np.array([0]))[0] + s_c2 = -(r1 * s_c1 * np.exp(r1 * t2)) / (r2 * np.exp(r2 * t2)) + + # check with g(t) and g'(t) + # assert np.abs(a3_gt(s_c1, s_c2, t2)) < 0.0001 + # assert np.abs(a3_dgt(s_c1, s_c2, t2)) < 0.0001 # substitute into EQ(9) for h def a3_ht(t): - return (a_ans * (1 - theta - gamma)) / m_ans * a3_dgt(s_c1, s_c2, t) + a3_gt(s_c1, s_c2, t) + theta + k1 = ((a_ans * (1 - theta - gamma) / m_ans) * s_c1 * r1 + s_c1) + k2 = ((a_ans * (1 - theta - gamma) / m_ans) * s_c2 * r2 + s_c2) + return k1*np.exp(r1*t) + k2*np.exp(r2*t) + c/b + theta # if phi > gamma, then phase A3 transitions into phase A4 before AnS is empty ht3 = 1 - max(phi, gamma) @@ -86,13 +112,21 @@ def a3_ht(t): if ht2 <= a3_ht(np.inf) <= ht3: return np.inf, a3_ht(np.inf), a3_gt(s_c1, s_c2, np.inf) else: - t3 = optimize.fsolve(lambda t: abs(ht3 - a3_ht(t)), x0=np.array([t2]))[0] + t3 = optimize.fsolve(lambda t: np.abs(ht3 - a3_ht(t)), x0=np.array([t2]))[0] # use t3 to get g(t3) return t3, ht3, a3_gt(s_c1, s_c2, t3) @staticmethod - def phase_a4(t3: float, ht3: float, gt3: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, - theta: float, gamma: float, phi: float) -> (float, float): + def phase_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + # phase A3 is not applicable if gamma is greater or equal to phi if gamma >= phi: return t3, ht3, gt3 @@ -111,18 +145,30 @@ def a4_dgt(c1, _, t): # derivative g'(t3) can be calculated manually dgt3 = (m_ans * (ht3 - gt3 - theta)) / (a_ans * (1 - theta - gamma)) - def general_gt(p): - c1, c2 = p - # g(t3) = previously estimated gt3 - est_gt3 = a4_gt(c1, c2, t3) - # g'(t3) = manually estimated derivative - est_dgt3 = a4_dgt(c1, c2, t3) - return abs(est_gt3 - gt3), abs(est_dgt3 - dgt3) - - # solve for c1 and c2 - res = optimize.fsolve(general_gt, x0=np.array([0, 0])) - s_c1 = float(res[0]) - s_c2 = float(res[1]) + def a4_gtdgt(c2): + c1 = (b / a / np.exp(-a * t3)) - dgt3 + return np.abs(((((b * t3) + (c1 * math.exp(-a * t3))) / a) + c2) - gt3) + + # def general_gt(p): + # c1, c2 = p + # # g(t3) = previously estimated gt3 + # est_gt3 = a4_gt(c1, c2, t3) + # # g'(t3) = manually estimated derivative + # est_dgt3 = a4_dgt(c1, c2, t3) + # return abs(est_gt3 - gt3), abs(est_dgt3 - dgt3) + # + # # solve for c1 and c2 + # res = optimize.fsolve(general_gt, x0=np.array([0, 0]), xtol=ODEThreeCompHydSimulator.tolerance) + # s_c1 = float(res[0]) + # s_c2 = float(res[1]) + + s_c2 = optimize.fsolve(a4_gtdgt, x0=np.array([0]))[0] + s_c1 = (b / a / np.exp(-a * t3)) - dgt3 + + # check with g(t) and g'(t) + # assert np.abs(a4_gt(s_c1, s_c2, t3) - gt3) < 0.0001 + # print(a4_dgt(s_c1, s_c2, t3), dgt3) + # assert np.abs(a4_dgt(s_c1, s_c2, t3) - dgt3) < 0.0001 def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) @@ -134,12 +180,20 @@ def a4_ht(t): return np.inf, a4_ht(np.inf), a4_gt(s_c1, s_c2, np.inf) else: # find end of A4 where g(t4) = (1-gamma) - t4 = optimize.fsolve(lambda t: abs(ht4 - a4_ht(t)), x0=np.array([t3]))[0] + # t4 = optimize.minimize(lambda t: np.abs(ht4 - a4_ht(t)), x0=np.array([t3]))['x'] + t4 = optimize.fsolve(lambda t: np.abs(ht4 - a4_ht(t)), x0=np.array([t3]))[0] return t4, ht4, a4_gt(s_c1, s_c2, t4) @staticmethod - def phase_a5(t4: float, ht4: float, gt4: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, - theta: float, gamma: float, phi: float) -> (float, float): + def phase_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] # phase A5 is not applicable if phi is greater or equal to gamma if phi >= gamma: @@ -150,7 +204,10 @@ def a5_gt(c, t): return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) # find c that fits to known g(t4) = gt4 - s_cg = optimize.fsolve(lambda c: abs(gt4 - a5_gt(c, t4)), x0=np.array([1]))[0] + s_cg = optimize.fsolve(lambda c: np.abs(gt4 - a5_gt(c, t4)), x0=np.array([0]))[0] + # s_cg = optimize.minimize(lambda c: np.abs(gt4 - a5_gt(c, t4)), x0=np.array([0]))['x'] + + # assert np.abs(a5_gt(s_cg, t4) - gt4) < 0.0001 # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) @@ -162,7 +219,8 @@ def a5_ht(c, t): return ((-b * math.exp(-k * t)) / (a + k)) + (c * math.exp(a * t)) - (g / a) # find c that matches h(t4) = ht4 - s_ch = optimize.fsolve(lambda c: abs(ht4 - a5_ht(c, t4)), x0=np.array([1]))[0] + s_ch = optimize.fsolve(lambda c: np.abs(ht4 - a5_ht(c, t4)), x0=np.array([0]))[0] + # s_ch = optimize.minimize(lambda c: np.abs(ht4 - a5_ht(c, t4)), x0=np.array([0]))['x'] ht5 = (1 - phi) # check if equilibrium in this phase @@ -170,34 +228,46 @@ def a5_ht(c, t): return np.inf, a5_ht(np.inf, s_ch), a5_gt(s_cg, np.inf) else: # solve for time point where phase A5 ends h(t5)=(1-phi) - t5 = optimize.fsolve(lambda t: abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))[0] + + t5 = optimize.fsolve(lambda t: np.abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))[0] + # t5 = optimize.minimize(lambda t: np.abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))['x'] return t5, ht5, a5_gt(s_cg, t5) @staticmethod - def phase_a6(t5: float, ht5: float, gt5: float, p: float, a_anf: float, a_ans: float, m_ae: float, m_ans: float, - theta: float, - gamma: float, phi: float) -> (float, float): + def phase_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] def a6_gt(c, t): # generalised g(t) for phase A6 return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) - # find c that fits to known g(t5) = given gt5 - s_cg = optimize.fsolve(lambda c: abs(gt5 - a6_gt(c, t5)), x0=np.array([1]))[0] + # s_cg = optimize.minimize(lambda c: np.abs(gt5 - a6_gt(c, t5)), x0=np.array([0]))['x'] + s_cg = optimize.fsolve(lambda c: np.abs(gt5 - a6_gt(c, t5)), x0=np.array([0]))[0] + + # assert np.abs(a6_gt(s_cg, t5) - gt5) < 0.0001 k = m_ans / ((1 - theta - gamma) * a_ans) - a = (p - m_ae) / a_anf + a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) + g = p / a_anf - def a6_ht(cx, t) -> float: + def a6_ht(cx, t): # generalised h(t) for phase A6 - return (a * t) - ((b * math.exp(-k * t)) / k) + cx + return (a * t) - ((b * math.exp(-k * t)) / k) + cx + t * g # find ch that matches h(t5) = ht5 - s_ch = optimize.fsolve(lambda c: np.abs(ht5 - a6_ht(c, t5)), x0=np.array([1]))[0] + s_ch = optimize.fsolve(lambda c: np.abs(ht5 - a6_ht(c, t5)), x0=np.array([0]))[0] + # s_ch = optimize.minimize(lambda c: np.abs(ht5 - a6_ht(c, t5)), x0=np.array([0]))['x'] - ht6 = 1 + ht6 = 1.0 # find end of phase A6. The time point where h(t6)=1 # condition t > t5 added - t6 = optimize.fsolve(lambda t: np.abs(ht6 - a6_ht(s_ch, t)) + int(t < t5), x0=np.array([t5]))[0] + t6 = optimize.fsolve(lambda t: np.abs(ht6 - a6_ht(s_ch, t)), x0=np.array([t5]))[0] + # t6 = optimize.minimize(lambda t: np.abs(ht6 - a6_ht(s_ch, t)), x0=np.array([t5]))['x'] return t6, ht6, a6_gt(s_cg, t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index ace9c90..501f526 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -8,99 +8,72 @@ import numpy as np -warnings.filterwarnings("error") +# warnings.filterwarnings("error") -def test_procedure(hz, eps, conf, agent): - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - the = conf[5] - gam = conf[6] - phi = conf[7] +def test_procedure(hz, eps, conf, agent): p = 350 # all TTE phases - t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p, a_anf=a_anf, m_ae=m_ae, theta=the, phi=phi) + + # A1 + t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p, conf=conf) if t1 == np.inf: logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return logging.info("A1: {}".format(t1)) - t2, ht2, gt2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, ht1=ht1, gt1=gt1, p=p, a_anf=a_anf, m_ae=m_ae, theta=the, - phi=phi) + t2, ht2, gt2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, ht1=ht1, gt1=gt1, p=p, conf=conf) logging.info("A2: {}".format(t2)) - t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, a_anf=a_anf, a_ans=a_ans, - m_ae=m_ae, m_ans=m_ans, theta=the, gamma=gam, phi=phi) + # A3 + t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, conf=conf) if t3 == np.inf: logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return logging.info("A3: {}".format(t3)) - t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) + # A4 + t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, conf=conf) if t4 == np.inf: logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return logging.info("A4: {}".format(t4)) - t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) + # A5 + t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, conf=conf) if t5 == np.inf: logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return logging.info("A5: {}".format(t5)) - t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, a_anf=a_anf, a_ans=a_ans, m_ae=m_ae, - m_ans=m_ans, theta=the, gamma=gam, phi=phi) + # A6 + t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, conf=conf) if t6 == np.inf: logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return logging.info("A6: {}".format(t6)) - # A1 - agent.reset() - for _ in range(int(t1 * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - min(the, 1 - phi)) < eps - # A3 - agent.reset() - for _ in range(int(t3 * hz)): - agent.set_power(p) - agent.perform_one_step() - if phi >= (1 - the): - assert abs(agent.get_h() - the) < eps - else: - assert abs(agent.get_h() - (1 - max(phi, gam))) < eps - # A4 - agent.reset() - for _ in range(int(t4 * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - (1 - gam)) < eps - # A5 - agent.reset() - for _ in range(int(t5 * hz)): - agent.set_power(p) - agent.perform_one_step() - if phi >= gam: - assert abs(agent.get_h() - (1 - gam)) < eps - else: - # A5 is only different from t4 if phi >= gamma - assert abs(agent.get_h() - (1 - phi)) < eps - # A6 - agent.reset() - for _ in range(int(t6 * hz)): - agent.set_power(p) - agent.perform_one_step() - assert abs(agent.get_h() - 1) < eps + ts = [t1, t2, t3, t4, t5, t6] + hts = [ht1, ht2, ht3, ht4, ht5, ht6] + gts = [gt1, gt2, gt3, gt4, gt5, gt6] + + for i, t in enumerate(ts): + try: + agent.reset() + for _ in range(int(t * hz)): + agent.set_power(p) + agent.perform_one_step() + + g_diff = agent.get_g() - gts[i] + h_diff = agent.get_h() - hts[i] + assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) + assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) + except AssertionError as e: + logging.info(e) - print(t6) + logging.info(conf) if __name__ == "__main__": @@ -109,13 +82,15 @@ def test_procedure(hz, eps, conf, agent): format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # estimations per second for discrete agent - hz = 100 + hz = 50 # required precision of discrete to differential agent eps = 0.001 udp = MultiObjectiveThreeCompUDP(None, None) example_conf = udp.create_educated_initial_guess() + # example_conf = [23300.62830028783, 55208.70561675477, 81.80334699080105, 4484.892862385766, 28.970996944175734, + # 0.24068111663641112, 0.2755929224243251, 0.5794389684110414] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], From 6c8050e38d86c4c57a63c9a03288fc6adf3715b9 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 1 Jul 2021 17:53:36 +1000 Subject: [PATCH 11/71] remove tolerance var from ODE simulator --- src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 483f663..60814ff 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -8,7 +8,6 @@ class ODEThreeCompHydSimulator: """ Simulates Three Component Hydraulic Model responses using Ordinary Differential Equations """ - tolerance = 1.49012e-8 @staticmethod def phase_a1(p: float, conf: list) -> (float, float, float): From 40f75e13d3aa18bc8e9c5df70de83fcc9c84672d Mon Sep 17 00:00:00 2001 From: faweigend Date: Fri, 2 Jul 2021 22:45:10 +1000 Subject: [PATCH 12/71] remove requirement of root solving by optimization for A3 and A4 --- .../agents/three_comp_hyd_agent.py | 12 ++ .../simulator/ode_three_comp_hyd_simulator.py | 151 +++++++----------- tests/tte_phase_tests.py | 18 ++- 3 files changed, 83 insertions(+), 98 deletions(-) diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 45ab985..8ce5ab4 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -311,12 +311,24 @@ def get_g(self): """ return self.__g + def set_g(self, g): + """ + setter for state of depletion of vessel AnS + """ + self.__g = g + def get_h(self): """ :return state of depletion of vessel AnF """ return self.__h + def set_h(self, h): + """ + setter for state of depletion of vessel AnF + """ + self.__h = h + def get_p_ae(self): """ :return flow from Ae to AnF diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 60814ff..b181173 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -23,7 +23,7 @@ def phase_a1(p: float, conf: list) -> (float, float, float): return np.inf, h, 0 # end of phase A1 -> the time when h(t) = min(theta,1-phi) - t1 = (-(a_anf * (1 - phi)) / m_ae) * math.log(1 - ((m_ae * min(theta, 1 - phi)) / (p * (1 - phi)))) + t1 = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * min(theta, 1 - phi) / (p * (1 - phi)))) # return t1, h(t1), g(t1) return t1, min(theta, 1 - phi), 0 @@ -62,47 +62,28 @@ def phase_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, # taken from Equation 11 by Morton 1986 a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - b = (m_ae * m_ans) / ( + b = m_ae * m_ans / ( a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - c = (m_ans * (p * (1 - phi) - m_ae * theta)) / ( + c = m_ans * (p * (1 - phi) - m_ae * theta) / ( a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - # use auxiliary equation to derive both negative roots r1 and r2 - coeff = [1, a, b] - r1, r2 = np.roots(coeff) + # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) + r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) def a3_gt(c1, c2, t): # the general solution for g(t) return c1 * np.exp(r1 * t) + c2 * np.exp(r2 * t) + c / b - def a3_dgt(c1, c2, t): - # derivative of general solution - return r1 * c1 * np.exp(r1 * t) + r2 * c2 * math.exp(r2 * t) - - def a3_gtdgt(c1): - c2 = -(r1 * c1 * np.exp(r1 * t2)) / (r2 * np.exp(r2 * t2)) - return np.abs(c1 * np.exp(r1 * t2) + c2 * np.exp(r2 * t2) + c / b) - - # Find c1 and c2 that satisfy g(t2) and dg(t2) = 0 - # res = optimize.fsolve(lambda cx: [ - # a3_gt(cx[0], cx[1], t2), - # a3_dgt(cx[0], cx[1], t2) - # ], x0=np.array([0, 0]), xtol=ODEThreeCompHydSimulator.tolerance) - # s_c1 = float(res[0]) - # s_c2 = float(res[1]) - - s_c1 = optimize.fsolve(a3_gtdgt, x0=np.array([0]))[0] - s_c2 = -(r1 * s_c1 * np.exp(r1 * t2)) / (r2 * np.exp(r2 * t2)) - - # check with g(t) and g'(t) - # assert np.abs(a3_gt(s_c1, s_c2, t2)) < 0.0001 - # assert np.abs(a3_dgt(s_c1, s_c2, t2)) < 0.0001 + # c1 and c2 can be determined with g(t2) = g'(t2) = 0 + s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t2) + s_c2 = s_c1 * np.exp(r1 * t2) * np.exp(-r2 * t2) * -r1 / r2 # substitute into EQ(9) for h def a3_ht(t): - k1 = ((a_ans * (1 - theta - gamma) / m_ans) * s_c1 * r1 + s_c1) - k2 = ((a_ans * (1 - theta - gamma) / m_ans) * s_c2 * r2 + s_c2) - return k1*np.exp(r1*t) + k2*np.exp(r2*t) + c/b + theta + k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 + k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta # if phi > gamma, then phase A3 transitions into phase A4 before AnS is empty ht3 = 1 - max(phi, gamma) @@ -111,7 +92,7 @@ def a3_ht(t): if ht2 <= a3_ht(np.inf) <= ht3: return np.inf, a3_ht(np.inf), a3_gt(s_c1, s_c2, np.inf) else: - t3 = optimize.fsolve(lambda t: np.abs(ht3 - a3_ht(t)), x0=np.array([t2]))[0] + t3 = optimize.newton(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] # use t3 to get g(t3) return t3, ht3, a3_gt(s_c1, s_c2, t3) @@ -130,57 +111,35 @@ def phase_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, if gamma >= phi: return t3, ht3, gt3 - a = ((a_anf + a_ans) * m_ans) / (a_anf * a_ans * (1 - theta - gamma)) - b = ((p - m_ae) * m_ans) / (a_anf * a_ans * (1 - theta - gamma)) + # b is not needed by simplifying b/a as (p-m_ae)/(a_anf + a_ans) + a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + b = (p - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) def a4_gt(c1, c2, t): # general solution for g(t) - return (((b * t) + (c1 * math.exp(-a * t))) / a) + c2 + return t * b / a + c2 + c1 / a * np.exp(-a * t) - def a4_dgt(c1, _, t): + def a4_dgt(c1, t): # first derivative g'(t) - return (b / a) - c1 * math.exp(-a * t) + return b / a - c1 * np.exp(-a * t) # derivative g'(t3) can be calculated manually - dgt3 = (m_ans * (ht3 - gt3 - theta)) / (a_ans * (1 - theta - gamma)) - - def a4_gtdgt(c2): - c1 = (b / a / np.exp(-a * t3)) - dgt3 - return np.abs(((((b * t3) + (c1 * math.exp(-a * t3))) / a) + c2) - gt3) - - # def general_gt(p): - # c1, c2 = p - # # g(t3) = previously estimated gt3 - # est_gt3 = a4_gt(c1, c2, t3) - # # g'(t3) = manually estimated derivative - # est_dgt3 = a4_dgt(c1, c2, t3) - # return abs(est_gt3 - gt3), abs(est_dgt3 - dgt3) - # - # # solve for c1 and c2 - # res = optimize.fsolve(general_gt, x0=np.array([0, 0]), xtol=ODEThreeCompHydSimulator.tolerance) - # s_c1 = float(res[0]) - # s_c2 = float(res[1]) - - s_c2 = optimize.fsolve(a4_gtdgt, x0=np.array([0]))[0] - s_c1 = (b / a / np.exp(-a * t3)) - dgt3 - - # check with g(t) and g'(t) - # assert np.abs(a4_gt(s_c1, s_c2, t3) - gt3) < 0.0001 - # print(a4_dgt(s_c1, s_c2, t3), dgt3) - # assert np.abs(a4_dgt(s_c1, s_c2, t3) - dgt3) < 0.0001 + dgt3 = m_ans * (ht3 - gt3 - theta) / (a_ans * (1 - theta - gamma)) + + # which then allows to derive c1 and c2 + s_c1 = (b / a - dgt3) * np.exp(a * t3) + s_c2 = (-t3 * b + dgt3) / a - b / a ** 2 + gt3 def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) - return ((a_ans * (1 - theta - gamma)) / m_ans) * a4_dgt(s_c1, s_c2, t) + a4_gt(s_c1, s_c2, t) + theta + return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(s_c1, t) + a4_gt(s_c1, s_c2, t) + theta - ht4 = (1 - gamma) + ht4 = 1 - gamma # check if equilibrium in this phase if ht3 <= a4_ht(np.inf) <= ht4: return np.inf, a4_ht(np.inf), a4_gt(s_c1, s_c2, np.inf) else: - # find end of A4 where g(t4) = (1-gamma) - # t4 = optimize.minimize(lambda t: np.abs(ht4 - a4_ht(t)), x0=np.array([t3]))['x'] - t4 = optimize.fsolve(lambda t: np.abs(ht4 - a4_ht(t)), x0=np.array([t3]))[0] + t4 = optimize.newton(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] return t4, ht4, a4_gt(s_c1, s_c2, t4) @staticmethod @@ -200,36 +159,38 @@ def phase_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, def a5_gt(c, t): # generalised g(t) for phase A5 - return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + return (1 - theta - gamma) + c * math.exp(-m_ans * t / ((1 - theta - gamma) * a_ans)) - # find c that fits to known g(t4) = gt4 - s_cg = optimize.fsolve(lambda c: np.abs(gt4 - a5_gt(c, t4)), x0=np.array([0]))[0] - # s_cg = optimize.minimize(lambda c: np.abs(gt4 - a5_gt(c, t4)), x0=np.array([0]))['x'] - - # assert np.abs(a5_gt(s_cg, t4) - gt4) < 0.0001 + # find values for positive and negative signs + pos = (theta + gamma - 2) / math.exp(-m_ans * t4 / ((1 - theta - gamma) * a_ans)) + neg = (theta + gamma) / math.exp(-m_ans * t4 / ((1 - theta - gamma) * a_ans)) + s_cg = optimize.brentq(lambda c: gt4 - a5_gt(c, t4), a=pos, b=neg) # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) + # g not necessary as g/a = p*(1-phi)/m_ae a = -m_ae / ((1 - phi) * a_anf) - b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) - g = p / a_anf + b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) def a5_ht(c, t): - return ((-b * math.exp(-k * t)) / (a + k)) + (c * math.exp(a * t)) - (g / a) + return (-b * math.exp(-k * t) / (a + k)) + p * (1 - phi) / m_ae + (c * math.exp(a * t)) + mp = math.exp(a * t4) # multiplied part of a5_ht(t4) : c * mp + fp = a5_ht(0, t4) # first part in a5_ht(t4) : fp + c*mp + pos = (-fp + 1) / mp + neg = (-fp - 1) / mp # find c that matches h(t4) = ht4 - s_ch = optimize.fsolve(lambda c: np.abs(ht4 - a5_ht(c, t4)), x0=np.array([0]))[0] - # s_ch = optimize.minimize(lambda c: np.abs(ht4 - a5_ht(c, t4)), x0=np.array([0]))['x'] + s_ch = optimize.brentq(lambda c: ht4 - a5_ht(c, t4), a=pos, b=neg) - ht5 = (1 - phi) + ht5 = 1 - phi # check if equilibrium in this phase if ht4 <= a5_ht(np.inf, s_ch) <= ht5: return np.inf, a5_ht(np.inf, s_ch), a5_gt(s_cg, np.inf) else: # solve for time point where phase A5 ends h(t5)=(1-phi) - - t5 = optimize.fsolve(lambda t: np.abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))[0] - # t5 = optimize.minimize(lambda t: np.abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))['x'] + t5 = optimize.newton(lambda t: ht5 - a5_ht(s_ch, t), x0=np.array([t4]))[0] + # t5 = optimize.fsolve(lambda t: ht5 - a5_ht(s_ch, t), x0=np.array([t4]))[0] + # t5 = optimize.minimize(lambda t: np.abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))['x'][0] return t5, ht5, a5_gt(s_cg, t5) @staticmethod @@ -246,10 +207,10 @@ def a6_gt(c, t): # generalised g(t) for phase A6 return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) - # s_cg = optimize.minimize(lambda c: np.abs(gt5 - a6_gt(c, t5)), x0=np.array([0]))['x'] - s_cg = optimize.fsolve(lambda c: np.abs(gt5 - a6_gt(c, t5)), x0=np.array([0]))[0] - - # assert np.abs(a6_gt(s_cg, t5) - gt5) < 0.0001 + # find values for positive and negative signs + pos = (theta + gamma - 2) / math.exp((-m_ans * t5) / ((1 - theta - gamma) * a_ans)) + neg = (theta + gamma) / math.exp((-m_ans * t5) / ((1 - theta - gamma) * a_ans)) + s_cg = optimize.brentq(lambda c: gt5 - a6_gt(c, t5), a=pos, b=neg) k = m_ans / ((1 - theta - gamma) * a_ans) a = -m_ae / a_anf @@ -258,15 +219,19 @@ def a6_gt(c, t): def a6_ht(cx, t): # generalised h(t) for phase A6 - return (a * t) - ((b * math.exp(-k * t)) / k) + cx + t * g + return t * (a + g) - ((b * math.exp(-k * t)) / k) + cx + fp = a6_ht(0, t5) # get first part of equation for t5 to estimate limits for sign change + if fp >= 0: + pos = ht5 + 1 + neg = -fp + else: + pos = -fp + ht5 + 1 + neg = 0 # find ch that matches h(t5) = ht5 - s_ch = optimize.fsolve(lambda c: np.abs(ht5 - a6_ht(c, t5)), x0=np.array([0]))[0] - # s_ch = optimize.minimize(lambda c: np.abs(ht5 - a6_ht(c, t5)), x0=np.array([0]))['x'] + s_ch = optimize.brentq(lambda c: ht5 - a6_ht(c, t5), a=pos, b=neg) ht6 = 1.0 # find end of phase A6. The time point where h(t6)=1 - # condition t > t5 added - t6 = optimize.fsolve(lambda t: np.abs(ht6 - a6_ht(s_ch, t)), x0=np.array([t5]))[0] - # t6 = optimize.minimize(lambda t: np.abs(ht6 - a6_ht(s_ch, t)), x0=np.array([t5]))['x'] + t6 = optimize.newton(lambda t: ht6 - a6_ht(s_ch, t), x0=np.array([t5]))[0] return t6, ht6, a6_gt(s_cg, t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 501f526..dee795c 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -62,12 +62,14 @@ def test_procedure(hz, eps, conf, agent): for i, t in enumerate(ts): try: agent.reset() - for _ in range(int(t * hz)): + for _ in range(int(round(t * hz))): agent.set_power(p) agent.perform_one_step() g_diff = agent.get_g() - gts[i] h_diff = agent.get_h() - hts[i] + # print("error phase {}. h is off by {}".format(i + 1, h_diff)) + # print("error phase {}. g is off by {}".format(i + 1, g_diff)) assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) except AssertionError as e: @@ -82,15 +84,21 @@ def test_procedure(hz, eps, conf, agent): format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # estimations per second for discrete agent - hz = 50 + hz = 100 # required precision of discrete to differential agent - eps = 0.001 + eps = 0.0001 udp = MultiObjectiveThreeCompUDP(None, None) example_conf = udp.create_educated_initial_guess() - # example_conf = [23300.62830028783, 55208.70561675477, 81.80334699080105, 4484.892862385766, 28.970996944175734, - # 0.24068111663641112, 0.2755929224243251, 0.5794389684110414] + # example_conf = [15021.785191487204, 40177.64712294647, 261.6719508403627, 3292.148542348498, 41.81050507575445, 0.24621701417812314, 0.23688759866161735, 0.31117164164526034] + # example_conf = [9514.740288582507, 65647.39956250248, 130.7003311770526, 4032.783980729493, 40.700090552043754, + # 0.36558373229156543, 0.18079431363534032, 0.8814530286203209] + # example_conf = [20409.661337284382, 68400.5919305085, 22.91122107670973, 1674.8953998582301, 10.090793349034278, + # 0.09540964848746722, 0.0754656005957027, 0.5277055169053692] + + # example_conf = [11842.40873575802, 66678.34427155198, 254.65770817627214, 3308.703276921823, 34.225650319734704, + # 0.3535049658149634, 0.2453871731326824, 0.7281633626474274] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], From 9560792dcd69473ec092c53b52c6116bc58a8248 Mon Sep 17 00:00:00 2001 From: faweigend Date: Sat, 3 Jul 2021 12:18:09 +1000 Subject: [PATCH 13/71] increase lower boundary of AnF and AnS, fix equilibrium check A5 and move constants out of g(t) and h(t) functions --- .../evolutionary_fitter/three_comp_tools.py | 4 +- .../simulator/ode_three_comp_hyd_simulator.py | 111 ++++++++---------- tests/tte_phase_tests.py | 56 ++++----- 3 files changed, 71 insertions(+), 100 deletions(-) diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index bc55302..ece9993 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -9,8 +9,8 @@ # bounds for all parameters of the three comp hydraulic model three_comp_parameter_limits = { - "a_anf": [1000, 500000], - "a_ans": [1000, 500000], + "a_anf": [5000, 500000], + "a_ans": [5000, 500000], "m_ae": [1, 5000], "m_ans": [1, 5000], "m_anf": [1, 5000], diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index b181173..01dc79a 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -71,14 +71,14 @@ def phase_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - def a3_gt(c1, c2, t): - # the general solution for g(t) - return c1 * np.exp(r1 * t) + c2 * np.exp(r2 * t) + c / b - # c1 and c2 can be determined with g(t2) = g'(t2) = 0 s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t2) s_c2 = s_c1 * np.exp(r1 * t2) * np.exp(-r2 * t2) * -r1 / r2 + def a3_gt(t): + # the general solution for g(t) + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b + # substitute into EQ(9) for h def a3_ht(t): k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 @@ -90,11 +90,11 @@ def a3_ht(t): # check if equilibrium in this phase if ht2 <= a3_ht(np.inf) <= ht3: - return np.inf, a3_ht(np.inf), a3_gt(s_c1, s_c2, np.inf) + return np.inf, a3_ht(np.inf), a3_gt(np.inf) else: t3 = optimize.newton(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] # use t3 to get g(t3) - return t3, ht3, a3_gt(s_c1, s_c2, t3) + return t3, ht3, a3_gt(t3) @staticmethod def phase_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, float, float): @@ -111,36 +111,36 @@ def phase_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, if gamma >= phi: return t3, ht3, gt3 - # b is not needed by simplifying b/a as (p-m_ae)/(a_anf + a_ans) + # b/a can be simplified as (p-m_ae)/(a_anf + a_ans) a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) b = (p - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - def a4_gt(c1, c2, t): - # general solution for g(t) - return t * b / a + c2 + c1 / a * np.exp(-a * t) - - def a4_dgt(c1, t): - # first derivative g'(t) - return b / a - c1 * np.exp(-a * t) - # derivative g'(t3) can be calculated manually dgt3 = m_ans * (ht3 - gt3 - theta) / (a_ans * (1 - theta - gamma)) # which then allows to derive c1 and c2 - s_c1 = (b / a - dgt3) * np.exp(a * t3) - s_c2 = (-t3 * b + dgt3) / a - b / a ** 2 + gt3 + s_c1 = ((p - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t3) + s_c2 = (-t3 * b + dgt3) / a - (p - m_ae) / ((a_anf + a_ans) * a) + gt3 + + def a4_gt(t): + # general solution for g(t) + return t * (p - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) + + def a4_dgt(t): + # first derivative g'(t) + return (p - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) - return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(s_c1, t) + a4_gt(s_c1, s_c2, t) + theta + return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta ht4 = 1 - gamma # check if equilibrium in this phase if ht3 <= a4_ht(np.inf) <= ht4: - return np.inf, a4_ht(np.inf), a4_gt(s_c1, s_c2, np.inf) + return np.inf, a4_ht(np.inf), a4_gt(np.inf) else: t4 = optimize.newton(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] - return t4, ht4, a4_gt(s_c1, s_c2, t4) + return t4, ht4, a4_gt(t4) @staticmethod def phase_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, float, float): @@ -157,41 +157,33 @@ def phase_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, if phi >= gamma: return t4, ht4, gt4 - def a5_gt(c, t): - # generalised g(t) for phase A5 - return (1 - theta - gamma) + c * math.exp(-m_ans * t / ((1 - theta - gamma) * a_ans)) + # g(t4) = gt4 can be solved for c + s_cg = (gt4 - (1 - theta - gamma)) / np.exp(-m_ans * t4 / ((1 - theta - gamma) * a_ans)) - # find values for positive and negative signs - pos = (theta + gamma - 2) / math.exp(-m_ans * t4 / ((1 - theta - gamma) * a_ans)) - neg = (theta + gamma) / math.exp(-m_ans * t4 / ((1 - theta - gamma) * a_ans)) - s_cg = optimize.brentq(lambda c: gt4 - a5_gt(c, t4), a=pos, b=neg) + def a5_gt(t): + # generalised g(t) for phase A5 + return (1 - theta - gamma) + s_cg * math.exp(-m_ans * t / ((1 - theta - gamma) * a_ans)) # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) - # g not necessary as g/a = p*(1-phi)/m_ae a = -m_ae / ((1 - phi) * a_anf) + # g/a = p*(1-phi)/m_ae b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - def a5_ht(c, t): - return (-b * math.exp(-k * t) / (a + k)) + p * (1 - phi) / m_ae + (c * math.exp(a * t)) - - mp = math.exp(a * t4) # multiplied part of a5_ht(t4) : c * mp - fp = a5_ht(0, t4) # first part in a5_ht(t4) : fp + c*mp - pos = (-fp + 1) / mp - neg = (-fp - 1) / mp # find c that matches h(t4) = ht4 - s_ch = optimize.brentq(lambda c: ht4 - a5_ht(c, t4), a=pos, b=neg) + s_ch = (ht4 + b * math.exp(-k * t4) / (a + k) - p * (1 - phi) / m_ae) * np.exp(-a * t4) + + def a5_ht(t): + return -b * math.exp(-k * t) / (a + k) + p * (1 - phi) / m_ae + s_ch * math.exp(a * t) ht5 = 1 - phi # check if equilibrium in this phase - if ht4 <= a5_ht(np.inf, s_ch) <= ht5: - return np.inf, a5_ht(np.inf, s_ch), a5_gt(s_cg, np.inf) + if ht4 <= a5_ht(np.inf) <= ht5: + return np.inf, a5_ht(np.inf), a5_gt(np.inf) else: - # solve for time point where phase A5 ends h(t5)=(1-phi) - t5 = optimize.newton(lambda t: ht5 - a5_ht(s_ch, t), x0=np.array([t4]))[0] - # t5 = optimize.fsolve(lambda t: ht5 - a5_ht(s_ch, t), x0=np.array([t4]))[0] - # t5 = optimize.minimize(lambda t: np.abs(ht5 - a5_ht(s_ch, t)), x0=np.array([t4]))['x'][0] - return t5, ht5, a5_gt(s_cg, t5) + # solve for time point where phase A5 ends h(t5) = 1-phi + t5 = optimize.newton(lambda t: ht5 - a5_ht(t), x0=np.array([t4]))[0] + return t5, ht5, a5_gt(t5) @staticmethod def phase_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): @@ -203,35 +195,26 @@ def phase_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, theta = conf[5] gamma = conf[6] - def a6_gt(c, t): - # generalised g(t) for phase A6 - return (1 - theta - gamma) + c * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + # g(t5) = gt5 can be solved for c + s_cg = (gt5 - (1 - theta - gamma)) / np.exp(-m_ans * t5 / ((1 - theta - gamma) * a_ans)) - # find values for positive and negative signs - pos = (theta + gamma - 2) / math.exp((-m_ans * t5) / ((1 - theta - gamma) * a_ans)) - neg = (theta + gamma) / math.exp((-m_ans * t5) / ((1 - theta - gamma) * a_ans)) - s_cg = optimize.brentq(lambda c: gt5 - a6_gt(c, t5), a=pos, b=neg) + def a6_gt(t): + # generalised g(t) for phase A6 + return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) k = m_ans / ((1 - theta - gamma) * a_ans) a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) g = p / a_anf - def a6_ht(cx, t): - # generalised h(t) for phase A6 - return t * (a + g) - ((b * math.exp(-k * t)) / k) + cx + # h(t5) = ht5 can be solved for c + s_ch = -t5 * (a + g) + ((b * math.exp(-k * t5)) / k) + ht5 - fp = a6_ht(0, t5) # get first part of equation for t5 to estimate limits for sign change - if fp >= 0: - pos = ht5 + 1 - neg = -fp - else: - pos = -fp + ht5 + 1 - neg = 0 - # find ch that matches h(t5) = ht5 - s_ch = optimize.brentq(lambda c: ht5 - a6_ht(c, t5), a=pos, b=neg) + def a6_ht(t): + # generalised h(t) for phase A6 + return t * (a + g) - ((b * math.exp(-k * t)) / k) + s_ch ht6 = 1.0 # find end of phase A6. The time point where h(t6)=1 - t6 = optimize.newton(lambda t: ht6 - a6_ht(s_ch, t), x0=np.array([t5]))[0] - return t6, ht6, a6_gt(s_cg, t6) + t6 = optimize.newton(lambda t: ht6 - a6_ht(t), x0=np.array([t5]))[0] + return t6, ht6, a6_gt(t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index dee795c..456d2a7 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -60,22 +60,19 @@ def test_procedure(hz, eps, conf, agent): gts = [gt1, gt2, gt3, gt4, gt5, gt6] for i, t in enumerate(ts): - try: - agent.reset() - for _ in range(int(round(t * hz))): - agent.set_power(p) - agent.perform_one_step() + agent.reset() + for _ in range(int(round(t * hz))): + agent.set_power(p) + agent.perform_one_step() + + g_diff = agent.get_g() - gts[i] + h_diff = agent.get_h() - hts[i] + # print("error phase {}. h is off by {}".format(i + 1, h_diff)) + # print("error phase {}. g is off by {}".format(i + 1, g_diff)) + assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) + assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) - g_diff = agent.get_g() - gts[i] - h_diff = agent.get_h() - hts[i] - # print("error phase {}. h is off by {}".format(i + 1, h_diff)) - # print("error phase {}. g is off by {}".format(i + 1, g_diff)) - assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) - assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) - except AssertionError as e: - logging.info(e) - logging.info(conf) if __name__ == "__main__": @@ -84,27 +81,18 @@ def test_procedure(hz, eps, conf, agent): format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # estimations per second for discrete agent - hz = 100 + hz = 150 # required precision of discrete to differential agent - eps = 0.0001 - - udp = MultiObjectiveThreeCompUDP(None, None) - - example_conf = udp.create_educated_initial_guess() - # example_conf = [15021.785191487204, 40177.64712294647, 261.6719508403627, 3292.148542348498, 41.81050507575445, 0.24621701417812314, 0.23688759866161735, 0.31117164164526034] - # example_conf = [9514.740288582507, 65647.39956250248, 130.7003311770526, 4032.783980729493, 40.700090552043754, - # 0.36558373229156543, 0.18079431363534032, 0.8814530286203209] - # example_conf = [20409.661337284382, 68400.5919305085, 22.91122107670973, 1674.8953998582301, 10.090793349034278, - # 0.09540964848746722, 0.0754656005957027, 0.5277055169053692] - - # example_conf = [11842.40873575802, 66678.34427155198, 254.65770817627214, 3308.703276921823, 34.225650319734704, - # 0.3535049658149634, 0.2453871731326824, 0.7281633626474274] + eps = 0.001 - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) + while True: + udp = MultiObjectiveThreeCompUDP(None, None) - ThreeCompVisualisation(agent) + example_conf = udp.create_educated_initial_guess() + logging.info(example_conf) + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) - test_procedure(hz, eps, example_conf, agent) + test_procedure(hz, eps, example_conf, agent) From 794a3276fcd237f6401720da1ab783800b7d1034 Mon Sep 17 00:00:00 2001 From: faweigend Date: Sat, 17 Jul 2021 16:44:20 +1000 Subject: [PATCH 14/71] rename p_exp to p_work and begin plot adjustments --- src/threecomphyd/data_structure/simple_rec_measures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/threecomphyd/data_structure/simple_rec_measures.py b/src/threecomphyd/data_structure/simple_rec_measures.py index 6e46f40..a7d112c 100644 --- a/src/threecomphyd/data_structure/simple_rec_measures.py +++ b/src/threecomphyd/data_structure/simple_rec_measures.py @@ -37,7 +37,7 @@ def __len__(self): def iterate_measures(self): """ iterates through all measures and returns the essential values for the objective function - :return: p_exp, p_rec, t_rec, expected + :return: p_work, p_rec, t_rec, expected """ for p_exp, p_rec, t_rec, expected in list(self.__measures): yield p_exp, p_rec, t_rec, expected From 228364ab2ffe7b225797ec8d1a84e3dd03cbdb6b Mon Sep 17 00:00:00 2001 From: faweigend Date: Sat, 17 Jul 2021 16:52:11 +1000 Subject: [PATCH 15/71] finalise p_exp to p_work adjustments --- .../data_structure/simple_rec_measures.py | 14 +++++++------- .../evolutionary_fitter/three_comp_tools.py | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/threecomphyd/data_structure/simple_rec_measures.py b/src/threecomphyd/data_structure/simple_rec_measures.py index a7d112c..bf1568f 100644 --- a/src/threecomphyd/data_structure/simple_rec_measures.py +++ b/src/threecomphyd/data_structure/simple_rec_measures.py @@ -1,6 +1,6 @@ class SimpleRecMeasures: """ - Used store W' recovery ratios in a consistent way for evolutionary fitting estimations + Used to store W' recovery ratios in a consistent way for evolutionary fitting estimations """ def __init__(self, name: str): @@ -11,15 +11,15 @@ def __init__(self, name: str): self.__name = name self.__measures = [] - def add_measure(self, p_power: float, r_power: float, r_time: int, recovery_percent: float): + def add_measure(self, p_work: float, p_rec: float, r_time: int, recovery_percent: float): """ adds one observationt to internal list - :param p_power: intensity that lead to exhaustion - :param r_power: recovery intensity + :param p_work: intensity that lead to exhaustion + :param p_rec: recovery intensity :param r_time: recovery time :param recovery_percent: recovery in percent """ - self.__measures.append((p_power, r_power, r_time, recovery_percent)) + self.__measures.append((p_work, p_rec, r_time, recovery_percent)) def __str__(self): """ @@ -39,8 +39,8 @@ def iterate_measures(self): iterates through all measures and returns the essential values for the objective function :return: p_work, p_rec, t_rec, expected """ - for p_exp, p_rec, t_rec, expected in list(self.__measures): - yield p_exp, p_rec, t_rec, expected + for p_work, p_rec, t_rec, expected in list(self.__measures): + yield p_work, p_rec, t_rec, expected @property def name(self): diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index ebd6578..9927158 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -246,7 +246,7 @@ def prepare_caen_recovery_ratios(w_p: float, cp: float): p8 = round(cp + w_p / 480, 2) # predicted exhaustion after 8 min cp33 = round(cp * 0.33, 2) cp66 = round(cp * 0.66, 2) - # sub, test, wb_power, r_power, r_time, recovery_percent + # sub, test, p_work, p_rec, r_time, recovery_percent caen_data = [[p4, cp33, 120, 55.0], [p4, cp33, 240, 61.0], [p4, cp33, 360, 70.5], @@ -261,7 +261,7 @@ def prepare_caen_recovery_ratios(w_p: float, cp: float): [p8, cp66, 360, 50.0]] # name indicates used measures recs = SimpleRecMeasures("caen") - for p_exp, p_rec, t_rec, r_percent in caen_data: - recs.add_measure(p_power=p_exp, r_power=p_rec, r_time=t_rec, recovery_percent=r_percent) + for p_work, p_rec, t_rec, r_percent in caen_data: + recs.add_measure(p_work=p_work, p_rec=p_rec, r_time=t_rec, recovery_percent=r_percent) # return simple recs object return recs From 7bb1c4bc0791fb891728155016625759f6e4722e Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 16 Aug 2021 13:00:34 +1000 Subject: [PATCH 16/71] exchange optimizers in ODE test procedures --- .../simulator/ode_three_comp_hyd_simulator.py | 29 +++++++++----- tests/tte_phase_tests.py | 39 +++++++++++++------ 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 01dc79a..3787482 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -92,7 +92,7 @@ def a3_ht(t): if ht2 <= a3_ht(np.inf) <= ht3: return np.inf, a3_ht(np.inf), a3_gt(np.inf) else: - t3 = optimize.newton(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] + t3 = optimize.fsolve(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] # use t3 to get g(t3) return t3, ht3, a3_gt(t3) @@ -139,7 +139,7 @@ def a4_ht(t): if ht3 <= a4_ht(np.inf) <= ht4: return np.inf, a4_ht(np.inf), a4_gt(np.inf) else: - t4 = optimize.newton(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] + t4 = optimize.fsolve(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] return t4, ht4, a4_gt(t4) @staticmethod @@ -158,23 +158,23 @@ def phase_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, return t4, ht4, gt4 # g(t4) = gt4 can be solved for c - s_cg = (gt4 - (1 - theta - gamma)) / np.exp(-m_ans * t4 / ((1 - theta - gamma) * a_ans)) + s_cg = (gt4 - (1 - theta - gamma)) * np.exp((m_ans * t4) / ((1 - theta - gamma) * a_ans)) def a5_gt(t): # generalised g(t) for phase A5 - return (1 - theta - gamma) + s_cg * math.exp(-m_ans * t / ((1 - theta - gamma) * a_ans)) + return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) a = -m_ae / ((1 - phi) * a_anf) - # g/a = p*(1-phi)/m_ae + g = p / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) # find c that matches h(t4) = ht4 - s_ch = (ht4 + b * math.exp(-k * t4) / (a + k) - p * (1 - phi) / m_ae) * np.exp(-a * t4) + s_ch = (ht4 + b / ((a + k) * np.exp(k) ** t4) + g / a) / np.exp(a) ** t4 def a5_ht(t): - return -b * math.exp(-k * t) / (a + k) + p * (1 - phi) / m_ae + s_ch * math.exp(a * t) + return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a ht5 = 1 - phi # check if equilibrium in this phase @@ -182,7 +182,7 @@ def a5_ht(t): return np.inf, a5_ht(np.inf), a5_gt(np.inf) else: # solve for time point where phase A5 ends h(t5) = 1-phi - t5 = optimize.newton(lambda t: ht5 - a5_ht(t), x0=np.array([t4]))[0] + t5 = optimize.fsolve(lambda t: a5_ht(t) - ht5, x0=np.array([t4]))[0] return t5, ht5, a5_gt(t5) @staticmethod @@ -216,5 +216,16 @@ def a6_ht(t): ht6 = 1.0 # find end of phase A6. The time point where h(t6)=1 - t6 = optimize.newton(lambda t: ht6 - a6_ht(t), x0=np.array([t5]))[0] + t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([t5]))[0] + + a5_ts = [] + for it in range(int(t5), int(t5 * 2)): + a5_ts.append(a6_ht(it) - ht6) + import matplotlib.pyplot as plt + fig, ax = plt.subplots() + ax.plot(range(int(t5), int(t5 * 2)), a5_ts) + ax.axhline(0) + ax.axvline(t6) + plt.show() + return t6, ht6, a6_gt(t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 456d2a7..c345999 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -69,10 +69,22 @@ def test_procedure(hz, eps, conf, agent): h_diff = agent.get_h() - hts[i] # print("error phase {}. h is off by {}".format(i + 1, h_diff)) # print("error phase {}. g is off by {}".format(i + 1, g_diff)) - assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) + assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) + +def the_loop(hz: int = 250, eps: float = 0.001): + while True: + udp = MultiObjectiveThreeCompUDP(None, None) + example_conf = udp.create_educated_initial_guess() + logging.info(example_conf) + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) + + test_procedure(hz, eps, example_conf, agent) if __name__ == "__main__": @@ -81,18 +93,21 @@ def test_procedure(hz, eps, conf, agent): format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # estimations per second for discrete agent - hz = 150 + hz = 250 # required precision of discrete to differential agent - eps = 0.001 + eps = 0.005 - while True: - udp = MultiObjectiveThreeCompUDP(None, None) + # the_loop(hz, eps) - example_conf = udp.create_educated_initial_guess() - logging.info(example_conf) - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) + example_conf = [5000, 53133.06670527823, 332.98870744202634, 4717.909662627442, 12.975264125113473, + 0.17417286563111362, 0.2375006803695677, 0.2908045985003225] + # example_conf = [9581.23047165942, 90743.2215076573, 327.6150272718813, 2043.9625552044683, 12.186334615899417, + # 0.29402816909441, 0.19588603394320103, 0.0753503316221355] + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) - test_procedure(hz, eps, example_conf, agent) + ThreeCompVisualisation(agent) + + test_procedure(hz, eps, example_conf, agent) From a8e7a07c92efcda6984217359c252be4a13f5650 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 18 Aug 2021 16:12:45 +1000 Subject: [PATCH 17/71] merge a+g in A6 --- .../simulator/ode_three_comp_hyd_simulator.py | 34 +++++++++++-------- tests/tte_phase_tests.py | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 3787482..688061d 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -203,29 +203,33 @@ def a6_gt(t): return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) k = m_ans / ((1 - theta - gamma) * a_ans) - a = -m_ae / a_anf + #a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) - g = p / a_anf + #g = p / a_anf + ag = (p-m_ae)/a_anf # h(t5) = ht5 can be solved for c - s_ch = -t5 * (a + g) + ((b * math.exp(-k * t5)) / k) + ht5 + s_ch = -t5 * ag + ((b * math.exp(-k * t5)) / k) + ht5 def a6_ht(t): # generalised h(t) for phase A6 - return t * (a + g) - ((b * math.exp(-k * t)) / k) + s_ch + return t * ag - ((b * math.exp(-k * t)) / k) + s_ch ht6 = 1.0 + # estimate an intitial guess that assumes no contribution from g + initial_guess = (ht6 - s_ch)/ag # find end of phase A6. The time point where h(t6)=1 - t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([t5]))[0] - - a5_ts = [] - for it in range(int(t5), int(t5 * 2)): - a5_ts.append(a6_ht(it) - ht6) - import matplotlib.pyplot as plt - fig, ax = plt.subplots() - ax.plot(range(int(t5), int(t5 * 2)), a5_ts) - ax.axhline(0) - ax.axvline(t6) - plt.show() + t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] + + # import matplotlib.pyplot as plt + # a5_ts = [] + # for it in range(int(t5), int(t6 + (t6-t5))): + # a5_ts.append(a6_ht(it) - ht6) + # + # fig, ax = plt.subplots() + # ax.plot(range(int(t5), int(t6 + (t6-t5))), a5_ts) + # ax.axhline(0) + # ax.axvline(t6) + # plt.show() return t6, ht6, a6_gt(t6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index c345999..c1903bd 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -97,7 +97,7 @@ def the_loop(hz: int = 250, eps: float = 0.001): # required precision of discrete to differential agent eps = 0.005 - # the_loop(hz, eps) + the_loop(hz, eps) example_conf = [5000, 53133.06670527823, 332.98870744202634, 4717.909662627442, 12.975264125113473, 0.17417286563111362, 0.2375006803695677, 0.2908045985003225] From cd114217995112ab179cb0903c3e8b588d0d99ed Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 23 Aug 2021 15:26:52 +1000 Subject: [PATCH 18/71] adjust educated guess and finish first round of TTE verification add first scaffold for rec trial verification --- .../evolutionary_fitter/three_comp_tools.py | 20 +++-- .../simulator/ode_three_comp_hyd_simulator.py | 65 ++++++++++++--- tests/rec_trial_tests.py | 81 +++++++++++++++++++ tests/tte_phase_tests.py | 68 ++++++++++------ 4 files changed, 191 insertions(+), 43 deletions(-) create mode 100644 tests/rec_trial_tests.py diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index 812c573..d147404 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -11,9 +11,9 @@ three_comp_parameter_limits = { "a_anf": [5000, 500000], "a_ans": [5000, 500000], - "m_ae": [1, 5000], - "m_ans": [1, 5000], - "m_anf": [1, 5000], + "m_ae": [1, 2000], + "m_ans": [1, 2000], + "m_anf": [1, 2000], "theta": [0.01, 0.99], "gamma": [0.01, 0.99], "phi": [0.01, 0.99] @@ -55,12 +55,18 @@ def create_educated_initial_guess(self, cp: float = 250.0, w_p: float = 50000.0) np.random.normal(1, 0.4) * w_p * 0.3, # size AnF is expected to be smaller than AnS np.random.normal(1, 0.4) * w_p, # size AnS is expected to be larger and correlated to W' np.random.normal(1, 0.4) * cp, # max flow from Ae should be related to CP - np.random.normal(1, 0.4) * cp * 10, # max flow from AnS is expected to be high + np.random.normal(1, 0.4) * cp, # max flow from AnS np.random.normal(1, 0.4) * cp * 0.1, # max recovery flow is expected to be low - np.random.normal(1, 0.4) * 0.25, # AnS needs a considerable height - np.random.normal(1, 0.4) * 0.25, # AnS needs a considerable height - np.random.normal(1, 0.4) * 0.5, # for a curvelinear expenditure dynamic the pipe has to be halfway or lower + np.random.normal(1, 0.4) * 0.25, # theta: top of AnS + np.random.normal(1, 0.4) * 0.25, # gamma: 1 - bottom of AnS + np.random.normal(1, 0.4) * 0.5, # phi: for a curvelinear expenditure the pipe should be halfway or lower ] + + # make sure AnS has a positive cross-sectional area + while i_x[5] + i_x[6] > 0.99: + i_x[5] = np.random.normal(1, 0.4) * 0.25 # theta + i_x[6] = np.random.normal(1, 0.4) * 0.25 # gamma + # ensure values are within limits for i in range(len(i_x)): i_x[i] = max(self.__bounds[0][i], i_x[i]) # lower bound diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 688061d..ecf6e0c 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -1,3 +1,4 @@ +import logging import math import numpy as np @@ -9,6 +10,43 @@ class ODEThreeCompHydSimulator: Simulates Three Component Hydraulic Model responses using Ordinary Differential Equations """ + @staticmethod + def tte(p_exp: float, conf: list, max_time: int = 5000) -> (float, float, float): + + # A1 + t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p_exp, conf=conf) + if t1 == np.inf or t1 > max_time: + logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) + return t1, ht1, gt1 + + t2, ht2, gt2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, ht1=ht1, gt1=gt1, p=p_exp, conf=conf) + + # A3 + t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p_exp, conf=conf) + if t3 == np.inf or t3 > max_time: + logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) + return t2, ht2, gt2 + + # A4 + t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p_exp, conf=conf) + if t4 == np.inf or t4 > max_time: + logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) + return t4, ht4, gt4 + + # A5 + t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p_exp, conf=conf) + if t5 == np.inf or t5 > max_time: + logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) + return t5, ht5, gt5 + + # A6 + t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p_exp, conf=conf) + if t6 == np.inf or t6 > max_time: + logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) + return t6, ht6, gt6 + + return t6, ht6, gt6 + @staticmethod def phase_a1(p: float, conf: list) -> (float, float, float): @@ -60,12 +98,19 @@ def phase_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, return t2, ht2, gt2 # taken from Equation 11 by Morton 1986 - a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( - a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - b = m_ae * m_ans / ( - a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - c = m_ans * (p * (1 - phi) - m_ae * theta) / ( - a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + # a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( + # a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + # my simplified form + a = m_ae / (a_anf * (1 - phi)) + \ + m_ans / (a_ans * (1 - theta - gamma)) + \ + m_ans / (a_anf * (1 - theta - gamma)) + + b = m_ae * m_ans / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + c = m_ans * (p * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) @@ -203,10 +248,10 @@ def a6_gt(t): return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) k = m_ans / ((1 - theta - gamma) * a_ans) - #a = -m_ae / a_anf + # a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) - #g = p / a_anf - ag = (p-m_ae)/a_anf + # g = p / a_anf + ag = (p - m_ae) / a_anf # h(t5) = ht5 can be solved for c s_ch = -t5 * ag + ((b * math.exp(-k * t5)) / k) + ht5 @@ -217,7 +262,7 @@ def a6_ht(t): ht6 = 1.0 # estimate an intitial guess that assumes no contribution from g - initial_guess = (ht6 - s_ch)/ag + initial_guess = (ht6 - s_ch) / ag # find end of phase A6. The time point where h(t6)=1 t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py new file mode 100644 index 0000000..7427615 --- /dev/null +++ b/tests/rec_trial_tests.py @@ -0,0 +1,81 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator + +import logging +import warnings + +import numpy as np + + +# warnings.filterwarnings("error") + + +def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): + max_time = 5000 + # A6 + t6, ht6, gt6 = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, max_time=max_time) + + # if TTE too long + if t6 > max_time: + return + + agent.reset() + for _ in range(int(round(t6 * hz))): + agent.set_power(p_exp) + agent.perform_one_step() + + g_diff = agent.get_g() - gt6 + h_diff = agent.get_h() - ht6 + + if log_level >= 2: + print("error tte1. h is off by {}".format(h_diff)) + print("error tte1. g is off by {}".format(g_diff)) + + assert abs(g_diff) < eps, "error tte1. g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "error tte1. h is off by {}".format(h_diff) + + # recovery begins here + rt6, rh6, rg6 = ODEThreeCompHydSimulator.phase_a6(t5=t6, ht5=ht6, gt5=gt6, p=p_rec, conf=conf) + print(t6, ht6, gt6) + print(rt6, rh6, rg6) + + +def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, + t_rec: int = 240, hz: int = 250, eps: float = 0.001): + """ + creates random agents and tests the discretised against the differential one + """ + + while True: + udp = MultiObjectiveThreeCompUDP(None, None) + + example_conf = udp.create_educated_initial_guess() + logging.info(example_conf) + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, + hz=hz, eps=eps, conf=example_conf, + agent=agent) + + break + + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 100 + t_rec = 240 + + # estimations per second for discrete agent + hz = 250 + # required precision of discrete to differential agent + eps = 0.005 + + the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index c1903bd..3b46708 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -12,14 +12,13 @@ # warnings.filterwarnings("error") -def test_procedure(hz, eps, conf, agent): - p = 350 - +def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): # all TTE phases + max_time = 5000 # A1 t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p, conf=conf) - if t1 == np.inf: + if t1 == np.inf or t1 > max_time: logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return logging.info("A1: {}".format(t1)) @@ -29,28 +28,28 @@ def test_procedure(hz, eps, conf, agent): # A3 t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, conf=conf) - if t3 == np.inf: + if t3 == np.inf or t3 > max_time: logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return logging.info("A3: {}".format(t3)) # A4 t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, conf=conf) - if t4 == np.inf: + if t4 == np.inf or t4 > max_time: logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return logging.info("A4: {}".format(t4)) # A5 t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, conf=conf) - if t5 == np.inf: + if t5 == np.inf or t5 > max_time: logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return logging.info("A5: {}".format(t5)) # A6 t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, conf=conf) - if t6 == np.inf: + if t6 == np.inf or t6 > max_time: logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return logging.info("A6: {}".format(t6)) @@ -67,13 +66,22 @@ def test_procedure(hz, eps, conf, agent): g_diff = agent.get_g() - gts[i] h_diff = agent.get_h() - hts[i] - # print("error phase {}. h is off by {}".format(i + 1, h_diff)) - # print("error phase {}. g is off by {}".format(i + 1, g_diff)) + + if log_level >= 2: + print("error phase {}. h is off by {}".format(i + 1, h_diff)) + print("error phase {}. g is off by {}".format(i + 1, g_diff)) + assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) -def the_loop(hz: int = 250, eps: float = 0.001): +def the_loop(p: float = 350.0, + hz: int = 250, + eps: float = 0.001): + """ + creates random agents and tests the discretised against the differential one + """ + while True: udp = MultiObjectiveThreeCompUDP(None, None) @@ -83,8 +91,28 @@ def the_loop(hz: int = 250, eps: float = 0.001): agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) + tte_test_procedure(p, hz, eps, example_conf, agent) + + +def test_one_config(example_conf=None): + """ + tests given configuration and puts out some more details + """ + + # just a default value + if example_conf is None: + example_conf = [23673.002563739556, 19264.71349151817, 349.17619370868493, + 387.84166460276015, 25.973978416729608, + 0.3003437353302002, 0.317490719771348, 0.6722371555886124] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) + + ThreeCompVisualisation(agent) - test_procedure(hz, eps, example_conf, agent) + tte_test_procedure(p, hz, eps, example_conf, agent, log_level=2) if __name__ == "__main__": @@ -92,22 +120,10 @@ def the_loop(hz: int = 250, eps: float = 0.001): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + p = 350 # estimations per second for discrete agent hz = 250 # required precision of discrete to differential agent eps = 0.005 - the_loop(hz, eps) - - example_conf = [5000, 53133.06670527823, 332.98870744202634, 4717.909662627442, 12.975264125113473, - 0.17417286563111362, 0.2375006803695677, 0.2908045985003225] - # example_conf = [9581.23047165942, 90743.2215076573, 327.6150272718813, 2043.9625552044683, 12.186334615899417, - # 0.29402816909441, 0.19588603394320103, 0.0753503316221355] - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) - - ThreeCompVisualisation(agent) - - test_procedure(hz, eps, example_conf, agent) + the_loop(p=p, hz=hz, eps=eps) From df167ea9161f6618181147cc0cf8287e3f3bcaf1 Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 24 Aug 2021 18:42:36 +1000 Subject: [PATCH 19/71] [WIP] Ad phase A6 recovery and begin phase A4 --- .../simulator/ode_three_comp_hyd_simulator.py | 70 ++++++++++--- tests/rec_a4_trial.py | 97 +++++++++++++++++++ tests/rec_a6_trial.py | 87 +++++++++++++++++ tests/rec_trial_tests.py | 25 ++++- 4 files changed, 263 insertions(+), 16 deletions(-) create mode 100644 tests/rec_a4_trial.py create mode 100644 tests/rec_a6_trial.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index ecf6e0c..52f581d 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -231,8 +231,65 @@ def a5_ht(t): return t5, ht5, a5_gt(t5) @staticmethod - def phase_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): + def phase_a6_rec(gt6: float, p_rec: float, conf: list): + """ + recovery from exhaustive exercise. Assumes h reached 1.0 and resets time to 0. + :param gt6: start g(t=0) + :param p_rec: constant recovery intensity + :param conf: hydraulic model configuration + :return: [time at which A6 rec ends, h(rt6), g(rt6)] + """ + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + t6 = 0 + ht6 = 1.0 + + # g(t6) = gt6 can be solved for c + s_cg = (gt6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) + + def a6_gt(t): + # generalised g(t) for phase A6 + return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + + k = m_ans / ((1 - theta - gamma) * a_ans) + # a = -m_ae / a_anf + b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) + # g = p / a_anf + ag = (p_rec - m_ae) / a_anf + + # h(t6) = 1 can be solved for c + s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + ht6 + + def a6_ht(t): + # generalised h(t) for recovery phase A6 + return t * ag - ((b * math.exp(-k * t)) / k) + s_ch + + # TODO: needs a check for 1-phi + ht4 = 1 - gamma + # estimate an initial guess that assumes no contribution from g + initial_guess = 0 + + rt6 = optimize.fsolve(lambda t: a6_ht(t) - ht4, x0=np.array([initial_guess]))[0] + + return rt6, ht4, a6_gt(rt6) + + @staticmethod + def phase_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): + """ + Final phase A6 of a time to exhaustion trial. Expects inputs from Phase A5. + :param t5: time at which A5 ended + :param ht5: h(t5) + :param gt5: g(t5) + :param p: constant power output + :param conf: configuration of hydraulic model + :return: [t6: time until h=1, h(t6)=1, g(t6)] + """ a_anf = conf[0] a_ans = conf[1] m_ae = conf[2] @@ -266,15 +323,4 @@ def a6_ht(t): # find end of phase A6. The time point where h(t6)=1 t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] - # import matplotlib.pyplot as plt - # a5_ts = [] - # for it in range(int(t5), int(t6 + (t6-t5))): - # a5_ts.append(a6_ht(it) - ht6) - # - # fig, ax = plt.subplots() - # ax.plot(range(int(t5), int(t6 + (t6-t5))), a5_ts) - # ax.axhline(0) - # ax.axvline(t6) - # plt.show() - return t6, ht6, a6_gt(t6) diff --git a/tests/rec_a4_trial.py b/tests/rec_a4_trial.py new file mode 100644 index 0000000..4d6b3a7 --- /dev/null +++ b/tests/rec_a4_trial.py @@ -0,0 +1,97 @@ +import math + +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from scipy import optimize + +import logging +import warnings + +import numpy as np + +# warnings.filterwarnings("error") + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 100 + t_rec = 240 + # estimations per second for discrete agent + hz = 250 + + conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + # PHASE A4 + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # derived from tte estimator and + t4 = 0 + ht4 = 0.6475620355865783 + gt4 = 0.15679831105786776 + + # b/a can be simplified as (p-m_ae)/(a_anf + a_ans) + a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + b = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + + # derivative g'(t3) can be calculated manually + dgt3 = m_ans * (ht3 - gt3 - theta) / (a_ans * (1 - theta - gamma)) + + # which then allows to derive c1 and c2 + s_c1 = ((p_rec - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t3) + s_c2 = (-t3 * b + dgt3) / a - (p_rec - m_ae) / ((a_anf + a_ans) * a) + gt3 + + + def a4_gt(t): + # general solution for g(t) + return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) + + + def a4_dgt(t): + # first derivative g'(t) + return (p_rec - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) + + + def a4_ht(t): + # EQ(9) with constants for g(t) and g'(t) + return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta + + + ht4 = 1 - gamma + # check if equilibrium in this phase + if ht3 <= a4_ht(np.inf) <= ht4: + return np.inf, a4_ht(np.inf), a4_gt(np.inf) + else: + t4 = optimize.fsolve(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] + return t4, ht4, a4_gt(t4) + + agent.reset() + agent.set_g(gt4) + agent.set_h(ht4) + agent.set_power(p_rec) + + for i in range(int(rt6+1)): + for s in range(agent.hz): + agent.perform_one_step() + + print("h: ", a6_ht(rt6) - agent.get_h()) + print("g: ", a6_gt(rt6) - agent.get_g()) \ No newline at end of file diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py new file mode 100644 index 0000000..106dcbf --- /dev/null +++ b/tests/rec_a6_trial.py @@ -0,0 +1,87 @@ +import math + +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from scipy import optimize + +import logging +import warnings + +import numpy as np + +# warnings.filterwarnings("error") + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 100 + t_rec = 240 + # estimations per second for discrete agent + hz = 250 + + conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], + m_ans=conf[3], m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + # PHASE A6 + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + t6 = 0 + ht6 = 1.0 + gt6 = 0.28293497466056705 + + # g(t6) = gt6 can be solved for c + s_cg = (gt6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) + + + def a6_gt(t): + # generalised g(t) for phase A6 + return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + + + k = m_ans / ((1 - theta - gamma) * a_ans) + # a = -m_ae / a_anf + b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) + # g = p / a_anf + ag = (p_rec - m_ae) / a_anf + + # h(t6) = 1 can be solved for c + s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + ht6 + + + def a6_ht(t): + # generalised h(t) for recovery phase A6 + return t * ag - ((b * math.exp(-k * t)) / k) + s_ch + + ht4 = 1 - gamma + # estimate an initial guess that assumes no contribution from g + initial_guess = 0 + rt6 = optimize.fsolve(lambda t: a6_ht(t) - ht4, x0=np.array([initial_guess]))[0] + + agent.reset() + agent.set_g(gt6) + agent.set_h(1.0) + agent.set_power(p_rec) + + for i in range(int(rt6+1)): + for s in range(agent.hz): + agent.perform_one_step() + + print("h: ", a6_ht(rt6) - agent.get_h()) + print("g: ", a6_gt(rt6) - agent.get_g()) \ No newline at end of file diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 7427615..63af57f 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -37,9 +37,21 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): assert abs(h_diff) < eps, "error tte1. h is off by {}".format(h_diff) # recovery begins here - rt6, rh6, rg6 = ODEThreeCompHydSimulator.phase_a6(t5=t6, ht5=ht6, gt5=gt6, p=p_rec, conf=conf) - print(t6, ht6, gt6) - print(rt6, rh6, rg6) + rt6, rh6, rg6 = ODEThreeCompHydSimulator.phase_a6_rec(gt6=gt6, p_rec=p_rec, conf=conf) + + for _ in range(int(round(rt6 * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + + g_diff = agent.get_g() - rg6 + h_diff = agent.get_h() - rh6 + + if log_level >= 2: + print("error tte1. h is off by {}".format(h_diff)) + print("error tte1. g is off by {}".format(g_diff)) + + assert abs(g_diff) < eps, "error rec1. g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "error rec1. h is off by {}".format(h_diff) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, @@ -52,14 +64,19 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, udp = MultiObjectiveThreeCompUDP(None, None) example_conf = udp.create_educated_initial_guess() + example_conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, 363.2970828395908, 38.27073086773415, + 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] logging.info(example_conf) # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) + + ThreeCompVisualisation(agent) + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps, conf=example_conf, - agent=agent) + agent=agent, log_level=2) break From a0350f72194c6790d2e71c03fcdf7670788cff1d Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 25 Aug 2021 17:04:29 +1000 Subject: [PATCH 20/71] [WIP] Adjust phase A4 rec to be split into two and verify first half --- tests/rec_a4_trial.py | 48 +++++++++++++++++++++++++--------------- tests/tte_phase_tests.py | 2 ++ 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/tests/rec_a4_trial.py b/tests/rec_a4_trial.py index 4d6b3a7..879d209 100644 --- a/tests/rec_a4_trial.py +++ b/tests/rec_a4_trial.py @@ -40,6 +40,7 @@ a_ans = conf[1] m_ae = conf[2] m_ans = conf[3] + m_anf = conf[4] theta = conf[5] gamma = conf[6] phi = conf[7] @@ -49,26 +50,32 @@ ht4 = 0.6475620355865783 gt4 = 0.15679831105786776 - # b/a can be simplified as (p-m_ae)/(a_anf + a_ans) - a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - b = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + # if g is above h (flow from AnS into AnF) + a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + b_gh = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - # derivative g'(t3) can be calculated manually - dgt3 = m_ans * (ht3 - gt3 - theta) / (a_ans * (1 - theta - gamma)) + # derivative g'(t4) can be calculated manually + dgt4_gh = m_ans * (ht4 - gt4 - theta) / (a_ans * (1 - theta - gamma)) # which then allows to derive c1 and c2 - s_c1 = ((p_rec - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t3) - s_c2 = (-t3 * b + dgt3) / a - (p_rec - m_ae) / ((a_anf + a_ans) * a) + gt3 + s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) + s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + gt4 + # if h is above g (flow from AnF into AnS) + # a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) + # b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) + # + # dgt4_hg = -m_anf * (gt4 + theta - ht4) / (a_ans * (1 - gamma)) + def a4_gt(t): # general solution for g(t) - return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) + return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) def a4_dgt(t): # first derivative g'(t) - return (p_rec - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) + return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_gh * t) def a4_ht(t): @@ -76,22 +83,27 @@ def a4_ht(t): return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - ht4 = 1 - gamma + ht_end = 1 - phi # check if equilibrium in this phase - if ht3 <= a4_ht(np.inf) <= ht4: - return np.inf, a4_ht(np.inf), a4_gt(np.inf) - else: - t4 = optimize.fsolve(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] - return t4, ht4, a4_gt(t4) + + t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] + gt_end = a4_gt(t_end) + + print(t_end) + print(gt4 + theta, ht4) + print(gt_end + theta, ht_end, a4_ht(t_end)) agent.reset() agent.set_g(gt4) agent.set_h(ht4) agent.set_power(p_rec) - for i in range(int(rt6+1)): + for i in range(int(t_end)): + print("step {}".format(i)) + print("h: ", a4_ht(i) - agent.get_h()) + print("g: ", a4_gt(i) - agent.get_g()) for s in range(agent.hz): agent.perform_one_step() - print("h: ", a6_ht(rt6) - agent.get_h()) - print("g: ", a6_gt(rt6) - agent.get_g()) \ No newline at end of file + print("h: ", a4_ht(int(t_end)) - agent.get_h()) + print("g: ", a4_gt(int(t_end)) - agent.get_g()) \ No newline at end of file diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 3b46708..655f993 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -91,6 +91,8 @@ def the_loop(p: float = 350.0, agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) + + ThreeCompVisualisation(agent) tte_test_procedure(p, hz, eps, example_conf, agent) From b5072e823803f97a3bfdf56c552e295a06967637 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 26 Aug 2021 15:47:42 +1000 Subject: [PATCH 21/71] [WIP] Add A3 R1 trial. c1' and c2' are still way off --- tests/rec_a3_r1_trial.py | 108 +++++++++++++++++++++++++++++++++++++++ tests/rec_a4_trial.py | 19 +------ tests/rec_a6_trial.py | 13 ++--- 3 files changed, 114 insertions(+), 26 deletions(-) create mode 100644 tests/rec_a3_r1_trial.py diff --git a/tests/rec_a3_r1_trial.py b/tests/rec_a3_r1_trial.py new file mode 100644 index 0000000..5c7aa7b --- /dev/null +++ b/tests/rec_a3_r1_trial.py @@ -0,0 +1,108 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 100 + + # estimations per second for discrete agent + hz = 250 + + conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + t3_exp = 200.58698045085373 + t3 = 0 + ht3 = 0.7294083375575103 + gt3 = 0.31236471544758654 + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # my simplified form + a = m_ae / (a_anf * (1 - phi)) + \ + m_ans / (a_ans * (1 - theta - gamma)) + \ + m_ans / (a_anf * (1 - theta - gamma)) + + b = m_ae * m_ans / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + c = m_ans * (p_exp * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + # a new c' for p_rec + c_p = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) + r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) + + # derive the solution from a tte with p_exp + s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t3_exp) + s_c2 = s_c1 * np.exp(r1 * t3_exp) * np.exp(-r2 * t3_exp) * -r1 / r2 + + # use tte solution to determine c1' and c2' + s_c1_p = (gt3 - c_p / b) / ((1 - r1 / r2) * np.exp(-r1 * t3)) + s_c2_p = (r1 * s_c1 * np.exp(r1 * t3_exp) + + r2 * s_c2 * np.exp(r2 * t3_exp) - + r1 * s_c1_p * np.exp(r1 * t3_exp)) / \ + (r2 * np.exp(r2 * t3_exp)) + + def a3_gt(t): + # the general solution for g(t) + return s_c1_p * np.exp(r1 * t) + s_c2_p * np.exp(r2 * t) + c / b + + + # substitute into EQ(9) for h + def a3_ht(t): + k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1_p * r1 + s_c1_p + k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2_p * r2 + s_c2_p + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + + + eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([0]))[0] + + + print(a3_gt(0) + theta, a3_ht(0)) + print(gt3 + theta, ht3) + + print(eq_gh) + print(a3_gt(eq_gh) + theta, a3_ht(eq_gh)) + + agent.reset() + agent.set_g(gt3) + agent.set_h(ht3) + agent.set_power(p_rec) + + # for i in range(int(eq_gh)): + # print("step {}".format(i)) + # print("h: ", a3_ht(int(eq_gh)) - agent.get_h()) + # print("g: ", a3_gt(int(eq_gh)) - agent.get_g()) + # for s in range(agent.hz): + # agent.perform_one_step() + # + # print("h: ", a3_ht(int(eq_gh)) - agent.get_h()) + # print("g: ", a3_gt(int(eq_gh)) - agent.get_g()) diff --git a/tests/rec_a4_trial.py b/tests/rec_a4_trial.py index 879d209..96289cd 100644 --- a/tests/rec_a4_trial.py +++ b/tests/rec_a4_trial.py @@ -1,18 +1,10 @@ -import math - from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from scipy import optimize import logging -import warnings import numpy as np -# warnings.filterwarnings("error") - if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, @@ -20,7 +12,7 @@ p_exp = 350 p_rec = 100 - t_rec = 240 + # estimations per second for discrete agent hz = 250 @@ -89,21 +81,14 @@ def a4_ht(t): t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] gt_end = a4_gt(t_end) - print(t_end) - print(gt4 + theta, ht4) - print(gt_end + theta, ht_end, a4_ht(t_end)) - agent.reset() agent.set_g(gt4) agent.set_h(ht4) agent.set_power(p_rec) for i in range(int(t_end)): - print("step {}".format(i)) - print("h: ", a4_ht(i) - agent.get_h()) - print("g: ", a4_gt(i) - agent.get_g()) for s in range(agent.hz): agent.perform_one_step() print("h: ", a4_ht(int(t_end)) - agent.get_h()) - print("g: ", a4_gt(int(t_end)) - agent.get_g()) \ No newline at end of file + print("g: ", a4_gt(int(t_end)) - agent.get_g()) diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py index 106dcbf..e3d0183 100644 --- a/tests/rec_a6_trial.py +++ b/tests/rec_a6_trial.py @@ -1,18 +1,12 @@ import math from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from scipy import optimize import logging -import warnings import numpy as np -# warnings.filterwarnings("error") - if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, @@ -20,7 +14,7 @@ p_exp = 350 p_rec = 100 - t_rec = 240 + # estimations per second for discrete agent hz = 250 @@ -69,6 +63,7 @@ def a6_ht(t): # generalised h(t) for recovery phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch + ht4 = 1 - gamma # estimate an initial guess that assumes no contribution from g initial_guess = 0 @@ -79,9 +74,9 @@ def a6_ht(t): agent.set_h(1.0) agent.set_power(p_rec) - for i in range(int(rt6+1)): + for i in range(int(rt6 + 1)): for s in range(agent.hz): agent.perform_one_step() print("h: ", a6_ht(rt6) - agent.get_h()) - print("g: ", a6_gt(rt6) - agent.get_g()) \ No newline at end of file + print("g: ", a6_gt(rt6) - agent.get_g()) From c6b53222da794a6360419537d06b09cd1a8ff7ac Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 30 Aug 2021 16:59:39 +1000 Subject: [PATCH 22/71] fix low p_ae in example config, finalise first A3R1 trial and add visualisations to all recovery trials --- .../visualiser/three_comp_visualisation.py | 5 ++ tests/rec_a3_r1_trial.py | 68 ++++++++----------- tests/rec_a4_trial.py | 17 +++-- tests/rec_a6_trial.py | 17 +++-- tests/tte_phase_tests.py | 8 ++- 5 files changed, 62 insertions(+), 53 deletions(-) diff --git a/src/threecomphyd/visualiser/three_comp_visualisation.py b/src/threecomphyd/visualiser/three_comp_visualisation.py index 1d740e7..87477ec 100644 --- a/src/threecomphyd/visualiser/three_comp_visualisation.py +++ b/src/threecomphyd/visualiser/three_comp_visualisation.py @@ -76,6 +76,7 @@ def __init__(self, agent: ThreeCompHydAgent, # finish the basic layout self.__set_basic_layout() + self.update_basic_layout(agent) # now the animation components if self._animated: @@ -478,6 +479,10 @@ def update_basic_layout(self, agent): self._r2.set_xdata([ans_left, ans_left - 0.1]) self._r2.set_ydata([gamma_o, gamma_o]) + # update levels + self._h.set_height(1 - self._agent.get_h()) + self._g.set_height(self._agent.height_ans - self._agent.get_g()) + def update_animation_data(self, frame_number): """ For animations and simulations. diff --git a/tests/rec_a3_r1_trial.py b/tests/rec_a3_r1_trial.py index 5c7aa7b..10287a9 100644 --- a/tests/rec_a3_r1_trial.py +++ b/tests/rec_a3_r1_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": # set logging level to highest level @@ -16,7 +17,7 @@ # estimations per second for discrete agent hz = 250 - conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] @@ -27,10 +28,9 @@ m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - t3_exp = 200.58698045085373 t3 = 0 - ht3 = 0.7294083375575103 - gt3 = 0.31236471544758654 + ht3 = 0.5419771693142728 + gt3 = 0.07416467522715564 a_anf = conf[0] a_ans = conf[1] @@ -49,10 +49,7 @@ b = m_ae * m_ans / \ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - c = m_ans * (p_exp * (1 - phi) - m_ae * theta) / \ - (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - - # a new c' for p_rec + # c' for p_rec c_p = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) @@ -60,49 +57,44 @@ r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - # derive the solution from a tte with p_exp - s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t3_exp) - s_c2 = s_c1 * np.exp(r1 * t3_exp) * np.exp(-r2 * t3_exp) * -r1 / r2 + # uses Al dt/dl part of EQ(8) solved for c2 + # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) + # and then substituted in EQ(14) and solved for c1 + s_c1 = ((m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) + c_p / b - gt3) / \ + (np.exp(r2 * t3) * (r1 / r2 - 1)) + + # uses EQ(14) with solution for c1 and solves for c2 + s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - (c_p / b)) / np.exp(r2 * t3) - # use tte solution to determine c1' and c2' - s_c1_p = (gt3 - c_p / b) / ((1 - r1 / r2) * np.exp(-r1 * t3)) - s_c2_p = (r1 * s_c1 * np.exp(r1 * t3_exp) + - r2 * s_c2 * np.exp(r2 * t3_exp) - - r1 * s_c1_p * np.exp(r1 * t3_exp)) / \ - (r2 * np.exp(r2 * t3_exp)) def a3_gt(t): # the general solution for g(t) - return s_c1_p * np.exp(r1 * t) + s_c2_p * np.exp(r2 * t) + c / b + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c_p / b # substitute into EQ(9) for h def a3_ht(t): - k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1_p * r1 + s_c1_p - k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2_p * r2 + s_c2_p - return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - + k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 + k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c_p / b + theta - eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([0]))[0] + # find the point where h(t) == g(t) + eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([1]))[0] - print(a3_gt(0) + theta, a3_ht(0)) - print(gt3 + theta, ht3) - - print(eq_gh) - print(a3_gt(eq_gh) + theta, a3_ht(eq_gh)) - + # check in simulation agent.reset() agent.set_g(gt3) agent.set_h(ht3) + ThreeCompVisualisation(agent) agent.set_power(p_rec) - # for i in range(int(eq_gh)): - # print("step {}".format(i)) - # print("h: ", a3_ht(int(eq_gh)) - agent.get_h()) - # print("g: ", a3_gt(int(eq_gh)) - agent.get_g()) - # for s in range(agent.hz): - # agent.perform_one_step() - # - # print("h: ", a3_ht(int(eq_gh)) - agent.get_h()) - # print("g: ", a3_gt(int(eq_gh)) - agent.get_g()) + for _ in range(int(eq_gh * agent.hz)): + agent.perform_one_step() + + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(eq_gh, + a3_ht(eq_gh) - agent.get_h(), + a3_gt(eq_gh) - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a4_trial.py b/tests/rec_a4_trial.py index 96289cd..bae0fd3 100644 --- a/tests/rec_a4_trial.py +++ b/tests/rec_a4_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": # set logging level to highest level @@ -16,7 +17,7 @@ # estimations per second for discrete agent hz = 250 - conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] @@ -84,11 +85,15 @@ def a4_ht(t): agent.reset() agent.set_g(gt4) agent.set_h(ht4) + ThreeCompVisualisation(agent) agent.set_power(p_rec) - for i in range(int(t_end)): - for s in range(agent.hz): - agent.perform_one_step() + for _ in range(int(t_end * agent.hz)): + agent.perform_one_step() - print("h: ", a4_ht(int(t_end)) - agent.get_h()) - print("g: ", a4_gt(int(t_end)) - agent.get_g()) + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(t_end, + a4_ht(t_end) - agent.get_h(), + a4_gt(t_end) - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py index e3d0183..f7f85c3 100644 --- a/tests/rec_a6_trial.py +++ b/tests/rec_a6_trial.py @@ -6,6 +6,7 @@ import logging import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": # set logging level to highest level @@ -18,7 +19,7 @@ # estimations per second for discrete agent hz = 250 - conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] @@ -72,11 +73,15 @@ def a6_ht(t): agent.reset() agent.set_g(gt6) agent.set_h(1.0) + ThreeCompVisualisation(agent) agent.set_power(p_rec) - for i in range(int(rt6 + 1)): - for s in range(agent.hz): - agent.perform_one_step() + for _ in range(int(rt6 * agent.hz)): + agent.perform_one_step() - print("h: ", a6_ht(rt6) - agent.get_h()) - print("g: ", a6_gt(rt6) - agent.get_g()) + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(rt6, + a6_ht(rt6) - agent.get_h(), + a6_gt(rt6) - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 655f993..ee9a47e 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -103,9 +103,9 @@ def test_one_config(example_conf=None): # just a default value if example_conf is None: - example_conf = [23673.002563739556, 19264.71349151817, 349.17619370868493, - 387.84166460276015, 25.973978416729608, - 0.3003437353302002, 0.317490719771348, 0.6722371555886124] + example_conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], @@ -128,4 +128,6 @@ def test_one_config(example_conf=None): # required precision of discrete to differential agent eps = 0.005 + test_one_config() + the_loop(p=p, hz=hz, eps=eps) From 8ad6f3e08dc0fd34ae0f266d8637085ea3468d7f Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 31 Aug 2021 16:14:56 +1000 Subject: [PATCH 23/71] Add A3R2 --- tests/rec_a3_r1_trial.py | 18 ++++--- tests/rec_a3_r2_trial.py | 112 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 8 deletions(-) create mode 100644 tests/rec_a3_r2_trial.py diff --git a/tests/rec_a3_r1_trial.py b/tests/rec_a3_r1_trial.py index 10287a9..4eb5d27 100644 --- a/tests/rec_a3_r1_trial.py +++ b/tests/rec_a3_r1_trial.py @@ -50,8 +50,8 @@ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) # c' for p_rec - c_p = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ - (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + c = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) @@ -60,27 +60,27 @@ # uses Al dt/dl part of EQ(8) solved for c2 # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) # and then substituted in EQ(14) and solved for c1 - s_c1 = ((m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) + c_p / b - gt3) / \ - (np.exp(r2 * t3) * (r1 / r2 - 1)) + s_c1 = (c / b + (m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) - gt3) / \ + (np.exp(r1 * t3) * (r1 / r2 - 1)) # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - (c_p / b)) / np.exp(r2 * t3) + s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) def a3_gt(t): # the general solution for g(t) - return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c_p / b + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b # substitute into EQ(9) for h def a3_ht(t): k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 - return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c_p / b + theta + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta # find the point where h(t) == g(t) - eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([1]))[0] + eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([0]))[0] # check in simulation agent.reset() @@ -88,6 +88,8 @@ def a3_ht(t): agent.set_h(ht3) ThreeCompVisualisation(agent) agent.set_power(p_rec) + print(a3_ht(eq_gh)) + print(a3_gt(eq_gh)) for _ in range(int(eq_gh * agent.hz)): agent.perform_one_step() diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py new file mode 100644 index 0000000..a4f479e --- /dev/null +++ b/tests/rec_a3_r2_trial.py @@ -0,0 +1,112 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 0 + + # estimations per second for discrete agent + hz = 500 + + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + # End of A3 R1 + t3 = 0 + ht3 = 0.25051681084552113 + gt3 = 0.10159452985149524 + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # EQ 16 and 17 substituted in EQ 8 + a = m_ae / (a_anf * (1 - phi)) + \ + m_anf / (a_ans * (1 - gamma)) + \ + m_anf / (a_anf * (1 - gamma)) + + b = m_ae * m_anf / \ + (a_anf * a_ans * (1 - phi) * (1 - gamma)) + + # c = (p_rec - (m_ae * theta) / (1 - phi)) * m_anf / \ + # (a_anf * a_ans * (1 - gamma)) + c = m_anf * (p_rec * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - gamma)) + + # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) + r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) + + # uses Al dt/dl part of EQ(16) == dl/dt of EQ(14) solved for c2 + # and then substituted in EQ(14) and solved for c1 + s_c1 = (c / b - (m_anf * (gt3 + theta - ht3)) / (a_ans * r2 * (1 - gamma)) - gt3) / \ + (np.exp(r1 * t3) * (r1 / r2 - 1)) + + # uses EQ(14) with solution for c1 and solves for c2 + s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) + + + def a3_gt(t): + # the general solution for g(t) + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b + + + # substitute into EQ(9) for h + def a3_ht(t): + k1 = a_ans * (1 - gamma) / m_anf * s_c1 * r1 + s_c1 + k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + + + # find the point where g(t) == 0 + eq_gh = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([0]))[0] + print(eq_gh) + + # check in simulation + agent.reset() + agent.set_g(gt3) + agent.set_h(ht3) + ThreeCompVisualisation(agent) + agent.set_power(p_rec) + + for i in range(int(eq_gh * agent.hz)): + # if i % agent.hz == 0: + # logging.info("predicted time: {} \n" + # "diff h: {}\n" + # "diff g: {}".format(i, + # a3_ht(i / agent.hz) - agent.get_h(), + # a3_gt(i / agent.hz) - agent.get_g())) + agent.perform_one_step() + + logging.info("predicted time: {} \n" + "diff h: {} - {} = {}\n" + "diff g: {} - {} = {}".format(eq_gh, + a3_ht(eq_gh), + agent.get_h(), + a3_ht(eq_gh) - agent.get_h(), + a3_gt(eq_gh), + agent.get_g(), + a3_gt(eq_gh) - agent.get_g())) + ThreeCompVisualisation(agent) From f79aa71a74cc674e110fd0ae7bd09e70cc61e2b0 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 1 Sep 2021 11:24:47 +1000 Subject: [PATCH 24/71] add educated initial guess to A3R2 --- tests/rec_a3_r2_trial.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py index a4f479e..bb407b0 100644 --- a/tests/rec_a3_r2_trial.py +++ b/tests/rec_a3_r2_trial.py @@ -15,9 +15,9 @@ p_rec = 0 # estimations per second for discrete agent - hz = 500 + hz = 1000 - conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + conf = [5101.24769778409, 10209.27743067988, 252.71702367096787, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] @@ -80,9 +80,13 @@ def a3_ht(t): return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + # use the quickest possible recovery as the initial guess (assumes h=0) + in_c1 = (gt3 + theta) * np.exp(-m_anf * t3 / ((gamma - 1) * a_ans)) + in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf + # find the point where g(t) == 0 - eq_gh = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([0]))[0] - print(eq_gh) + g0 = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] + print(g0) # check in simulation agent.reset() @@ -91,22 +95,16 @@ def a3_ht(t): ThreeCompVisualisation(agent) agent.set_power(p_rec) - for i in range(int(eq_gh * agent.hz)): - # if i % agent.hz == 0: - # logging.info("predicted time: {} \n" - # "diff h: {}\n" - # "diff g: {}".format(i, - # a3_ht(i / agent.hz) - agent.get_h(), - # a3_gt(i / agent.hz) - agent.get_g())) + for _ in range(int(g0 * agent.hz)): agent.perform_one_step() logging.info("predicted time: {} \n" "diff h: {} - {} = {}\n" - "diff g: {} - {} = {}".format(eq_gh, - a3_ht(eq_gh), + "diff g: {} - {} = {}".format(g0, + a3_ht(g0), agent.get_h(), - a3_ht(eq_gh) - agent.get_h(), - a3_gt(eq_gh), + a3_ht(g0) - agent.get_h(), + a3_gt(g0), agent.get_g(), - a3_gt(eq_gh) - agent.get_g())) + a3_gt(g0) - agent.get_g())) ThreeCompVisualisation(agent) From 029cc66ed3fca7e109d8cf5ad88317d74776a06b Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 1 Sep 2021 11:29:37 +1000 Subject: [PATCH 25/71] adjust A3R2 test values --- tests/rec_a3_r2_trial.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py index bb407b0..17384fc 100644 --- a/tests/rec_a3_r2_trial.py +++ b/tests/rec_a3_r2_trial.py @@ -15,9 +15,9 @@ p_rec = 0 # estimations per second for discrete agent - hz = 1000 + hz = 250 - conf = [5101.24769778409, 10209.27743067988, 252.71702367096787, + conf = [10101.24769778409, 80209.27743067988, 252.71702367096787, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] From dbb105d4881e380278ec012f7b188c91f95713f4 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 2 Sep 2021 18:01:23 +1000 Subject: [PATCH 26/71] add phase A1 recovery --- tests/rec_a1_trial.py | 70 ++++++++++++++++++++++++++++++++++++++++ tests/rec_a3_r2_trial.py | 1 - 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 tests/rec_a1_trial.py diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py new file mode 100644 index 0000000..92016f2 --- /dev/null +++ b/tests/rec_a1_trial.py @@ -0,0 +1,70 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 0 + + # estimations per second for discrete agent + hz = 250 + + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + t1 = 0 + ht1 = 0.016964525316181377 + gt1 = 0.0 + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + s_c1 = (ht1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) + + + # full recovery is only possible if p_rec is 0 + def a1_ht(t): + return s_c1 * np.exp(m_ae * t / a_anf * (phi - 1)) - phi * p_rec / m_ae + p_rec / m_ae + + + # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 + t0 = a_anf * (1 + phi) / - m_ae * np.log(0.0001 / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) + + # check in simulation + agent.reset() + agent.set_g(gt1) + agent.set_h(ht1) + ThreeCompVisualisation(agent) + agent.set_power(p_rec) + + for _ in range(int(t0 * agent.hz)): + agent.perform_one_step() + + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(t0, + 0 - agent.get_h(), + 0 - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py index 17384fc..677c671 100644 --- a/tests/rec_a3_r2_trial.py +++ b/tests/rec_a3_r2_trial.py @@ -86,7 +86,6 @@ def a3_ht(t): # find the point where g(t) == 0 g0 = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] - print(g0) # check in simulation agent.reset() From fa61289f9af901d0e4716573e9891b90a9f58411 Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 7 Sep 2021 15:22:25 +1000 Subject: [PATCH 27/71] add A5 recovery trial --- .../simulator/ode_three_comp_hyd_simulator.py | 111 +++++++++--------- tests/configurations.py | 42 +++++++ tests/rec_a5_trial.py | 87 ++++++++++++++ tests/rec_a6_trial.py | 14 ++- tests/tte_phase_tests.py | 12 +- 5 files changed, 201 insertions(+), 65 deletions(-) create mode 100644 tests/configurations.py create mode 100644 tests/rec_a5_trial.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 52f581d..5df52bd 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -14,33 +14,33 @@ class ODEThreeCompHydSimulator: def tte(p_exp: float, conf: list, max_time: int = 5000) -> (float, float, float): # A1 - t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p_exp, conf=conf) + t1, ht1, gt1 = ODEThreeCompHydSimulator.tte_a1(p=p_exp, conf=conf) if t1 == np.inf or t1 > max_time: logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return t1, ht1, gt1 - t2, ht2, gt2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, ht1=ht1, gt1=gt1, p=p_exp, conf=conf) + t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t1=t1, ht1=ht1, gt1=gt1, p=p_exp, conf=conf) # A3 - t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p_exp, conf=conf) + t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t2=t2, ht2=ht2, gt2=gt2, p=p_exp, conf=conf) if t3 == np.inf or t3 > max_time: logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return t2, ht2, gt2 # A4 - t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p_exp, conf=conf) + t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t3=t3, ht3=ht3, gt3=gt3, p=p_exp, conf=conf) if t4 == np.inf or t4 > max_time: logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return t4, ht4, gt4 # A5 - t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p_exp, conf=conf) + t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t4=t4, ht4=ht4, gt4=gt4, p=p_exp, conf=conf) if t5 == np.inf or t5 > max_time: logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return t5, ht5, gt5 # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p_exp, conf=conf) + t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t5=t5, ht5=ht5, gt5=gt5, p=p_exp, conf=conf) if t6 == np.inf or t6 > max_time: logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return t6, ht6, gt6 @@ -48,7 +48,7 @@ def tte(p_exp: float, conf: list, max_time: int = 5000) -> (float, float, float) return t6, ht6, gt6 @staticmethod - def phase_a1(p: float, conf: list) -> (float, float, float): + def tte_a1(p: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] @@ -67,7 +67,7 @@ def phase_a1(p: float, conf: list) -> (float, float, float): return t1, min(theta, 1 - phi), 0 @staticmethod - def phase_a2(t1: float, ht1: float, gt1: float, p: float, conf: list) -> (float, float, float): + def tte_a2(t1: float, ht1: float, gt1: float, p: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] @@ -83,7 +83,7 @@ def phase_a2(t1: float, ht1: float, gt1: float, p: float, conf: list) -> (float, return t2, theta, gt1 @staticmethod - def phase_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, float, float): + def tte_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -142,7 +142,7 @@ def a3_ht(t): return t3, ht3, a3_gt(t3) @staticmethod - def phase_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, float, float): + def tte_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -188,7 +188,7 @@ def a4_ht(t): return t4, ht4, a4_gt(t4) @staticmethod - def phase_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, float, float): + def tte_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -231,13 +231,15 @@ def a5_ht(t): return t5, ht5, a5_gt(t5) @staticmethod - def phase_a6_rec(gt6: float, p_rec: float, conf: list): + def tte_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): """ - recovery from exhaustive exercise. Assumes h reached 1.0 and resets time to 0. - :param gt6: start g(t=0) - :param p_rec: constant recovery intensity - :param conf: hydraulic model configuration - :return: [time at which A6 rec ends, h(rt6), g(rt6)] + Final phase A6 of a time to exhaustion trial. Expects inputs from Phase A5. + :param t5: time at which A5 ended + :param ht5: h(t5) + :param gt5: g(t5) + :param p: constant power output + :param conf: configuration of hydraulic model + :return: [t6: time until h=1, h(t6)=1, g(t6)] """ a_anf = conf[0] a_ans = conf[1] @@ -245,13 +247,9 @@ def phase_a6_rec(gt6: float, p_rec: float, conf: list): m_ans = conf[3] theta = conf[5] gamma = conf[6] - phi = conf[7] - - t6 = 0 - ht6 = 1.0 - # g(t6) = gt6 can be solved for c - s_cg = (gt6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) + # g(t5) = gt5 can be solved for c + s_cg = (gt5 - (1 - theta - gamma)) / np.exp(-m_ans * t5 / ((1 - theta - gamma) * a_ans)) def a6_gt(t): # generalised g(t) for phase A6 @@ -261,44 +259,50 @@ def a6_gt(t): # a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) # g = p / a_anf - ag = (p_rec - m_ae) / a_anf + ag = (p - m_ae) / a_anf - # h(t6) = 1 can be solved for c - s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + ht6 + # h(t5) = ht5 can be solved for c + s_ch = -t5 * ag + ((b * math.exp(-k * t5)) / k) + ht5 def a6_ht(t): - # generalised h(t) for recovery phase A6 + # generalised h(t) for phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - # TODO: needs a check for 1-phi - ht4 = 1 - gamma - # estimate an initial guess that assumes no contribution from g - initial_guess = 0 + ht6 = 1.0 + # estimate an intitial guess that assumes no contribution from g + initial_guess = (ht6 - s_ch) / ag + # find end of phase A6. The time point where h(t6)=1 + t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] + + return t6, ht6, a6_gt(t6) - rt6 = optimize.fsolve(lambda t: a6_ht(t) - ht4, x0=np.array([initial_guess]))[0] + @staticmethod + def rec(p_rec: float, conf: list, start_h: float, start_g: float, max_time: int = 5000) -> (float, float, float): - return rt6, ht4, a6_gt(rt6) + t5, h5, g5 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, p_rec=p_rec, conf=conf) @staticmethod - def phase_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): + def rec_a6(t6: float, h6: float, g6: float, p_rec: float, conf: list): """ - Final phase A6 of a time to exhaustion trial. Expects inputs from Phase A5. - :param t5: time at which A5 ended - :param ht5: h(t5) - :param gt5: g(t5) - :param p: constant power output - :param conf: configuration of hydraulic model - :return: [t6: time until h=1, h(t6)=1, g(t6)] + recovery from exhaustive exercise. + :param t6: time in seconds at which recovery starts + :param h6: depletion state of AnF when recovery starts + :param g6: depletion state of AnS when recovery starts + :param p_rec: constant recovery intensity + :param conf: hydraulic model configuration + :return: [time at which A6 rec ends, h(rt6), g(rt6)] """ + # PHASE A6 a_anf = conf[0] a_ans = conf[1] m_ae = conf[2] m_ans = conf[3] theta = conf[5] gamma = conf[6] + phi = conf[7] - # g(t5) = gt5 can be solved for c - s_cg = (gt5 - (1 - theta - gamma)) / np.exp(-m_ans * t5 / ((1 - theta - gamma) * a_ans)) + # g(t6) = g6 can be solved for c + s_cg = (g6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) def a6_gt(t): # generalised g(t) for phase A6 @@ -308,19 +312,20 @@ def a6_gt(t): # a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) # g = p / a_anf - ag = (p - m_ae) / a_anf + ag = (p_rec - m_ae) / a_anf - # h(t5) = ht5 can be solved for c - s_ch = -t5 * ag + ((b * math.exp(-k * t5)) / k) + ht5 + # h(t6) = 1 can be solved for c + s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + h6 def a6_ht(t): - # generalised h(t) for phase A6 + # generalised h(t) for recovery phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - ht6 = 1.0 - # estimate an intitial guess that assumes no contribution from g - initial_guess = (ht6 - s_ch) / ag - # find end of phase A6. The time point where h(t6)=1 - t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] + # A6 rec ends either at beginning of A4 or A5 + h_target = max(1 - gamma, 1 - phi) - return t6, ht6, a6_gt(t6) + # estimate an initial guess that assumes no contribution from g + initial_guess = 0 + rt6 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] + + return rt6, h_target, a6_gt(rt6) diff --git a/tests/configurations.py b/tests/configurations.py new file mode 100644 index 0000000..f353856 --- /dev/null +++ b/tests/configurations.py @@ -0,0 +1,42 @@ +import logging + +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +# an A configuration +a = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.14892228099402588, # m_anf, theta + 0.3524379644134216, 0.1580228306857272] # gamma, phi +# a B configuration +b = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.14892228099402588, # m_anf, theta + 0.2580228306857272, 0.2580228306857272] # gamma, phi +# a C configuration +c = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.14892228099402588, # m_anf, theta + 0.1580228306857272, 0.3580228306857272] # gamma, phi +# a D configuration +d = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.64892228099402588, # m_anf, theta + 0.1580228306857272, 0.6580228306857272] # gamma, phi + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + hz = 10 + configs = [a, b, c, d] + + for conf in configs: + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a5_trial.py b/tests/rec_a5_trial.py new file mode 100644 index 0000000..1202e78 --- /dev/null +++ b/tests/rec_a5_trial.py @@ -0,0 +1,87 @@ +import math + +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +from tests import configurations + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 100 + + # estimations per second for discrete agent + hz = 250 + + conf = configurations.a + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], + m_ans=conf[3], m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + # PHASE A6 + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + t5 = 0 + h5 = 1.0 - phi + g5 = 0.1 + + # g(t4) = gt4 can be solved for c + s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) + + + def a5_gt(t): + # generalised g(t) for phase A5 + return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + + + # as defined for EQ(21) + k = m_ans / ((1 - theta - gamma) * a_ans) + a = -m_ae / ((1 - phi) * a_anf) + g = p_rec / a_anf + b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) + + # find c that matches h(t4) = ht4 + s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 + + + def a5_ht(t): + return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a + + + h_target = 1 - gamma + + # estimate an initial guess that assumes no contribution from g + initial_guess = 0 + rt5 = optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess]))[0] + + agent.reset() + agent.set_g(g5) + agent.set_h(h5) + ThreeCompVisualisation(agent) + agent.set_power(p_rec) + + for _ in range(int(rt5 * agent.hz)): + agent.perform_one_step() + + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(rt5, + a5_ht(rt5) - agent.get_h(), + a5_gt(rt5) - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py index f7f85c3..db98794 100644 --- a/tests/rec_a6_trial.py +++ b/tests/rec_a6_trial.py @@ -8,6 +8,8 @@ import numpy as np from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from tests import configurations + if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, @@ -19,9 +21,7 @@ # estimations per second for discrete agent hz = 250 - conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, - 363.2970828395908, 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] + conf = configurations.d # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], @@ -39,7 +39,7 @@ t6 = 0 ht6 = 1.0 - gt6 = 0.28293497466056705 + gt6 = 0.1 # g(t6) = gt6 can be solved for c s_cg = (gt6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) @@ -65,10 +65,12 @@ def a6_ht(t): return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - ht4 = 1 - gamma + # A6 rec ends either at beginning of A4 or A5 + h_target = max(1 - gamma, 1 - phi) + # estimate an initial guess that assumes no contribution from g initial_guess = 0 - rt6 = optimize.fsolve(lambda t: a6_ht(t) - ht4, x0=np.array([initial_guess]))[0] + rt6 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] agent.reset() agent.set_g(gt6) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index ee9a47e..62cd106 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -17,38 +17,38 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): max_time = 5000 # A1 - t1, ht1, gt1 = ODEThreeCompHydSimulator.phase_a1(p=p, conf=conf) + t1, ht1, gt1 = ODEThreeCompHydSimulator.tte_a1(p=p, conf=conf) if t1 == np.inf or t1 > max_time: logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return logging.info("A1: {}".format(t1)) - t2, ht2, gt2 = ODEThreeCompHydSimulator.phase_a2(t1=t1, ht1=ht1, gt1=gt1, p=p, conf=conf) + t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t1=t1, ht1=ht1, gt1=gt1, p=p, conf=conf) logging.info("A2: {}".format(t2)) # A3 - t3, ht3, gt3 = ODEThreeCompHydSimulator.phase_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, conf=conf) + t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, conf=conf) if t3 == np.inf or t3 > max_time: logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return logging.info("A3: {}".format(t3)) # A4 - t4, ht4, gt4 = ODEThreeCompHydSimulator.phase_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, conf=conf) + t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, conf=conf) if t4 == np.inf or t4 > max_time: logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return logging.info("A4: {}".format(t4)) # A5 - t5, ht5, gt5 = ODEThreeCompHydSimulator.phase_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, conf=conf) + t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, conf=conf) if t5 == np.inf or t5 > max_time: logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return logging.info("A5: {}".format(t5)) # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.phase_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, conf=conf) + t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, conf=conf) if t6 == np.inf or t6 > max_time: logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return From 881409687cce6e7af4743add8e33a929872987ae Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 7 Sep 2021 17:42:54 +1000 Subject: [PATCH 28/71] split A4 into R1 and R2 --- .../simulator/ode_three_comp_hyd_simulator.py | 121 +++++++++++++++++- tests/{rec_a4_trial.py => rec_a4_r1_trial.py} | 7 - tests/rec_a4_r2_trial.py | 93 ++++++++++++++ tests/rec_a5_trial.py | 6 +- 4 files changed, 210 insertions(+), 17 deletions(-) rename tests/{rec_a4_trial.py => rec_a4_r1_trial.py} (91%) create mode 100644 tests/rec_a4_r2_trial.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 5df52bd..5110206 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -202,7 +202,7 @@ def tte_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, f if phi >= gamma: return t4, ht4, gt4 - # g(t4) = gt4 can be solved for c + # g(t4) = g4 can be solved for c s_cg = (gt4 - (1 - theta - gamma)) * np.exp((m_ans * t4) / ((1 - theta - gamma) * a_ans)) def a5_gt(t): @@ -215,7 +215,7 @@ def a5_gt(t): g = p / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - # find c that matches h(t4) = ht4 + # find c that matches h(t4) = h4 s_ch = (ht4 + b / ((a + k) * np.exp(k) ** t4) + g / a) / np.exp(a) ** t4 def a5_ht(t): @@ -269,7 +269,7 @@ def a6_ht(t): return t * ag - ((b * math.exp(-k * t)) / k) + s_ch ht6 = 1.0 - # estimate an intitial guess that assumes no contribution from g + # estimate an initial guess that assumes no contribution from g initial_guess = (ht6 - s_ch) / ag # find end of phase A6. The time point where h(t6)=1 t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] @@ -281,6 +281,8 @@ def rec(p_rec: float, conf: list, start_h: float, start_g: float, max_time: int t5, h5, g5 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, p_rec=p_rec, conf=conf) + t4, h4, g4 = ODEThreeCompHydSimulator.rec_a5(t5=0, h5=start_h, g5=start_g, p_rec=p_rec, conf=conf) + @staticmethod def rec_a6(t6: float, h6: float, g6: float, p_rec: float, conf: list): """ @@ -292,7 +294,7 @@ def rec_a6(t6: float, h6: float, g6: float, p_rec: float, conf: list): :param conf: hydraulic model configuration :return: [time at which A6 rec ends, h(rt6), g(rt6)] """ - # PHASE A6 + a_anf = conf[0] a_ans = conf[1] m_ae = conf[2] @@ -301,6 +303,14 @@ def rec_a6(t6: float, h6: float, g6: float, p_rec: float, conf: list): gamma = conf[6] phi = conf[7] + # A6 rec ends either at beginning of A4 or A5 + h_target = max(1 - gamma, 1 - phi) + + # check whether phase is applicable or if h is + # already above the end of the phase + if h6 <= h_target: + return t6, h6, g6 + # g(t6) = g6 can be solved for c s_cg = (g6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) @@ -321,11 +331,108 @@ def a6_ht(t): # generalised h(t) for recovery phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - # A6 rec ends either at beginning of A4 or A5 - h_target = max(1 - gamma, 1 - phi) - # estimate an initial guess that assumes no contribution from g initial_guess = 0 rt6 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] return rt6, h_target, a6_gt(rt6) + + @staticmethod + def rec_a5(t5: float, h5: float, g5: float, p_rec: float, conf: list): + """ + recovery from exhaustive exercise. + :param t5: time in seconds at which recovery starts + :param h5: depletion state of AnF when recovery starts + :param g5: depletion state of AnS when recovery starts + :param p_rec: constant recovery intensity + :param conf: hydraulic model configuration + :return: [time at which A6 rec ends, h(rt6), g(rt6)] + """ + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # A5 always ends when h reaches pipe exit of AnS + h_target = 1 - gamma + + # check whether phase is applicable or if h is + # already above the end of the phase + if h5 <= h_target: + return t5, h5, g5 + + # g(t5) = g5 can be solved for c + s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) + + def a5_gt(t): + # generalised g(t) for phase A5 + return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + + # as defined for EQ(21) + k = m_ans / ((1 - theta - gamma) * a_ans) + a = -m_ae / ((1 - phi) * a_anf) + g = p_rec / a_anf + b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) + + # find c that matches h(t5) = h5 + s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 + + def a5_ht(t): + return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a + + # estimate an initial guess that assumes no contribution from g + initial_guess = 0 + rt4 = optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess]))[0] + + return rt4, h_target, a5_gt(rt4) + + @staticmethod + def rec_a4(t4: float, h4: float, g4: float, p_rec: float, conf: list): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # if g is above h (flow from AnS into AnF) + a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + b_gh = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + + # derivative g'(t4) can be calculated manually + dgt4_gh = m_ans * (h4 - g4 - theta) / (a_ans * (1 - theta - gamma)) + + # which then allows to derive c1 and c2 + s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) + s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + gt4 + + # if h is above g (flow from AnF into AnS) + # a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) + # b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) + # + # dgt4_hg = -m_anf * (g4 + theta - h4) / (a_ans * (1 - gamma)) + + def a4_gt(t): + # general solution for g(t) + return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) + + def a4_dgt(t): + # first derivative g'(t) + return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_gh * t) + + def a4_ht(t): + # EQ(9) with constants for g(t) and g'(t) + return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta + + ht_end = 1 - phi + # check if equilibrium in this phase + + t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] + gt_end = a4_gt(t_end) \ No newline at end of file diff --git a/tests/rec_a4_trial.py b/tests/rec_a4_r1_trial.py similarity index 91% rename from tests/rec_a4_trial.py rename to tests/rec_a4_r1_trial.py index bae0fd3..8c7fe63 100644 --- a/tests/rec_a4_trial.py +++ b/tests/rec_a4_r1_trial.py @@ -54,13 +54,6 @@ s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + gt4 - - # if h is above g (flow from AnF into AnS) - # a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) - # b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) - # - # dgt4_hg = -m_anf * (gt4 + theta - ht4) / (a_ans * (1 - gamma)) - def a4_gt(t): # general solution for g(t) return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) diff --git a/tests/rec_a4_r2_trial.py b/tests/rec_a4_r2_trial.py new file mode 100644 index 0000000..0b22771 --- /dev/null +++ b/tests/rec_a4_r2_trial.py @@ -0,0 +1,93 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 100 + + # estimations per second for discrete agent + hz = 250 + + conf = [15101.24769778409, 486209.27743067988, 252.71702367096787, + 363.2970828395908, 43.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + # PHASE A4 + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # derived from tte estimator and + t4 = 0 + h4 = 0.6 + g4 = h4 - theta + + # if h is above g (flow from AnF into AnS) + a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) + b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) + + # derivative g'(t4) can be calculated manually + dgt4_hg = - m_anf * (g4 + theta - h4) / (a_ans * (1 - gamma)) + + # which then allows to derive c1 and c2 + s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t4) + s_c2_gh = (-t4 * b_hg + dgt4_hg) / a_hg - (p_rec - m_ae) / ((a_anf + a_ans) * a_hg) + g4 + + + def a4_gt(t): + # general solution for g(t) + return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) + + + def a4_dgt(t): + # first derivative g'(t) + return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) + + + def a4_ht(t): + # EQ(16) with constants for g(t) and g'(t) + return a_ans * (1 - gamma) / m_anf * a4_dgt(t) + a4_gt(t) + theta + + + ht_end = 1 - phi + # check if equilibrium in this phase + + t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] + gt_end = a4_gt(t_end) + + agent.reset() + agent.set_g(g4) + agent.set_h(h4) + ThreeCompVisualisation(agent) + agent.set_power(p_rec) + + for _ in range(int(t_end * agent.hz)): + agent.perform_one_step() + + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(t_end, + a4_ht(t_end) - agent.get_h(), + a4_gt(t_end) - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a5_trial.py b/tests/rec_a5_trial.py index 1202e78..8f31734 100644 --- a/tests/rec_a5_trial.py +++ b/tests/rec_a5_trial.py @@ -28,7 +28,7 @@ m_ans=conf[3], m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - # PHASE A6 + # PHASE A5 a_anf = conf[0] a_ans = conf[1] m_ae = conf[2] @@ -41,7 +41,7 @@ h5 = 1.0 - phi g5 = 0.1 - # g(t4) = gt4 can be solved for c + # g(t5) = g5 can be solved for c s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) @@ -56,7 +56,7 @@ def a5_gt(t): g = p_rec / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - # find c that matches h(t4) = ht4 + # find c that matches h(t5) = h5 s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 From 5065924db6f5fdc15784deb3b5336bf29386f46d Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 8 Sep 2021 17:43:55 +1000 Subject: [PATCH 29/71] [WIP] add testing routine for configurations --- .../simulator/ode_three_comp_hyd_simulator.py | 134 ++++++++++++++---- tests/configurations.py | 45 +++++- 2 files changed, 153 insertions(+), 26 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 5110206..cef6432 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -277,22 +277,48 @@ def a6_ht(t): return t6, ht6, a6_gt(t6) @staticmethod - def rec(p_rec: float, conf: list, start_h: float, start_g: float, max_time: int = 5000) -> (float, float, float): + def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: float = 5000.0) -> ( + float, float, float): - t5, h5, g5 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, p_rec=p_rec, conf=conf) + # A6 + t5, h5, g5 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t5 == t_rec: + logging.info("RECOVERY END IN A6: t: {} h: {} g: {}".format(t5, h5, g5)) + return t5, h5, g5 - t4, h4, g4 = ODEThreeCompHydSimulator.rec_a5(t5=0, h5=start_h, g5=start_g, p_rec=p_rec, conf=conf) + # A5 + t4, h4, g4 = ODEThreeCompHydSimulator.rec_a5(t5=t5, h5=h5, g5=g5, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t4 == t_rec: + logging.info("RECOVERY END IN A5: t: {} h: {} g: {}".format(t4, h4, g4)) + return t4, h4, g4 + + # A4 R1 + t4r1, h4r1, g4r1 = ODEThreeCompHydSimulator.rec_a4_r1(t4=t4, h4=h4, g4=g4, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t4r1 == t_rec: + logging.info("RECOVERY END IN A4 R1: t: {} h: {} g: {}".format(t4r1, h4r1, g4r1)) + return t4r1, h4r1, g4r1 + + # A4 R2 + t3, h3, g3 = ODEThreeCompHydSimulator.rec_a4_r2(t4=t4r1, h4=h4r1, g4=g4r1, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t3 == t_rec: + logging.info("RECOVERY END IN A4 R2: t: {} h: {} g: {}".format(t3, h3, g3)) + return t3, h3, g3 @staticmethod - def rec_a6(t6: float, h6: float, g6: float, p_rec: float, conf: list): + def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: list): """ recovery from exhaustive exercise. :param t6: time in seconds at which recovery starts :param h6: depletion state of AnF when recovery starts :param g6: depletion state of AnS when recovery starts :param p_rec: constant recovery intensity + :param t_rec: the maximal recovery time :param conf: hydraulic model configuration - :return: [time at which A6 rec ends, h(rt6), g(rt6)] + :return: [rt5 = min(time at which A6 rec ends, t_rec), h(rt5), g(rt5)] """ a_anf = conf[0] @@ -333,20 +359,24 @@ def a6_ht(t): # estimate an initial guess that assumes no contribution from g initial_guess = 0 - rt6 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] + rt5 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] - return rt6, h_target, a6_gt(rt6) + # if targeted recovery time is smaller than end of A6 estimate model state at t_rec + rt5 = min(t_rec, float(rt5)) + + return rt5, a6_ht(rt5), a6_gt(rt5) @staticmethod - def rec_a5(t5: float, h5: float, g5: float, p_rec: float, conf: list): + def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_rec: float, conf: list): """ recovery from exhaustive exercise. :param t5: time in seconds at which recovery starts :param h5: depletion state of AnF when recovery starts :param g5: depletion state of AnS when recovery starts :param p_rec: constant recovery intensity + :param t_rec: the maximal recovery time :param conf: hydraulic model configuration - :return: [time at which A6 rec ends, h(rt6), g(rt6)] + :return: [rt4 = min(time at which A5 rec ends, t_rec), h(rt4), g(rt4)] """ a_anf = conf[0] @@ -388,20 +418,26 @@ def a5_ht(t): initial_guess = 0 rt4 = optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess]))[0] - return rt4, h_target, a5_gt(rt4) + # if targeted recovery time is smaller than end of A6 estimate model state at t_rec + rt4 = min(t_rec, float(rt4)) + + return rt4, a5_ht(rt4), a5_gt(rt4) @staticmethod - def rec_a4(t4: float, h4: float, g4: float, p_rec: float, conf: list): + def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: list): a_anf = conf[0] a_ans = conf[1] m_ae = conf[2] m_ans = conf[3] - m_anf = conf[4] theta = conf[5] gamma = conf[6] phi = conf[7] + # A4 R1 is only applicable if g is above h and h below pipe exit of Ae + if h4 <= 1 - phi or g4 + theta > h4: + return t4, h4, g4 + # if g is above h (flow from AnS into AnF) a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) b_gh = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) @@ -409,15 +445,9 @@ def rec_a4(t4: float, h4: float, g4: float, p_rec: float, conf: list): # derivative g'(t4) can be calculated manually dgt4_gh = m_ans * (h4 - g4 - theta) / (a_ans * (1 - theta - gamma)) - # which then allows to derive c1 and c2 + # ... which then allows to derive c1 and c2 s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) - s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + gt4 - - # if h is above g (flow from AnF into AnS) - # a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) - # b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) - # - # dgt4_hg = -m_anf * (g4 + theta - h4) / (a_ans * (1 - gamma)) + s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + g4 def a4_gt(t): # general solution for g(t) @@ -431,8 +461,64 @@ def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - ht_end = 1 - phi - # check if equilibrium in this phase + # phase ends when g drops below h (or if h <= 1-phi) + tgth = optimize.fsolve(lambda t: theta + a4_gt(t) - a4_ht(t), x0=np.array([0]))[0] + + # in case h rises above g before phase A4 ends, return time at which they are equal + if a4_ht(tgth) >= 1 - phi: + # check if targeted recovery time is before phase end time + tgth = min(float(tgth), t_rec) + return tgth, a4_ht(tgth), a4_gt(tgth) + # otherwise phase ends at h(t) = 1-phi + else: + t_end = optimize.fsolve(lambda t: 1 - phi - a4_ht(t), x0=np.array([0]))[0] + # check if targeted recovery time is before phase end time + t_end = min(float(t_end), t_rec) + return t_end, a4_ht(t_end), a4_gt(t_end) + + @staticmethod + def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: list): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # A4 R2 is only applicable if h is above g (epsilon subtracted) and h below pipe exit of Ae + if h4 <= 1 - phi or g4 + theta < h4 - 0.000001: + logging.info("skipped A4 R2. {} {}".format(h4, g4 + theta)) + return t4, h4, g4 + + # if h is above g simulate flow from AnF into AnS + a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) + b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) + + # derivative g'(t4) can be calculated manually from g4, t4, and h4 + dgt4_hg = - m_anf * (g4 + theta - h4) / (a_ans * (1 - gamma)) + + # which then allows to derive c1 and c2 + s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t4) + s_c2_gh = (-t4 * b_hg + dgt4_hg) / a_hg - (p_rec - m_ae) / ((a_anf + a_ans) * a_hg) + g4 + + def a4_gt(t): + # general solution for g(t) + return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) + + def a4_dgt(t): + # first derivative g'(t) + return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) + + def a4_ht(t): + # EQ(16) with constants for g(t) and g'(t) + return a_ans * (1 - gamma) / m_anf * a4_dgt(t) + a4_gt(t) + theta + + h_target = 1 - phi + t_end = optimize.fsolve(lambda t: h_target - a4_ht(t), x0=np.array([0]))[0] + + # check if targeted recovery time is before phase end time + t_end = min(float(t_end), t_rec) - t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] - gt_end = a4_gt(t_end) \ No newline at end of file + return t_end, a4_ht(t_end), a4_gt(t_end) diff --git a/tests/configurations.py b/tests/configurations.py index f353856..c75a3c1 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -1,6 +1,7 @@ import logging from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation # an A configuration @@ -29,8 +30,16 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - hz = 10 - configs = [a, b, c, d] + hz = 250 + p_exp = 350 + p_rec = 100 + t_rec = 15 + eps = 0.005 + + configs = [#a, + #b, + #c, + d] for conf in configs: # create three component hydraulic agent with example configuration @@ -40,3 +49,35 @@ m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) ThreeCompVisualisation(agent) + + # Start with first time to exhaustion bout + tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) + + # double-check with discrete agent + for _ in range(int(round(tte * hz))): + agent.set_power(p_exp) + agent.perform_one_step() + g_diff = agent.get_g() - g_tte + h_diff = agent.get_h() - h_tte + assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) + + logging.info("TTE END t: {} h: {} g: {}".format(tte, abs(h_diff), abs(g_diff))) + ThreeCompVisualisation(agent) + + # Now recovery + rec, h_rec, g_rec = ODEThreeCompHydSimulator.rec(conf=conf, start_h=h_tte, + start_g=g_tte, p_rec=p_rec, + t_rec=t_rec) + + # double-check with discrete agent + for _ in range(int(round(rec * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g_rec + h_diff = agent.get_h() - h_rec + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + + logging.info("REC END t: {} h: {} g: {}".format(rec, abs(h_diff), abs(g_diff))) + ThreeCompVisualisation(agent) From afca90c257d2882499af9d8e54952262acb8e4a4 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 13 Sep 2021 18:35:55 +1000 Subject: [PATCH 30/71] [WIP] add recovery phase A2, fix attempt for A1 and add detailed rec trail test procedure --- .../simulator/ode_three_comp_hyd_simulator.py | 194 +++++++++++++++++- tests/configurations.py | 16 +- tests/rec_a1_trial.py | 10 +- tests/rec_a2_trial.py | 67 ++++++ tests/rec_a3_r2_trial.py | 18 +- tests/rec_trial_tests.py | 166 +++++++++++---- 6 files changed, 417 insertions(+), 54 deletions(-) create mode 100644 tests/rec_a2_trial.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index cef6432..d3e2044 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -3,6 +3,7 @@ import numpy as np from scipy import optimize +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent class ODEThreeCompHydSimulator: @@ -10,6 +11,9 @@ class ODEThreeCompHydSimulator: Simulates Three Component Hydraulic Model responses using Ordinary Differential Equations """ + # precision epsilon for threshold checks + eps = 0.000001 + @staticmethod def tte(p_exp: float, conf: list, max_time: int = 5000) -> (float, float, float): @@ -308,6 +312,20 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: f logging.info("RECOVERY END IN A4 R2: t: {} h: {} g: {}".format(t3, h3, g3)) return t3, h3, g3 + # A3 R1 + t3r1, h3r1, g3r1 = ODEThreeCompHydSimulator.rec_a3_r1(t3=t3, h3=h3, g3=g3, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t3r1 == t_rec: + logging.info("RECOVERY END IN A3 R1: t: {} h: {} g: {}".format(t3r1, h3r1, g3r1)) + return t3r1, h3r1, g3r1 + + # A3 R2 + t3r2, h3r2, g3r2 = ODEThreeCompHydSimulator.rec_a3_r2(t3=t3r1, h3=h3r1, g3=g3r1, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t3r2 == t_rec: + logging.info("RECOVERY END IN A3 R2: t: {} h: {} g: {}".format(t3r2, h3r2, g3r2)) + return t3r2, h3r2, g3r2 + @staticmethod def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: list): """ @@ -334,7 +352,7 @@ def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: li # check whether phase is applicable or if h is # already above the end of the phase - if h6 <= h_target: + if h6 <= h_target - ODEThreeCompHydSimulator.eps: return t6, h6, g6 # g(t6) = g6 can be solved for c @@ -392,7 +410,7 @@ def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_rec: float, conf: li # check whether phase is applicable or if h is # already above the end of the phase - if h5 <= h_target: + if h5 <= h_target - ODEThreeCompHydSimulator.eps: return t5, h5, g5 # g(t5) = g5 can be solved for c @@ -435,7 +453,7 @@ def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: phi = conf[7] # A4 R1 is only applicable if g is above h and h below pipe exit of Ae - if h4 <= 1 - phi or g4 + theta > h4: + if h4 <= 1 - phi - ODEThreeCompHydSimulator.eps or g4 + theta > h4: return t4, h4, g4 # if g is above h (flow from AnS into AnF) @@ -488,7 +506,8 @@ def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: phi = conf[7] # A4 R2 is only applicable if h is above g (epsilon subtracted) and h below pipe exit of Ae - if h4 <= 1 - phi or g4 + theta < h4 - 0.000001: + if h4 <= 1 - phi - ODEThreeCompHydSimulator.eps or \ + g4 + theta < h4 - ODEThreeCompHydSimulator.eps: logging.info("skipped A4 R2. {} {}".format(h4, g4 + theta)) return t4, h4, g4 @@ -522,3 +541,170 @@ def a4_ht(t): t_end = min(float(t_end), t_rec) return t_end, a4_ht(t_end), a4_gt(t_end) + + @staticmethod + def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: list): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # A3 starts when h <= 1-phi + # R1 is only applicable if g+theta < h + if h3 > 1 - phi + ODEThreeCompHydSimulator.eps or not g3 + theta < h3: + logging.info("skipped A3 R1. {} {}".format(h3, g3 + theta)) + return t3, h3, g3 + + # my simplified form + a = m_ae / (a_anf * (1 - phi)) + \ + m_ans / (a_ans * (1 - theta - gamma)) + \ + m_ans / (a_anf * (1 - theta - gamma)) + + b = m_ae * m_ans / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + # c' for p_rec + c = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) + + # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) + r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) + + # uses Al dt/dl part of EQ(8) solved for c2 + # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) + # and then substituted in EQ(14) and solved for c1 + s_c1 = (c / b + (m_ans * (h3 - g3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) - g3) / \ + (np.exp(r1 * t3) * (r1 / r2 - 1)) + + # uses EQ(14) with solution for c1 and solves for c2 + s_c2 = (g3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) + + def a3_gt(t): + # the general solution for g(t) + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b + + # substitute into EQ(9) for h + def a3_ht(t): + k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 + k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + + # find the point where h(t) == g(t) + eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([0]))[0] + + # check if targeted recovery time is before phase end time + t_end = min(float(eq_gh), t_rec) + + return t_end, a3_ht(t_end), a3_gt(t_end) + + @staticmethod + def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: list): + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # A3 starts when h <= 1-phi + # R2 is only applicable if g+theta >= h + if h3 > 1 - phi + ODEThreeCompHydSimulator.eps or not g3 + theta >= h3: + logging.info("skipped A3 R2. {} {}".format(h3, g3 + theta)) + return t3, h3, g3 + + # EQ 16 and 17 substituted in EQ 8 + a = m_ae / (a_anf * (1 - phi)) + \ + m_anf / (a_ans * (1 - gamma)) + \ + m_anf / (a_anf * (1 - gamma)) + + b = m_ae * m_anf / \ + (a_anf * a_ans * (1 - phi) * (1 - gamma)) + + # c = (p_rec - (m_ae * theta) / (1 - phi)) * m_anf / \ + # (a_anf * a_ans * (1 - gamma)) + c = m_anf * (p_rec * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - gamma)) + + # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) + r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) + + # uses Al dt/dl part of EQ(16) == dl/dt of EQ(14) solved for c2 + # and then substituted in EQ(14) and solved for c1 + s_c1 = (c / b - (m_anf * (g3 + theta - h3)) / (a_ans * r2 * (1 - gamma)) - g3) / \ + (np.exp(r1 * t3) * (r1 / r2 - 1)) + + # uses EQ(14) with solution for c1 and solves for c2 + s_c2 = (g3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) + + def a3_gt(t): + # the general solution for g(t) + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b + + # substitute into EQ(9) for h + def a3_ht(t): + k1 = a_ans * (1 - gamma) / m_anf * s_c1 * r1 + s_c1 + k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + + # use the quickest possible recovery as the initial guess (assumes h=0) + in_c1 = (g3 + theta) * np.exp(-m_anf * t3 / ((gamma - 1) * a_ans)) + in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf + + # find the point where g(t) == 0 + g0 = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] + + # check if targeted recovery time is before phase end time + t_end = min(float(g0), t_rec) + + return t_end, a3_ht(t_end), a3_gt(t_end) + + @staticmethod + def rec_a2(t2: float, h2: float, p_rec: float, t_rec: float, conf: list): + + a_anf = conf[0] + m_ae = conf[2] + phi = conf[7] + + if h2 < 1 - phi: + logging.info("skipped A2. {}".format(h2)) + return t2, h2 + + def a2_ht(t): + return h2 - t * (m_ae - p_rec) / a_anf + + t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + + # check if targeted recovery time is before phase end time + t_end = min(t_end, t_rec) + + return t_end, a2_ht(t_end) + + @staticmethod + def rec_a1(t1: float, h1: float, p_rec: float, t_rec: float, conf: list): + + a_anf = conf[0] + m_ae = conf[2] + phi = conf[7] + + s_c1 = (h1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) + + # full recovery is only possible if p_rec is 0 + def a1_ht(t): + return p_rec * (1 - phi) / m_ae * (1 - np.exp(- m_ae * t / (a_anf * (1 - phi)))) + + # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = epsilon + t_end = a_anf * (1 + phi) / - m_ae * np.log( + 0.0001 / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) + + # check if targeted recovery time is before phase end time + t_end = min(t_end, t_rec) + + return t_end, a1_ht(t_end) diff --git a/tests/configurations.py b/tests/configurations.py index c75a3c1..b1276c1 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -22,7 +22,7 @@ # a D configuration d = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans - 38.27073086773415, 0.64892228099402588, # m_anf, theta + 380.27073086773415, 0.64892228099402588, # m_anf, theta 0.1580228306857272, 0.6580228306857272] # gamma, phi if __name__ == "__main__": @@ -31,15 +31,17 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") hz = 250 - p_exp = 350 + p_exp = 550 p_rec = 100 - t_rec = 15 + t_rec = 150 eps = 0.005 - configs = [#a, - #b, - #c, - d] + configs = [ + # a, + # b, + # c, + d + ] for conf in configs: # create three component hydraulic agent with example configuration diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py index 92016f2..873cde4 100644 --- a/tests/rec_a1_trial.py +++ b/tests/rec_a1_trial.py @@ -28,7 +28,7 @@ m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - t1 = 0 + t1 = 5000 ht1 = 0.016964525316181377 gt1 = 0.0 @@ -41,14 +41,14 @@ gamma = conf[6] phi = conf[7] - s_c1 = (ht1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) - # full recovery is only possible if p_rec is 0 + # use equation (4) from morton with own addition def a1_ht(t): - return s_c1 * np.exp(m_ae * t / a_anf * (phi - 1)) - phi * p_rec / m_ae + p_rec / m_ae - + return p_rec * (1 - phi) / m_ae * (1 - np.exp(- m_ae * t / (a_anf * (1 - phi)))) + # our general solution to the integral + s_c1 = (ht1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 t0 = a_anf * (1 + phi) / - m_ae * np.log(0.0001 / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) diff --git a/tests/rec_a2_trial.py b/tests/rec_a2_trial.py new file mode 100644 index 0000000..cc67c9a --- /dev/null +++ b/tests/rec_a2_trial.py @@ -0,0 +1,67 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 350 + p_rec = 0 + + # estimations per second for discrete agent + hz = 250 + + # a D configuration + conf = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.64892228099402588, # m_anf, theta + 0.1580228306857272, 0.6580228306857272] # gamma, phi + + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, + a_anf=conf[0], a_ans=conf[1], + m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + t2 = 0 + h2 = 0.5 + g2 = 0.0 + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_ans = conf[3] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + + def a2_ht(t): + return h2 - t * (m_ae - p_rec) / a_anf + + t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + + # check in simulation + agent.reset() + agent.set_g(g2) + agent.set_h(h2) + ThreeCompVisualisation(agent) + agent.set_power(p_rec) + + for _ in range(int(t_end * agent.hz)): + agent.perform_one_step() + + logging.info("predicted time: {} \n" + "diff h: {}\n" + "diff g: {}".format(t_end, + (1 - phi) - agent.get_h(), + 0 - agent.get_g())) + ThreeCompVisualisation(agent) diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py index 677c671..65de15c 100644 --- a/tests/rec_a3_r2_trial.py +++ b/tests/rec_a3_r2_trial.py @@ -30,8 +30,8 @@ # End of A3 R1 t3 = 0 - ht3 = 0.25051681084552113 - gt3 = 0.10159452985149524 + ht3 = 0.2 + gt3 = 0.2 a_anf = conf[0] a_ans = conf[1] @@ -94,9 +94,21 @@ def a3_ht(t): ThreeCompVisualisation(agent) agent.set_power(p_rec) - for _ in range(int(g0 * agent.hz)): + for i in range(int(g0 * agent.hz)): agent.perform_one_step() + if i % agent.hz == 0: + test_t = i / agent.hz + logging.info("predicted time: {} \n" + "diff h: {} - {} = {}\n" + "diff g: {} - {} = {}".format(test_t, + a3_ht(test_t), + agent.get_h(), + a3_ht(test_t) - agent.get_h(), + a3_gt(test_t), + agent.get_g(), + a3_gt(test_t) - agent.get_g())) + logging.info("predicted time: {} \n" "diff h: {} - {} = {}\n" "diff g: {} - {} = {}".format(g0, diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 63af57f..3c739b9 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -13,45 +13,127 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): - max_time = 5000 - # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, max_time=max_time) - - # if TTE too long - if t6 > max_time: - return + # Start with first time to exhaustion bout + tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) - agent.reset() - for _ in range(int(round(t6 * hz))): + # double-check with discrete agent + for _ in range(int(round(tte * hz))): agent.set_power(p_exp) agent.perform_one_step() + g_diff = agent.get_g() - g_tte + h_diff = agent.get_h() - h_tte + assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) - g_diff = agent.get_g() - gt6 - h_diff = agent.get_h() - ht6 - - if log_level >= 2: - print("error tte1. h is off by {}".format(h_diff)) - print("error tte1. g is off by {}".format(g_diff)) - - assert abs(g_diff) < eps, "error tte1. g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "error tte1. h is off by {}".format(h_diff) + logging.info("TTE END t: {} h: {} g: {}".format(tte, abs(h_diff), abs(g_diff))) + ThreeCompVisualisation(agent) - # recovery begins here - rt6, rh6, rg6 = ODEThreeCompHydSimulator.phase_a6_rec(gt6=gt6, p_rec=p_rec, conf=conf) - - for _ in range(int(round(rt6 * hz))): + # Now Recovery + # A6 + a6_t, a6_h, a6_g = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=h_tte, g6=g_tte, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round(a6_t * hz))): agent.set_power(p_rec) agent.perform_one_step() - - g_diff = agent.get_g() - rg6 - h_diff = agent.get_h() - rh6 - - if log_level >= 2: - print("error tte1. h is off by {}".format(h_diff)) - print("error tte1. g is off by {}".format(g_diff)) - - assert abs(g_diff) < eps, "error rec1. g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "error rec1. h is off by {}".format(h_diff) + g_diff = agent.get_g() - a6_g + h_diff = agent.get_h() - a6_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A5 + a5_t, a5_h, a5_g = ODEThreeCompHydSimulator.rec_a5(t5=a6_t, h5=a6_h, g5=a6_g, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round((a5_t - a6_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - a5_g + h_diff = agent.get_h() - a5_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A4R1 + a4r1_t, a4r1_h, a4r1_g = ODEThreeCompHydSimulator.rec_a4_r1(t4=a5_t, h4=a5_h, g4=a5_g, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round((a4r1_t - a5_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - a4r1_g + h_diff = agent.get_h() - a4r1_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A4R2 + a4r2_t, a4r2_h, a4r2_g = ODEThreeCompHydSimulator.rec_a4_r2(t4=a4r1_t, h4=a4r1_h, g4=a4r1_g, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round((a4r2_t - a4r1_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - a4r2_g + h_diff = agent.get_h() - a4r2_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A3R1 + a3r1_t, a3r1_h, a3r1_g = ODEThreeCompHydSimulator.rec_a3_r1(t3=a4r2_t, h3=a4r2_h, g3=a4r2_g, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round((a3r1_t - a4r2_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - a3r1_g + h_diff = agent.get_h() - a3r1_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A3R2 + a3r2_t, a3r2_h, a3r2_g = ODEThreeCompHydSimulator.rec_a3_r2(t3=a3r1_t, h3=a3r1_h, g3=a3r1_g, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round((a3r2_t - a3r1_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - a3r2_g + h_diff = agent.get_h() - a3r2_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A2 + a2_t, a2_h = ODEThreeCompHydSimulator.rec_a2(t2=a3r2_t, h2=a3r2_h, + p_rec=p_rec, t_rec=t_rec, conf=conf) + # double-check with discrete agent + for _ in range(int(round((a2_t - a3r2_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - 0 + h_diff = agent.get_h() - a2_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) + + # A1 + a1_t, a1_h = ODEThreeCompHydSimulator.rec_a1(t1=a2_t, h1=a2_h, + p_rec=p_rec, t_rec=t_rec, conf=conf) + + print(a1_t, a1_h) + # double-check with discrete agent + for _ in range(int(round((a1_t - a2_t) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - 0 + h_diff = agent.get_h() - a1_h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + ThreeCompVisualisation(agent) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, @@ -87,12 +169,26 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") p_exp = 350 - p_rec = 100 - t_rec = 240 + p_rec = 0 + t_rec = 5000 # estimations per second for discrete agent hz = 250 # required precision of discrete to differential agent eps = 0.005 - the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps) + # a C configuration + c = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.14892228099402588, # m_anf, theta + 0.1580228306857272, 0.3580228306857272] # gamma, phi + + agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], + m_ans=c[3], m_anf=c[4], the=c[5], + gam=c[6], phi=c[7]) + + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, + hz=hz, eps=eps, conf=c, + agent=agent, log_level=2) + + # the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps) From e2090f21810fdd7727f7e2d0acb5d43806e8fe28 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 13 Sep 2021 22:24:15 +1000 Subject: [PATCH 31/71] [WIP] finalise A1 and complete first entire rec procedure --- .../simulator/ode_three_comp_hyd_simulator.py | 48 ++++++++++++------- tests/configurations.py | 2 +- tests/rec_a1_trial.py | 6 +-- tests/rec_trial_tests.py | 1 - 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index d3e2044..21b9c02 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -285,35 +285,35 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: f float, float, float): # A6 - t5, h5, g5 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, + t6, h6, g6 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, p_rec=p_rec, t_rec=t_rec, conf=conf) - if t5 == t_rec: - logging.info("RECOVERY END IN A6: t: {} h: {} g: {}".format(t5, h5, g5)) - return t5, h5, g5 + if t6 == t_rec: + logging.info("RECOVERY END IN A6: t: {} h: {} g: {}".format(t6, h6, g6)) + return t6, h6, g6 # A5 - t4, h4, g4 = ODEThreeCompHydSimulator.rec_a5(t5=t5, h5=h5, g5=g5, + t5, h5, g5 = ODEThreeCompHydSimulator.rec_a5(t5=t6, h5=h6, g5=g6, p_rec=p_rec, t_rec=t_rec, conf=conf) - if t4 == t_rec: - logging.info("RECOVERY END IN A5: t: {} h: {} g: {}".format(t4, h4, g4)) - return t4, h4, g4 + if t5 == t_rec: + logging.info("RECOVERY END IN A5: t: {} h: {} g: {}".format(t5, h5, g5)) + return t5, h5, g5 # A4 R1 - t4r1, h4r1, g4r1 = ODEThreeCompHydSimulator.rec_a4_r1(t4=t4, h4=h4, g4=g4, + t4r1, h4r1, g4r1 = ODEThreeCompHydSimulator.rec_a4_r1(t4=t5, h4=h5, g4=g5, p_rec=p_rec, t_rec=t_rec, conf=conf) if t4r1 == t_rec: logging.info("RECOVERY END IN A4 R1: t: {} h: {} g: {}".format(t4r1, h4r1, g4r1)) return t4r1, h4r1, g4r1 # A4 R2 - t3, h3, g3 = ODEThreeCompHydSimulator.rec_a4_r2(t4=t4r1, h4=h4r1, g4=g4r1, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t3 == t_rec: - logging.info("RECOVERY END IN A4 R2: t: {} h: {} g: {}".format(t3, h3, g3)) - return t3, h3, g3 + t4r2, h4r2, g4r2 = ODEThreeCompHydSimulator.rec_a4_r2(t4=t4r1, h4=h4r1, g4=g4r1, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t4r2 == t_rec: + logging.info("RECOVERY END IN A4 R2: t: {} h: {} g: {}".format(t4r2, h4r2, g4r2)) + return t4r2, h4r2, g4r2 # A3 R1 - t3r1, h3r1, g3r1 = ODEThreeCompHydSimulator.rec_a3_r1(t3=t3, h3=h3, g3=g3, + t3r1, h3r1, g3r1 = ODEThreeCompHydSimulator.rec_a3_r1(t3=t4r2, h3=h4r2, g3=g4r2, p_rec=p_rec, t_rec=t_rec, conf=conf) if t3r1 == t_rec: logging.info("RECOVERY END IN A3 R1: t: {} h: {} g: {}".format(t3r1, h3r1, g3r1)) @@ -326,6 +326,20 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: f logging.info("RECOVERY END IN A3 R2: t: {} h: {} g: {}".format(t3r2, h3r2, g3r2)) return t3r2, h3r2, g3r2 + # A2 + t2, h2 = ODEThreeCompHydSimulator.rec_a2(t2=t3r2, h2=h3r2, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t2 == t_rec: + logging.info("RECOVERY END IN A2: t: {} h: {}".format(t2, h2)) + return t2, h2, 0 + + # A1 + t1, h1 = ODEThreeCompHydSimulator.rec_a1(t1=t2, h1=h2, + p_rec=p_rec, t_rec=t_rec, conf=conf) + if t1 == t_rec: + logging.info("RECOVERY END IN A2: t: {} h: {}".format(t1, h1)) + return t1, h1, 0 + @staticmethod def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: list): """ @@ -698,11 +712,11 @@ def rec_a1(t1: float, h1: float, p_rec: float, t_rec: float, conf: list): # full recovery is only possible if p_rec is 0 def a1_ht(t): - return p_rec * (1 - phi) / m_ae * (1 - np.exp(- m_ae * t / (a_anf * (1 - phi)))) + return p_rec * (1 - phi) / m_ae * s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = epsilon t_end = a_anf * (1 + phi) / - m_ae * np.log( - 0.0001 / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) + ODEThreeCompHydSimulator.eps / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) # check if targeted recovery time is before phase end time t_end = min(t_end, t_rec) diff --git a/tests/configurations.py b/tests/configurations.py index b1276c1..2b040bc 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -31,7 +31,7 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") hz = 250 - p_exp = 550 + p_exp = 450 p_rec = 100 t_rec = 150 eps = 0.005 diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py index 873cde4..91f2480 100644 --- a/tests/rec_a1_trial.py +++ b/tests/rec_a1_trial.py @@ -41,14 +41,14 @@ gamma = conf[6] phi = conf[7] + # our general solution to the integral + s_c1 = (ht1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) # full recovery is only possible if p_rec is 0 # use equation (4) from morton with own addition def a1_ht(t): - return p_rec * (1 - phi) / m_ae * (1 - np.exp(- m_ae * t / (a_anf * (1 - phi)))) + return p_rec * (1 - phi) / m_ae * s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) - # our general solution to the integral - s_c1 = (ht1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 t0 = a_anf * (1 + phi) / - m_ae * np.log(0.0001 / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 3c739b9..8ba1c11 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -124,7 +124,6 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): a1_t, a1_h = ODEThreeCompHydSimulator.rec_a1(t1=a2_t, h1=a2_h, p_rec=p_rec, t_rec=t_rec, conf=conf) - print(a1_t, a1_h) # double-check with discrete agent for _ in range(int(round((a1_t - a2_t) * hz))): agent.set_power(p_rec) From 5ea8edab86097e4d6a8a8efd8009972d49b9a4cb Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 14 Sep 2021 14:00:47 +1000 Subject: [PATCH 32/71] finish first complete run-through for ODE recovery estimations on four configuration types --- .../simulator/ode_three_comp_hyd_simulator.py | 151 +++++++---------- tests/configurations.py | 9 +- tests/rec_a1_trial.py | 8 +- tests/rec_trial_tests.py | 157 +++++------------- 4 files changed, 113 insertions(+), 212 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 21b9c02..6e766ae 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -3,7 +3,6 @@ import numpy as np from scipy import optimize -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent class ODEThreeCompHydSimulator: @@ -284,61 +283,29 @@ def a6_ht(t): def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: float = 5000.0) -> ( float, float, float): - # A6 - t6, h6, g6 = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=start_h, g6=start_g, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t6 == t_rec: - logging.info("RECOVERY END IN A6: t: {} h: {} g: {}".format(t6, h6, g6)) - return t6, h6, g6 - - # A5 - t5, h5, g5 = ODEThreeCompHydSimulator.rec_a5(t5=t6, h5=h6, g5=g6, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t5 == t_rec: - logging.info("RECOVERY END IN A5: t: {} h: {} g: {}".format(t5, h5, g5)) - return t5, h5, g5 - - # A4 R1 - t4r1, h4r1, g4r1 = ODEThreeCompHydSimulator.rec_a4_r1(t4=t5, h4=h5, g4=g5, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t4r1 == t_rec: - logging.info("RECOVERY END IN A4 R1: t: {} h: {} g: {}".format(t4r1, h4r1, g4r1)) - return t4r1, h4r1, g4r1 - - # A4 R2 - t4r2, h4r2, g4r2 = ODEThreeCompHydSimulator.rec_a4_r2(t4=t4r1, h4=h4r1, g4=g4r1, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t4r2 == t_rec: - logging.info("RECOVERY END IN A4 R2: t: {} h: {} g: {}".format(t4r2, h4r2, g4r2)) - return t4r2, h4r2, g4r2 - - # A3 R1 - t3r1, h3r1, g3r1 = ODEThreeCompHydSimulator.rec_a3_r1(t3=t4r2, h3=h4r2, g3=g4r2, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t3r1 == t_rec: - logging.info("RECOVERY END IN A3 R1: t: {} h: {} g: {}".format(t3r1, h3r1, g3r1)) - return t3r1, h3r1, g3r1 - - # A3 R2 - t3r2, h3r2, g3r2 = ODEThreeCompHydSimulator.rec_a3_r2(t3=t3r1, h3=h3r1, g3=g3r1, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t3r2 == t_rec: - logging.info("RECOVERY END IN A3 R2: t: {} h: {} g: {}".format(t3r2, h3r2, g3r2)) - return t3r2, h3r2, g3r2 - - # A2 - t2, h2 = ODEThreeCompHydSimulator.rec_a2(t2=t3r2, h2=h3r2, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t2 == t_rec: - logging.info("RECOVERY END IN A2: t: {} h: {}".format(t2, h2)) - return t2, h2, 0 - - # A1 - t1, h1 = ODEThreeCompHydSimulator.rec_a1(t1=t2, h1=h2, - p_rec=p_rec, t_rec=t_rec, conf=conf) - if t1 == t_rec: - logging.info("RECOVERY END IN A2: t: {} h: {}".format(t1, h1)) - return t1, h1, 0 + # now iterate through all recovery phases + phases = [ODEThreeCompHydSimulator.rec_a6, + ODEThreeCompHydSimulator.rec_a5, + ODEThreeCompHydSimulator.rec_a4_r1, + ODEThreeCompHydSimulator.rec_a4_r2, + ODEThreeCompHydSimulator.rec_a3_r1, + ODEThreeCompHydSimulator.rec_a3_r2, + ODEThreeCompHydSimulator.rec_a2, + ODEThreeCompHydSimulator.rec_a1] + + # start time from 0 and given start fill level + t = 0 + h, g = start_h, start_g + + # iterate through all phases until end is reached + for phase in phases: + t, h, g = phase(t, h, g, p_rec=p_rec, t_rec=t_rec, conf=conf) + # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) + + # if recovery time is reached return fill levels at that point + if t == t_rec: + logging.info("RECOVERY END IN {}".format(phase)) + return t, h, g @staticmethod def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: list): @@ -366,7 +333,7 @@ def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: li # check whether phase is applicable or if h is # already above the end of the phase - if h6 <= h_target - ODEThreeCompHydSimulator.eps: + if not h6 > h_target: return t6, h6, g6 # g(t6) = g6 can be solved for c @@ -424,7 +391,7 @@ def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_rec: float, conf: li # check whether phase is applicable or if h is # already above the end of the phase - if h5 <= h_target - ODEThreeCompHydSimulator.eps: + if not h5 > h_target: return t5, h5, g5 # g(t5) = g5 can be solved for c @@ -466,8 +433,10 @@ def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: gamma = conf[6] phi = conf[7] - # A4 R1 is only applicable if g is above h and h below pipe exit of Ae - if h4 <= 1 - phi - ODEThreeCompHydSimulator.eps or g4 + theta > h4: + # A4 R1 is only applicable h below pipe exit of Ae ... + # ... and if g is above h (allows error of epsilon) + if not h4 > 1 - phi \ + or not h4 > g4 + theta + ODEThreeCompHydSimulator.eps: return t4, h4, g4 # if g is above h (flow from AnS into AnF) @@ -519,10 +488,10 @@ def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: gamma = conf[6] phi = conf[7] - # A4 R2 is only applicable if h is above g (epsilon subtracted) and h below pipe exit of Ae - if h4 <= 1 - phi - ODEThreeCompHydSimulator.eps or \ - g4 + theta < h4 - ODEThreeCompHydSimulator.eps: - logging.info("skipped A4 R2. {} {}".format(h4, g4 + theta)) + # A4 R2 is only applicable if h below pipe exit of Ae... + # ... and h is above g (error of epsilon tolerated) + if not h4 > 1 - phi or \ + not h4 < g4 + theta + ODEThreeCompHydSimulator.eps: return t4, h4, g4 # if h is above g simulate flow from AnF into AnS @@ -549,7 +518,15 @@ def a4_ht(t): return a_ans * (1 - gamma) / m_anf * a4_dgt(t) + a4_gt(t) + theta h_target = 1 - phi - t_end = optimize.fsolve(lambda t: h_target - a4_ht(t), x0=np.array([0]))[0] + t_end = optimize.fsolve(lambda t: h_target - a4_ht(t), x0=np.array([t4]))[0] + + # A4 also ends if AnS is completely refilled + if a4_gt(t_end) < 0: + # use the quickest possible recovery as the initial guess (assumes h=0) + in_c1 = (g4 + theta) * np.exp(-m_anf * t4 / ((gamma - 1) * a_ans)) + in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf + # find time at which AnS is full + t_end = optimize.fsolve(lambda x: a4_gt(x), x0=np.array([in_t]))[0] # check if targeted recovery time is before phase end time t_end = min(float(t_end), t_rec) @@ -567,10 +544,10 @@ def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: gamma = conf[6] phi = conf[7] - # A3 starts when h <= 1-phi - # R1 is only applicable if g+theta < h - if h3 > 1 - phi + ODEThreeCompHydSimulator.eps or not g3 + theta < h3: - logging.info("skipped A3 R1. {} {}".format(h3, g3 + theta)) + # A3 R1 is only applicable if h is above or at pipe exit of Ae... + # ... and g is above h (error of epsilon tolerated) + if not h3 <= 1 - phi + ODEThreeCompHydSimulator.eps \ + or not h3 > g3 + theta + ODEThreeCompHydSimulator.eps: return t3, h3, g3 # my simplified form @@ -627,10 +604,10 @@ def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: gamma = conf[6] phi = conf[7] - # A3 starts when h <= 1-phi - # R2 is only applicable if g+theta >= h - if h3 > 1 - phi + ODEThreeCompHydSimulator.eps or not g3 + theta >= h3: - logging.info("skipped A3 R2. {} {}".format(h3, g3 + theta)) + # A3 R2 is only applicable if h is above or at pipe exit of Ae... + # ... and h is above g (error of epsilon tolerated) + if not h3 <= 1 - phi + ODEThreeCompHydSimulator.eps \ + or not h3 < g3 + theta + ODEThreeCompHydSimulator.eps: return t3, h3, g3 # EQ 16 and 17 substituted in EQ 8 @@ -681,44 +658,44 @@ def a3_ht(t): return t_end, a3_ht(t_end), a3_gt(t_end) @staticmethod - def rec_a2(t2: float, h2: float, p_rec: float, t_rec: float, conf: list): + def rec_a2(t2: float, h2: float, g2: float, p_rec: float, t_rec: float, conf: list): a_anf = conf[0] m_ae = conf[2] phi = conf[7] - if h2 < 1 - phi: - logging.info("skipped A2. {}".format(h2)) - return t2, h2 + if 1 - phi > h2: + return t2, h2, g2 def a2_ht(t): - return h2 - t * (m_ae - p_rec) / a_anf + return h2 - (t - t2) * (m_ae - p_rec) / a_anf - t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + # the total duration of recovery phase A2 from t2 on + t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + t2 # check if targeted recovery time is before phase end time t_end = min(t_end, t_rec) - return t_end, a2_ht(t_end) + return t_end, a2_ht(t_end), g2 @staticmethod - def rec_a1(t1: float, h1: float, p_rec: float, t_rec: float, conf: list): + def rec_a1(t1: float, h1: float, g1: float, p_rec: float, t_rec: float, conf: list): a_anf = conf[0] m_ae = conf[2] phi = conf[7] - s_c1 = (h1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) + s_c1 = (h1 - p_rec * (1 - phi) / m_ae) / np.exp(- m_ae * t1 / (a_anf * (1 - phi))) # full recovery is only possible if p_rec is 0 def a1_ht(t): - return p_rec * (1 - phi) / m_ae * s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + return s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + p_rec * (1 - phi) / m_ae - # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = epsilon - t_end = a_anf * (1 + phi) / - m_ae * np.log( - ODEThreeCompHydSimulator.eps / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) + # h(t) = approximately 0 (epsilon) + t_end = a_anf * (1 - phi) / - m_ae * np.log( + ODEThreeCompHydSimulator.eps / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) # check if targeted recovery time is before phase end time t_end = min(t_end, t_rec) - return t_end, a1_ht(t_end) + return t_end, a1_ht(t_end), g1 diff --git a/tests/configurations.py b/tests/configurations.py index 2b040bc..9232ffa 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -32,16 +32,11 @@ hz = 250 p_exp = 450 - p_rec = 100 + p_rec = 0 t_rec = 150 eps = 0.005 - configs = [ - # a, - # b, - # c, - d - ] + configs = [a, b, c, d] for conf in configs: # create three component hydraulic agent with example configuration diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py index 91f2480..0286057 100644 --- a/tests/rec_a1_trial.py +++ b/tests/rec_a1_trial.py @@ -42,15 +42,17 @@ phi = conf[7] # our general solution to the integral - s_c1 = (ht1 + p_rec * phi / m_ae - p_rec / m_ae) * np.exp(-m_ae * t1 / (a_anf * (phi - 1))) + s_c1 = (ht1 - p_rec * (1 - phi) / m_ae) / np.exp(- m_ae * t1 / (a_anf * (1 - phi))) + # full recovery is only possible if p_rec is 0 # use equation (4) from morton with own addition def a1_ht(t): - return p_rec * (1 - phi) / m_ae * s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + return s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + p_rec * (1 - phi) / m_ae + # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 - t0 = a_anf * (1 + phi) / - m_ae * np.log(0.0001 / s_c1 + p_rec * (phi + 1) / m_ae * s_c1) + t0 = a_anf * (1 - phi) / - m_ae * np.log(0.000001 / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) # check in simulation agent.reset() diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 8ba1c11..46a4c65 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -14,125 +14,52 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): # Start with first time to exhaustion bout - tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) + t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) # double-check with discrete agent - for _ in range(int(round(tte * hz))): + for _ in range(int(round(t * hz))): agent.set_power(p_exp) agent.perform_one_step() - g_diff = agent.get_g() - g_tte - h_diff = agent.get_h() - h_tte - assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) - - logging.info("TTE END t: {} h: {} g: {}".format(tte, abs(h_diff), abs(g_diff))) - ThreeCompVisualisation(agent) - - # Now Recovery - # A6 - a6_t, a6_h, a6_g = ODEThreeCompHydSimulator.rec_a6(t6=0, h6=h_tte, g6=g_tte, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round(a6_t * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - a6_g - h_diff = agent.get_h() - a6_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) - - # A5 - a5_t, a5_h, a5_g = ODEThreeCompHydSimulator.rec_a5(t5=a6_t, h5=a6_h, g5=a6_g, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((a5_t - a6_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - a5_g - h_diff = agent.get_h() - a5_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) - - # A4R1 - a4r1_t, a4r1_h, a4r1_g = ODEThreeCompHydSimulator.rec_a4_r1(t4=a5_t, h4=a5_h, g4=a5_g, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((a4r1_t - a5_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - a4r1_g - h_diff = agent.get_h() - a4r1_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) - - # A4R2 - a4r2_t, a4r2_h, a4r2_g = ODEThreeCompHydSimulator.rec_a4_r2(t4=a4r1_t, h4=a4r1_h, g4=a4r1_g, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((a4r2_t - a4r1_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - a4r2_g - h_diff = agent.get_h() - a4r2_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) ThreeCompVisualisation(agent) - # A3R1 - a3r1_t, a3r1_h, a3r1_g = ODEThreeCompHydSimulator.rec_a3_r1(t3=a4r2_t, h3=a4r2_h, g3=a4r2_g, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((a3r1_t - a4r2_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - a3r1_g - h_diff = agent.get_h() - a3r1_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) - - # A3R2 - a3r2_t, a3r2_h, a3r2_g = ODEThreeCompHydSimulator.rec_a3_r2(t3=a3r1_t, h3=a3r1_h, g3=a3r1_g, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((a3r2_t - a3r1_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - a3r2_g - h_diff = agent.get_h() - a3r2_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) - - # A2 - a2_t, a2_h = ODEThreeCompHydSimulator.rec_a2(t2=a3r2_t, h2=a3r2_h, - p_rec=p_rec, t_rec=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((a2_t - a3r2_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - 0 - h_diff = agent.get_h() - a2_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) - - # A1 - a1_t, a1_h = ODEThreeCompHydSimulator.rec_a1(t1=a2_t, h1=a2_h, - p_rec=p_rec, t_rec=t_rec, conf=conf) - - # double-check with discrete agent - for _ in range(int(round((a1_t - a2_t) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - 0 - h_diff = agent.get_h() - a1_h - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) + # now iterate through all recovery phases + phases = [ODEThreeCompHydSimulator.rec_a6, + ODEThreeCompHydSimulator.rec_a5, + ODEThreeCompHydSimulator.rec_a4_r1, + ODEThreeCompHydSimulator.rec_a4_r2, + ODEThreeCompHydSimulator.rec_a3_r1, + ODEThreeCompHydSimulator.rec_a3_r2, + ODEThreeCompHydSimulator.rec_a2, + ODEThreeCompHydSimulator.rec_a1] + + # restart time from 0 + t = 0 + + # detailed checks for every phase + for phase in phases: + # save previous time to estimate time difference + t_p = t + + # get estimated time of phase end + t, h, g = phase(t, h, g, p_rec=p_rec, t_rec=t_rec, conf=conf) + logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) + + # double-check with discrete agent + for _ in range(int(round((t - t_p) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + + assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) + assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) + + # display fill-levels + ThreeCompVisualisation(agent) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, @@ -167,7 +94,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 350 + p_exp = 450 p_rec = 0 t_rec = 5000 @@ -180,7 +107,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, c = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 38.27073086773415, 0.14892228099402588, # m_anf, theta - 0.1580228306857272, 0.3580228306857272] # gamma, phi + 0.2580228306857272, 0.2580228306857272] # gamma, phi agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], m_ans=c[3], m_anf=c[4], the=c[5], From 647147f12ed9f14a527a174ba89b77452c7770c9 Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 14 Sep 2021 16:11:18 +1000 Subject: [PATCH 33/71] fix overflow in recovery A1 --- .../simulator/ode_three_comp_hyd_simulator.py | 14 ++++++----- tests/rec_a1_trial.py | 22 +++++++++++++---- tests/rec_trial_tests.py | 24 ++++++++----------- 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 6e766ae..0baefd4 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -307,6 +307,9 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: f logging.info("RECOVERY END IN {}".format(phase)) return t, h, g + # if all phases complete full recovery is reached + return t, h, g + @staticmethod def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: list): """ @@ -685,15 +688,14 @@ def rec_a1(t1: float, h1: float, g1: float, p_rec: float, t_rec: float, conf: li m_ae = conf[2] phi = conf[7] - s_c1 = (h1 - p_rec * (1 - phi) / m_ae) / np.exp(- m_ae * t1 / (a_anf * (1 - phi))) - - # full recovery is only possible if p_rec is 0 def a1_ht(t): - return s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + p_rec * (1 - phi) / m_ae + return (h1 - p_rec * (1 - phi) / m_ae) * \ + np.exp(m_ae * (t1 - t) / (a_anf * (1 - phi))) + \ + p_rec * (1 - phi) / m_ae # h(t) = approximately 0 (epsilon) - t_end = a_anf * (1 - phi) / - m_ae * np.log( - ODEThreeCompHydSimulator.eps / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) + t_end = a_anf * (1 - phi) / - m_ae * (np.log(ODEThreeCompHydSimulator.eps - p_rec * (1 - phi) / m_ae) - ( + np.log(h1 - p_rec * (1 - phi) / m_ae) + m_ae * t1 / (a_anf * (1 - phi)))) # check if targeted recovery time is before phase end time t_end = min(t_end, t_rec) diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py index 0286057..aa94102 100644 --- a/tests/rec_a1_trial.py +++ b/tests/rec_a1_trial.py @@ -41,18 +41,32 @@ gamma = conf[6] phi = conf[7] - # our general solution to the integral - s_c1 = (ht1 - p_rec * (1 - phi) / m_ae) / np.exp(- m_ae * t1 / (a_anf * (1 - phi))) + # our general solution to the integral + # s_c1 = (ht1 - p_rec * (1 - phi) / m_ae) / np.exp(- m_ae * t1 / (a_anf * (1 - phi))) # full recovery is only possible if p_rec is 0 # use equation (4) from morton with own addition + # def a1_ht(t): + # return s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + p_rec * (1 - phi) / m_ae + + # with substituted c1 def a1_ht(t): - return s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + p_rec * (1 - phi) / m_ae + return (ht1 - p_rec * (1 - phi) / m_ae) * \ + np.exp(m_ae * (t1 - t) / (a_anf * (1 - phi))) + \ + p_rec * (1 - phi) / m_ae # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 - t0 = a_anf * (1 - phi) / - m_ae * np.log(0.000001 / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) + # t0 = a_anf * (1 - phi) / - m_ae * np.log(0.000001 / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) + + # with substituted c1 + t0 = a_anf * (1 - phi) / - m_ae * \ + (np.log(0.000001 - p_rec * (1 - phi) / m_ae) - + (np.log(ht1 - p_rec * (1 - phi) / m_ae) + + m_ae * t1 / (a_anf * (1 - phi)) + ) + ) # check in simulation agent.reset() diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 46a4c65..a06fe09 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -16,6 +16,9 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): # Start with first time to exhaustion bout t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) + if t == np.inf: + return + # double-check with discrete agent for _ in range(int(round(t * hz))): agent.set_power(p_exp) @@ -24,7 +27,7 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): h_diff = agent.get_h() - h assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) - ThreeCompVisualisation(agent) + # ThreeCompVisualisation(agent) # now iterate through all recovery phases phases = [ODEThreeCompHydSimulator.rec_a6, @@ -46,7 +49,7 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): # get estimated time of phase end t, h, g = phase(t, h, g, p_rec=p_rec, t_rec=t_rec, conf=conf) - logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) + # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) # double-check with discrete agent for _ in range(int(round((t - t_p) * hz))): @@ -59,7 +62,7 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) # display fill-levels - ThreeCompVisualisation(agent) + # ThreeCompVisualisation(agent) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, @@ -72,22 +75,18 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, udp = MultiObjectiveThreeCompUDP(None, None) example_conf = udp.create_educated_initial_guess() - example_conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, 363.2970828395908, 38.27073086773415, - 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] logging.info(example_conf) # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) - ThreeCompVisualisation(agent) + # ThreeCompVisualisation(agent) rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps, conf=example_conf, agent=agent, log_level=2) - break - if __name__ == "__main__": # set logging level to highest level @@ -104,11 +103,8 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, eps = 0.005 # a C configuration - c = [15101.24769778409, 86209.27743067988, # anf, ans - 252.71702367096787, 363.2970828395908, # m_ae, m_ans - 38.27073086773415, 0.14892228099402588, # m_anf, theta - 0.2580228306857272, 0.2580228306857272] # gamma, phi - + c = [5381.910924589501, 46699.39277057565, 458.9489361010397, 276.5611634824839, 44.10265998056244, + 0.20621747468462412, 0.2349622469556709, 0.84093243148828] agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], m_ans=c[3], m_anf=c[4], the=c[5], gam=c[6], phi=c[7]) @@ -117,4 +113,4 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, hz=hz, eps=eps, conf=c, agent=agent, log_level=2) - # the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps) + the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps) From fa760daa13472a53fdf7c665cde87d19322a29fe Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 15 Sep 2021 16:22:35 +1000 Subject: [PATCH 34/71] introduce new optimizer strategy to find phase transition times --- .../simulator/ode_three_comp_hyd_simulator.py | 397 ++++++++++-------- tests/configurations.py | 39 +- tests/rec_a1_trial.py | 27 +- tests/rec_a3_r1_trial.py | 19 +- tests/rec_a3_r2_trial.py | 16 +- tests/rec_a4_r1_trial.py | 17 +- tests/rec_a4_r2_trial.py | 16 +- tests/rec_a5_trial.py | 17 +- tests/rec_a6_trial.py | 17 +- tests/rec_trial_tests.py | 53 ++- tests/tte_phase_tests.py | 10 +- 11 files changed, 385 insertions(+), 243 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 0baefd4..69364cb 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -1,8 +1,6 @@ -import logging import math import numpy as np -from scipy import optimize class ODEThreeCompHydSimulator: @@ -14,63 +12,54 @@ class ODEThreeCompHydSimulator: eps = 0.000001 @staticmethod - def tte(p_exp: float, conf: list, max_time: int = 5000) -> (float, float, float): - - # A1 - t1, ht1, gt1 = ODEThreeCompHydSimulator.tte_a1(p=p_exp, conf=conf) - if t1 == np.inf or t1 > max_time: - logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) - return t1, ht1, gt1 - - t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t1=t1, ht1=ht1, gt1=gt1, p=p_exp, conf=conf) - - # A3 - t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t2=t2, ht2=ht2, gt2=gt2, p=p_exp, conf=conf) - if t3 == np.inf or t3 > max_time: - logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) - return t2, ht2, gt2 - - # A4 - t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t3=t3, ht3=ht3, gt3=gt3, p=p_exp, conf=conf) - if t4 == np.inf or t4 > max_time: - logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) - return t4, ht4, gt4 - - # A5 - t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t4=t4, ht4=ht4, gt4=gt4, p=p_exp, conf=conf) - if t5 == np.inf or t5 > max_time: - logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) - return t5, ht5, gt5 - - # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t5=t5, ht5=ht5, gt5=gt5, p=p_exp, conf=conf) - if t6 == np.inf or t6 > max_time: - logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) - return t6, ht6, gt6 - - return t6, ht6, gt6 + def tte(conf: list, p_exp: float, t_max: float = 5000) -> (float, float, float): + + phases = [ODEThreeCompHydSimulator.tte_a1, + ODEThreeCompHydSimulator.tte_a2, + ODEThreeCompHydSimulator.tte_a3, + ODEThreeCompHydSimulator.tte_a4, + ODEThreeCompHydSimulator.tte_a4, + ODEThreeCompHydSimulator.tte_a6] + + # start with fully reset agent + t, h, g = 0, 0, 0 + # iterate through all phases until end is reached + for phase in phases: + t, h, g = phase(t, h, g, + p_exp=p_exp, t_max=t_max, conf=conf) + + # if recovery time is reached return fill levels at that point + if t == t_max: + return t, h, g + + # if all phases complete full exhaustion is reached + return t, h, g @staticmethod - def tte_a1(p: float, conf: list) -> (float, float, float): + def tte_a1(t1: float, h1: float, g1: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] theta = conf[5] phi = conf[7] + if t1 > 0 or h1 > 0 or g1 > 0: + raise UserWarning("TTE expects agent to be fully reset (t=0, h=0, g=0)") + + # TODO: t_max not considered yet # check if equilibrium in phase A1 - if ((m_ae * min(theta, 1 - phi)) / (p * (1 - phi))) >= 1: - h = (a_anf * (1 - phi)) / m_ae * (1 - np.exp(-(m_ae * np.inf) / (a_anf * (1 - phi)))) * p / a_anf + if ((m_ae * min(theta, 1 - phi)) / (p_exp * (1 - phi))) >= 1: + h = (a_anf * (1 - phi)) / m_ae * (1 - np.exp(-(m_ae * np.inf) / (a_anf * (1 - phi)))) * p_exp / a_anf return np.inf, h, 0 # end of phase A1 -> the time when h(t) = min(theta,1-phi) - t1 = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * min(theta, 1 - phi) / (p * (1 - phi)))) + t1 = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * min(theta, 1 - phi) / (p_exp * (1 - phi)))) # return t1, h(t1), g(t1) return t1, min(theta, 1 - phi), 0 @staticmethod - def tte_a2(t1: float, ht1: float, gt1: float, p: float, conf: list) -> (float, float, float): + def tte_a2(t2: float, h2: float, g2: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] @@ -79,14 +68,17 @@ def tte_a2(t1: float, ht1: float, gt1: float, p: float, conf: list) -> (float, f # A2 is only applicable if phi is higher than the top of AnS if phi <= (1 - theta): - return t1, ht1, gt1 + return t2, h2, g2 + + # TODO: t_max not considered yet # linear utilization -> no equilibrium possible - t2 = t1 + ((phi - (1 - theta)) * a_anf) / (p - m_ae) + t_end = t2 + ((phi - (1 - theta)) * a_anf) / (p_exp - m_ae) + # return t2, h(t2), g(t2) - return t2, theta, gt1 + return t_end, theta, g2 @staticmethod - def tte_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, float, float): + def tte_a3(t3: float, h3: float, g3: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -98,7 +90,7 @@ def tte_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, f # A3 is only applicable if flow from Ae is not at max if phi > (1 - theta): - return t2, ht2, gt2 + return t3, h3, g3 # taken from Equation 11 by Morton 1986 # a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( @@ -112,7 +104,7 @@ def tte_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, f b = m_ae * m_ans / \ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - c = m_ans * (p * (1 - phi) - m_ae * theta) / \ + c = m_ans * (p_exp * (1 - phi) - m_ae * theta) / \ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c @@ -120,8 +112,8 @@ def tte_a3(t2: float, ht2: float, gt2: float, p: float, conf: list) -> (float, f r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) # c1 and c2 can be determined with g(t2) = g'(t2) = 0 - s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t2) - s_c2 = s_c1 * np.exp(r1 * t2) * np.exp(-r2 * t2) * -r1 / r2 + s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t3) + s_c2 = s_c1 * np.exp(r1 * t3) * np.exp(-r2 * t3) * -r1 / r2 def a3_gt(t): # the general solution for g(t) @@ -134,18 +126,18 @@ def a3_ht(t): return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta # if phi > gamma, then phase A3 transitions into phase A4 before AnS is empty - ht3 = 1 - max(phi, gamma) + h_target = 1 - max(phi, gamma) - # check if equilibrium in this phase - if ht2 <= a3_ht(np.inf) <= ht3: - return np.inf, a3_ht(np.inf), a3_gt(np.inf) - else: - t3 = optimize.fsolve(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] - # use t3 to get g(t3) - return t3, ht3, a3_gt(t3) + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a3_ht(t), + initial_guess=t3, + max_steps=t_max) + + # t3 = optimize.fsolve(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] + # use t3 to get g(t3) + return t_end, a3_ht(t_end), a3_gt(t_end) @staticmethod - def tte_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, float, float): + def tte_a4(t4: float, h4: float, g4: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -157,41 +149,40 @@ def tte_a4(t3: float, ht3: float, gt3: float, p: float, conf: list) -> (float, f # phase A3 is not applicable if gamma is greater or equal to phi if gamma >= phi: - return t3, ht3, gt3 + return t4, h4, g4 # b/a can be simplified as (p-m_ae)/(a_anf + a_ans) a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - b = (p - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + b = (p_exp - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) # derivative g'(t3) can be calculated manually - dgt3 = m_ans * (ht3 - gt3 - theta) / (a_ans * (1 - theta - gamma)) + dgt3 = m_ans * (h4 - g4 - theta) / (a_ans * (1 - theta - gamma)) # which then allows to derive c1 and c2 - s_c1 = ((p - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t3) - s_c2 = (-t3 * b + dgt3) / a - (p - m_ae) / ((a_anf + a_ans) * a) + gt3 + s_c1 = ((p_exp - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t4) + s_c2 = (-t4 * b + dgt3) / a - (p_exp - m_ae) / ((a_anf + a_ans) * a) + g4 def a4_gt(t): # general solution for g(t) - return t * (p - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) + return t * (p_exp - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) def a4_dgt(t): # first derivative g'(t) - return (p - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) + return (p_exp - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - ht4 = 1 - gamma - # check if equilibrium in this phase - if ht3 <= a4_ht(np.inf) <= ht4: - return np.inf, a4_ht(np.inf), a4_gt(np.inf) - else: - t4 = optimize.fsolve(lambda t: ht4 - a4_ht(t), x0=np.array([t3]))[0] - return t4, ht4, a4_gt(t4) + h_target = 1 - gamma + + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a4_ht(t), + initial_guess=t4, + max_steps=t_max) + return t_end, a4_ht(t_end), a4_gt(t_end) @staticmethod - def tte_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, float, float): + def tte_a5(t5: float, h5: float, g5: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -203,10 +194,10 @@ def tte_a5(t4: float, ht4: float, gt4: float, p: float, conf: list) -> (float, f # phase A5 is not applicable if phi is greater or equal to gamma if phi >= gamma: - return t4, ht4, gt4 + return t5, h5, g5 # g(t4) = g4 can be solved for c - s_cg = (gt4 - (1 - theta - gamma)) * np.exp((m_ans * t4) / ((1 - theta - gamma) * a_ans)) + s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) def a5_gt(t): # generalised g(t) for phase A5 @@ -215,34 +206,32 @@ def a5_gt(t): # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) a = -m_ae / ((1 - phi) * a_anf) - g = p / a_anf + g = p_exp / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) # find c that matches h(t4) = h4 - s_ch = (ht4 + b / ((a + k) * np.exp(k) ** t4) + g / a) / np.exp(a) ** t4 + s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 def a5_ht(t): return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a - ht5 = 1 - phi - # check if equilibrium in this phase - if ht4 <= a5_ht(np.inf) <= ht5: - return np.inf, a5_ht(np.inf), a5_gt(np.inf) - else: - # solve for time point where phase A5 ends h(t5) = 1-phi - t5 = optimize.fsolve(lambda t: a5_ht(t) - ht5, x0=np.array([t4]))[0] - return t5, ht5, a5_gt(t5) + h_target = 1 - phi + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a5_ht(t), + initial_guess=t5, + max_steps=t_max) + return t_end, a5_ht(t_end), a5_gt(t_end) @staticmethod - def tte_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, float, float): + def tte_a6(t6: float, h6: float, g6: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): """ Final phase A6 of a time to exhaustion trial. Expects inputs from Phase A5. - :param t5: time at which A5 ended - :param ht5: h(t5) - :param gt5: g(t5) - :param p: constant power output + :param t6: time at which A5 ended + :param h6: h(t5) + :param g6: g(t5) + :param p_exp: constant power output + :param t_max: maximal time limit :param conf: configuration of hydraulic model - :return: [t6: time until h=1, h(t6)=1, g(t6)] + :return: [t_end: time until h=1, h(t_end)=1, g(t_end)] """ a_anf = conf[0] a_ans = conf[1] @@ -252,7 +241,7 @@ def tte_a6(t5: float, ht5: float, gt5: float, p: float, conf: list) -> (float, f gamma = conf[6] # g(t5) = gt5 can be solved for c - s_cg = (gt5 - (1 - theta - gamma)) / np.exp(-m_ans * t5 / ((1 - theta - gamma) * a_ans)) + s_cg = (g6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) def a6_gt(t): # generalised g(t) for phase A6 @@ -262,25 +251,24 @@ def a6_gt(t): # a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) # g = p / a_anf - ag = (p - m_ae) / a_anf + ag = (p_exp - m_ae) / a_anf # h(t5) = ht5 can be solved for c - s_ch = -t5 * ag + ((b * math.exp(-k * t5)) / k) + ht5 + s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + h6 def a6_ht(t): # generalised h(t) for phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - ht6 = 1.0 - # estimate an initial guess that assumes no contribution from g - initial_guess = (ht6 - s_ch) / ag - # find end of phase A6. The time point where h(t6)=1 - t6 = optimize.fsolve(lambda t: ht6 - a6_ht(t), x0=np.array([initial_guess]))[0] - - return t6, ht6, a6_gt(t6) + h_target = 1.0 + # find end of phase A6. The time point where h(t_end)=1 + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a6_ht(t), + initial_guess=t6, + max_steps=t_max) + return t_end, a6_ht(t_end), a6_gt(t_end) @staticmethod - def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: float = 5000.0) -> ( + def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: float = 5000.0) -> ( float, float, float): # now iterate through all recovery phases @@ -299,26 +287,24 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_rec: f # iterate through all phases until end is reached for phase in phases: - t, h, g = phase(t, h, g, p_rec=p_rec, t_rec=t_rec, conf=conf) - # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) + t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point - if t == t_rec: - logging.info("RECOVERY END IN {}".format(phase)) + if t == t_max: return t, h, g # if all phases complete full recovery is reached return t, h, g @staticmethod - def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: list): + def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_max: float, conf: list): """ recovery from exhaustive exercise. :param t6: time in seconds at which recovery starts :param h6: depletion state of AnF when recovery starts :param g6: depletion state of AnS when recovery starts :param p_rec: constant recovery intensity - :param t_rec: the maximal recovery time + :param t_max: the maximal recovery time :param conf: hydraulic model configuration :return: [rt5 = min(time at which A6 rec ends, t_rec), h(rt5), g(rt5)] """ @@ -331,6 +317,10 @@ def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_rec: float, conf: li gamma = conf[6] phi = conf[7] + # no recovery possible + if p_rec >= m_ae: + return np.inf, 1.0, 1.0 + # A6 rec ends either at beginning of A4 or A5 h_target = max(1 - gamma, 1 - phi) @@ -360,23 +350,21 @@ def a6_ht(t): return t * ag - ((b * math.exp(-k * t)) / k) + s_ch # estimate an initial guess that assumes no contribution from g - initial_guess = 0 - rt5 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] - - # if targeted recovery time is smaller than end of A6 estimate model state at t_rec - rt5 = min(t_rec, float(rt5)) - - return rt5, a6_ht(rt5), a6_gt(rt5) + initial_guess = t6 + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a6_ht(t) - h_target, + initial_guess=initial_guess, + max_steps=t_max) + return t_end, a6_ht(t_end), a6_gt(t_end) @staticmethod - def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_rec: float, conf: list): + def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_max: float, conf: list): """ recovery from exhaustive exercise. :param t5: time in seconds at which recovery starts :param h5: depletion state of AnF when recovery starts :param g5: depletion state of AnS when recovery starts :param p_rec: constant recovery intensity - :param t_rec: the maximal recovery time + :param t_max: the maximal recovery time :param conf: hydraulic model configuration :return: [rt4 = min(time at which A5 rec ends, t_rec), h(rt4), g(rt4)] """ @@ -416,17 +404,16 @@ def a5_gt(t): def a5_ht(t): return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a - # estimate an initial guess that assumes no contribution from g - initial_guess = 0 - rt4 = optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess]))[0] - - # if targeted recovery time is smaller than end of A6 estimate model state at t_rec - rt4 = min(t_rec, float(rt4)) - - return rt4, a5_ht(rt4), a5_gt(rt4) + # find the time at which the phase stops + initial_guess = t5 + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a5_ht(t) - h_target, + initial_guess=initial_guess, + max_steps=t_max) + # return with fill levels at that time + return t_end, a5_ht(t_end), a5_gt(t_end) @staticmethod - def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: list): + def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -465,23 +452,25 @@ def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - # phase ends when g drops below h (or if h <= 1-phi) - tgth = optimize.fsolve(lambda t: theta + a4_gt(t) - a4_ht(t), x0=np.array([0]))[0] + initial_guess = t4 - # in case h rises above g before phase A4 ends, return time at which they are equal - if a4_ht(tgth) >= 1 - phi: - # check if targeted recovery time is before phase end time - tgth = min(float(tgth), t_rec) - return tgth, a4_ht(tgth), a4_gt(tgth) - # otherwise phase ends at h(t) = 1-phi - else: - t_end = optimize.fsolve(lambda t: 1 - phi - a4_ht(t), x0=np.array([0]))[0] - # check if targeted recovery time is before phase end time - t_end = min(float(t_end), t_rec) - return t_end, a4_ht(t_end), a4_gt(t_end) + # phase ends when g drops below h... + tgth = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (theta + a4_gt(t)), + initial_guess=initial_guess, + max_steps=t_max) + + # ...or if h reaches 1-phi + tphi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (1 - phi), + initial_guess=initial_guess, + max_steps=t_max) + + # choose minimal time at which this phase ends + t_end = min(tphi, tgth) + + return t_end, a4_ht(t_end), a4_gt(t_end) @staticmethod - def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_rec: float, conf: list): + def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -521,23 +510,24 @@ def a4_ht(t): return a_ans * (1 - gamma) / m_anf * a4_dgt(t) + a4_gt(t) + theta h_target = 1 - phi - t_end = optimize.fsolve(lambda t: h_target - a4_ht(t), x0=np.array([t4]))[0] - # A4 also ends if AnS is completely refilled - if a4_gt(t_end) < 0: - # use the quickest possible recovery as the initial guess (assumes h=0) - in_c1 = (g4 + theta) * np.exp(-m_anf * t4 / ((gamma - 1) * a_ans)) - in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf - # find time at which AnS is full - t_end = optimize.fsolve(lambda x: a4_gt(x), x0=np.array([in_t]))[0] + # A4 ends if AnS is completely refilled + t_fill = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_gt(t), + initial_guess=t4, + max_steps=t_max) - # check if targeted recovery time is before phase end time - t_end = min(float(t_end), t_rec) + # A4 also ends by surpassing 1 - phi + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - h_target, + initial_guess=t4, + max_steps=t_max) + + # choose minimal time at which phase ends + t_end = min(t_fill, t_phi) return t_end, a4_ht(t_end), a4_gt(t_end) @staticmethod - def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: list): + def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -588,16 +578,16 @@ def a3_ht(t): k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + # phase A3 R1 only ends if h(t) rises above g(t) # find the point where h(t) == g(t) - eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([0]))[0] - - # check if targeted recovery time is before phase end time - t_end = min(float(eq_gh), t_rec) - + # As h(t3) is assumed to be below g(t3) and AnS is limited, this point must be reached + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_ht(t) - (a3_gt(t) + theta), + initial_guess=t3, + max_steps=t_max) return t_end, a3_ht(t_end), a3_gt(t_end) @staticmethod - def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: list): + def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -621,12 +611,10 @@ def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_rec: float, conf: b = m_ae * m_anf / \ (a_anf * a_ans * (1 - phi) * (1 - gamma)) - # c = (p_rec - (m_ae * theta) / (1 - phi)) * m_anf / \ - # (a_anf * a_ans * (1 - gamma)) c = m_anf * (p_rec * (1 - phi) - m_ae * theta) / \ (a_anf * a_ans * (1 - phi) * (1 - gamma)) - # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + # solutions for l''(t) + a*l'(t) + b*l(t) = c r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) @@ -648,26 +636,25 @@ def a3_ht(t): k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - # use the quickest possible recovery as the initial guess (assumes h=0) - in_c1 = (g3 + theta) * np.exp(-m_anf * t3 / ((gamma - 1) * a_ans)) - in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf - - # find the point where g(t) == 0 - g0 = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] - - # check if targeted recovery time is before phase end time - t_end = min(float(g0), t_rec) - - return t_end, a3_ht(t_end), a3_gt(t_end) + # Recovery phase A3R2 only ends if AnS is refilled + if a3_gt(t_max) > 0: + return t_max, a3_ht(t_max), a3_gt(t_max) + else: + # find the point where g(t) == 0 + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_gt(t), + initial_guess=t3, + max_steps=t_max) + return t_end, a3_ht(t_end), a3_gt(t_end) @staticmethod - def rec_a2(t2: float, h2: float, g2: float, p_rec: float, t_rec: float, conf: list): + def rec_a2(t2: float, h2: float, g2: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] m_ae = conf[2] phi = conf[7] - if 1 - phi > h2: + # Recovery phase A2 is only applicable if h is below pipe exit of Ae and AnS is full + if 1 - phi > h2 or g2 > ODEThreeCompHydSimulator.eps: return t2, h2, g2 def a2_ht(t): @@ -677,12 +664,12 @@ def a2_ht(t): t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + t2 # check if targeted recovery time is before phase end time - t_end = min(t_end, t_rec) + t_end = min(t_end, t_max) return t_end, a2_ht(t_end), g2 @staticmethod - def rec_a1(t1: float, h1: float, g1: float, p_rec: float, t_rec: float, conf: list): + def rec_a1(t1: float, h1: float, g1: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] m_ae = conf[2] @@ -693,11 +680,57 @@ def a1_ht(t): np.exp(m_ae * (t1 - t) / (a_anf * (1 - phi))) + \ p_rec * (1 - phi) / m_ae - # h(t) = approximately 0 (epsilon) - t_end = a_anf * (1 - phi) / - m_ae * (np.log(ODEThreeCompHydSimulator.eps - p_rec * (1 - phi) / m_ae) - ( - np.log(h1 - p_rec * (1 - phi) / m_ae) + m_ae * t1 / (a_anf * (1 - phi)))) + # full recovery can't be reached as log(inf) only approximates 0 + # a1_ht(max rec time) is the most recovery possible + target_h = a1_ht(t_max) + + return t_max, target_h, g1 + + # # return min(h) if even after the maximal recovery time h>epsilon + # if target_h > ODEThreeCompHydSimulator.eps: + # return t_rec, target_h, g1 + # + # # otherwise return time when h(t) reaches approximately 0 (epsilon) + # t_end = a_anf * (1 - phi) / - m_ae * ( + # np.log(target_h - p_rec * (1 - phi) / m_ae) - ( + # np.log(h1 - p_rec * (1 - phi) / m_ae) + m_ae * t1 / (a_anf * (1 - phi)) + # ) + # ) + # + # return t_end, a1_ht(t_end), g1 - # check if targeted recovery time is before phase end time - t_end = min(t_end, t_rec) + @staticmethod + def optimize(func, initial_guess, max_steps): + """ + This optimiser finds t with func(t) == 0 by increasing t at increasing precision + until the sing switches (func(t) becomes negative). It is assumed that func(t0 = initial guess) is positive. + If it becomes clear that t>=max_steps, max_steps is returned. + :return: found t + """ + + # start precision + step_size = 1000.0 + + # check if initial guess conforms to underlying optimizer assumption + t = initial_guess + if func(t) < 0: + raise UserWarning("initial guess for func is not positive") + + # while maximal precision is not reached + while step_size > 0.000001: + t_p = t + # increase t until function turns negative + while t <= max_steps and func(t) >= 0: + t_p = t + t += step_size + + # back to the one before values turned negative + t = t_p + # if t is above defined max steps we know our target is beyond the time limit + if t >= max_steps: + return max_steps + + # increase precision + step_size = step_size / 10.0 - return t_end, a1_ht(t_end), g1 + return t diff --git a/tests/configurations.py b/tests/configurations.py index 9232ffa..c8468bf 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -1,3 +1,4 @@ +import itertools import logging from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent @@ -30,22 +31,34 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - hz = 250 - p_exp = 450 - p_rec = 0 - t_rec = 150 - eps = 0.005 + hz = 250 # delta t + eps = 0.001 # required precision + # setting combinations + p_exps = [260, 300, 421, 681] + rec_times = [10, 180, 240, 1800, 3600] + p_recs = [0, 13, 57, 130, 247] configs = [a, b, c, d] - for conf in configs: + combs = list(itertools.product(p_exps, rec_times, p_recs, configs)) + + # iterate through all possible setting combinations + for i, comb in enumerate(combs): + logging.info("{}/{} Comb {}".format(i, len(combs), comb)) + + # get settings from combination + p_exp = comb[0] + rec_time = comb[1] + p_rec = comb[2] + conf = comb[3] + # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], m_ans=conf[3], m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - ThreeCompVisualisation(agent) + # ThreeCompVisualisation(agent) # Start with first time to exhaustion bout tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) @@ -59,13 +72,13 @@ assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) - logging.info("TTE END t: {} h: {} g: {}".format(tte, abs(h_diff), abs(g_diff))) - ThreeCompVisualisation(agent) + # logging.info("TTE END t: {} h: {} g: {}".format(tte, abs(h_diff), abs(g_diff))) + # ThreeCompVisualisation(agent) # Now recovery rec, h_rec, g_rec = ODEThreeCompHydSimulator.rec(conf=conf, start_h=h_tte, start_g=g_tte, p_rec=p_rec, - t_rec=t_rec) + t_max=rec_time) # double-check with discrete agent for _ in range(int(round(rec * hz))): @@ -76,5 +89,7 @@ assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - logging.info("REC END t: {} h: {} g: {}".format(rec, abs(h_diff), abs(g_diff))) - ThreeCompVisualisation(agent) + # logging.info("REC END t: {} h: {} g: {}".format(rec, abs(h_diff), abs(g_diff))) + # ThreeCompVisualisation(agent) + + logging.info("PASSED") diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py index aa94102..842b7e0 100644 --- a/tests/rec_a1_trial.py +++ b/tests/rec_a1_trial.py @@ -12,7 +12,8 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") p_exp = 350 - p_rec = 0 + p_rec = 5 + t_rec = 10000 # max time # estimations per second for discrete agent hz = 250 @@ -57,16 +58,24 @@ def a1_ht(t): p_rec * (1 - phi) / m_ae + # full recovery is never reached as log(inf) only approximates 0 + # a1_ht(max rec time) results in target_h + target_h = a1_ht(t_rec) + # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 # t0 = a_anf * (1 - phi) / - m_ae * np.log(0.000001 / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) - # with substituted c1 - t0 = a_anf * (1 - phi) / - m_ae * \ - (np.log(0.000001 - p_rec * (1 - phi) / m_ae) - - (np.log(ht1 - p_rec * (1 - phi) / m_ae) + - m_ae * t1 / (a_anf * (1 - phi)) - ) - ) + # return min(h) if even after the maximal recovery time h>epsilon + if target_h > 0.00001: + t0 = t_rec + else: + # with substituted c1 + t0 = a_anf * (1 - phi) / - m_ae * \ + (np.log(0.00001 - p_rec * (1 - phi) / m_ae) - + (np.log(ht1 - p_rec * (1 - phi) / m_ae) + + m_ae * t1 / (a_anf * (1 - phi)) + ) + ) # check in simulation agent.reset() @@ -81,6 +90,6 @@ def a1_ht(t): logging.info("predicted time: {} \n" "diff h: {}\n" "diff g: {}".format(t0, - 0 - agent.get_h(), + target_h - agent.get_h(), 0 - agent.get_g())) ThreeCompVisualisation(agent) diff --git a/tests/rec_a3_r1_trial.py b/tests/rec_a3_r1_trial.py index 4eb5d27..0b71ea3 100644 --- a/tests/rec_a3_r1_trial.py +++ b/tests/rec_a3_r1_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": @@ -13,6 +14,7 @@ p_exp = 350 p_rec = 100 + t_max = 5000 # max rec time # estimations per second for discrete agent hz = 250 @@ -80,7 +82,20 @@ def a3_ht(t): # find the point where h(t) == g(t) - eq_gh = optimize.fsolve(lambda t: (a3_gt(t) + theta) - a3_ht(t), x0=np.array([0]))[0] + import time + + t0 = time.process_time_ns() + eq_gh = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_ht(t) - (a3_gt(t) + theta), + initial_guess=t3, + max_steps=t_max) + t1 = time.process_time_ns() + print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) + + t0 = time.process_time_ns() + optimize.fsolve(lambda t: a3_ht(t) - (a3_gt(t) + theta), + x0=np.array([t3])) + t1 = time.process_time_ns() + print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) # check in simulation agent.reset() @@ -88,8 +103,6 @@ def a3_ht(t): agent.set_h(ht3) ThreeCompVisualisation(agent) agent.set_power(p_rec) - print(a3_ht(eq_gh)) - print(a3_gt(eq_gh)) for _ in range(int(eq_gh * agent.hz)): agent.perform_one_step() diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py index 65de15c..e7abba8 100644 --- a/tests/rec_a3_r2_trial.py +++ b/tests/rec_a3_r2_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": @@ -13,6 +14,7 @@ p_exp = 350 p_rec = 0 + t_max = 5000 # estimations per second for discrete agent hz = 250 @@ -85,7 +87,19 @@ def a3_ht(t): in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf # find the point where g(t) == 0 - g0 = optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] + import time + + t0 = time.process_time_ns() + g0 = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_gt(t), + initial_guess=t3, + max_steps=t_max) + t1 = time.process_time_ns() + print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) + + t0 = time.process_time_ns() + optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] + t1 = time.process_time_ns() + print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) # check in simulation agent.reset() diff --git a/tests/rec_a4_r1_trial.py b/tests/rec_a4_r1_trial.py index 8c7fe63..48019d8 100644 --- a/tests/rec_a4_r1_trial.py +++ b/tests/rec_a4_r1_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": @@ -13,6 +14,7 @@ p_exp = 350 p_rec = 100 + t_max = 5000 # estimations per second for discrete agent hz = 250 @@ -54,6 +56,7 @@ s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + gt4 + def a4_gt(t): # general solution for g(t) return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) @@ -70,9 +73,21 @@ def a4_ht(t): ht_end = 1 - phi + # check if equilibrium in this phase + import time + t0 = time.process_time_ns() + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - ht_end, + initial_guess=0, + max_steps=t_max) + t1 = time.process_time_ns() + print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) + + t0 = time.process_time_ns() + optimize.fsolve(lambda t: a4_ht(t) - ht_end, x0=np.array([0])) + t1 = time.process_time_ns() + print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] gt_end = a4_gt(t_end) agent.reset() diff --git a/tests/rec_a4_r2_trial.py b/tests/rec_a4_r2_trial.py index 0b22771..b2a492a 100644 --- a/tests/rec_a4_r2_trial.py +++ b/tests/rec_a4_r2_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": @@ -13,6 +14,7 @@ p_exp = 350 p_rec = 100 + t_max = 5000 # estimations per second for discrete agent hz = 250 @@ -73,7 +75,19 @@ def a4_ht(t): ht_end = 1 - phi # check if equilibrium in this phase - t_end = optimize.fsolve(lambda t: ht_end - a4_ht(t), x0=np.array([0]))[0] + import time + + t0 = time.process_time_ns() + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - ht_end, + initial_guess=0, + max_steps=t_max) + t1 = time.process_time_ns() + print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) + + t0 = time.process_time_ns() + optimize.fsolve(lambda t: a4_ht(t) - ht_end, x0=np.array([0])) + t1 = time.process_time_ns() + print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) gt_end = a4_gt(t_end) agent.reset() diff --git a/tests/rec_a5_trial.py b/tests/rec_a5_trial.py index 8f31734..6d0df11 100644 --- a/tests/rec_a5_trial.py +++ b/tests/rec_a5_trial.py @@ -6,6 +6,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from tests import configurations @@ -17,6 +18,7 @@ p_exp = 350 p_rec = 100 + t_max = 5000 # estimations per second for discrete agent hz = 250 @@ -68,7 +70,20 @@ def a5_ht(t): # estimate an initial guess that assumes no contribution from g initial_guess = 0 - rt5 = optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess]))[0] + + import time + + t0 = time.process_time_ns() + rt5 = ODEThreeCompHydSimulator.optimize(func=lambda t: a5_ht(t) - h_target, + initial_guess=initial_guess, + max_steps=t_max) + t1 = time.process_time_ns() + print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) + + t0 = time.process_time_ns() + optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess])) + t1 = time.process_time_ns() + print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) agent.reset() agent.set_g(g5) diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py index db98794..305e8c8 100644 --- a/tests/rec_a6_trial.py +++ b/tests/rec_a6_trial.py @@ -6,6 +6,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from tests import configurations @@ -17,6 +18,7 @@ p_exp = 350 p_rec = 100 + t_max = 5000 # estimations per second for discrete agent hz = 250 @@ -70,7 +72,20 @@ def a6_ht(t): # estimate an initial guess that assumes no contribution from g initial_guess = 0 - rt6 = optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess]))[0] + + import time + + t0 = time.process_time_ns() + rt6 = ODEThreeCompHydSimulator.optimize(func=lambda t: a6_ht(t) - h_target, + initial_guess=initial_guess, + max_steps=t_max) + t1 = time.process_time_ns() + print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) + + t0 = time.process_time_ns() + optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess])) + t1 = time.process_time_ns() + print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) agent.reset() agent.set_g(gt6) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index a06fe09..c016741 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -4,15 +4,11 @@ from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator import logging -import warnings import numpy as np -# warnings.filterwarnings("error") - - -def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): +def rec_trial_procedure(p_exp, p_rec, t_max, hz, eps, conf, agent, log_level=0): # Start with first time to exhaustion bout t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) @@ -48,7 +44,7 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): t_p = t # get estimated time of phase end - t, h, g = phase(t, h, g, p_rec=p_rec, t_rec=t_rec, conf=conf) + t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) # double-check with discrete agent @@ -58,15 +54,19 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, hz, eps, conf, agent, log_level=0): g_diff = agent.get_g() - g h_diff = agent.get_h() - h + # ThreeCompVisualisation(agent) + assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) - # display fill-levels - # ThreeCompVisualisation(agent) + if t == t_max: + logging.info("Max recovery reached in {}".format(phase)) + # ThreeCompVisualisation(agent) + return def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, - t_rec: int = 240, hz: int = 250, eps: float = 0.001): + t_max: float = 240, hz: int = 250, eps: float = 0.001): """ creates random agents and tests the discretised against the differential one """ @@ -81,9 +81,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) - # ThreeCompVisualisation(agent) - - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_max=t_max, hz=hz, eps=eps, conf=example_conf, agent=agent, log_level=2) @@ -93,24 +91,25 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 450 + p_exp = 260 + t_max = 180 p_rec = 0 - t_rec = 5000 # estimations per second for discrete agent hz = 250 # required precision of discrete to differential agent - eps = 0.005 - - # a C configuration - c = [5381.910924589501, 46699.39277057565, 458.9489361010397, 276.5611634824839, 44.10265998056244, - 0.20621747468462412, 0.2349622469556709, 0.84093243148828] - agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], - m_ans=c[3], m_anf=c[4], the=c[5], - gam=c[6], phi=c[7]) - - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, - hz=hz, eps=eps, conf=c, - agent=agent, log_level=2) + eps = 0.0001 + + # a configuration + # c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, + # 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] + # agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], + # m_ans=c[3], m_anf=c[4], the=c[5], + # gam=c[6], phi=c[7]) + # ThreeCompVisualisation(agent) + # + # rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_max=t_max, + # hz=hz, eps=eps, conf=c, + # agent=agent, log_level=2) - the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, hz=hz, eps=eps) + the_loop(p_exp=p_exp, p_rec=p_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 62cd106..1d5b106 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -23,32 +23,32 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): return logging.info("A1: {}".format(t1)) - t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t1=t1, ht1=ht1, gt1=gt1, p=p, conf=conf) + t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t2=t1, h2=ht1, g2=gt1, p_exp=p, conf=conf) logging.info("A2: {}".format(t2)) # A3 - t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t2=t2, ht2=ht2, gt2=gt2, p=p, conf=conf) + t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t3=t2, h3=ht2, g3=gt2, p_exp=p, conf=conf) if t3 == np.inf or t3 > max_time: logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return logging.info("A3: {}".format(t3)) # A4 - t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t3=t3, ht3=ht3, gt3=gt3, p=p, conf=conf) + t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t4=t3, h4=ht3, g4=gt3, p_exp=p, conf=conf) if t4 == np.inf or t4 > max_time: logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return logging.info("A4: {}".format(t4)) # A5 - t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t4=t4, ht4=ht4, gt4=gt4, p=p, conf=conf) + t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t5=t4, h5=ht4, g5=gt4, p_exp=p, conf=conf) if t5 == np.inf or t5 > max_time: logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return logging.info("A5: {}".format(t5)) # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t5=t5, ht5=ht5, gt5=gt5, p=p, conf=conf) + t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t_end=t5, h6=ht5, g6=gt5, p_exp=p, conf=conf) if t6 == np.inf or t6 > max_time: logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return From 028d4973aba58fd61c4959285af49ee889fd4ce3 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 15 Sep 2021 20:38:38 +1000 Subject: [PATCH 35/71] complete randomised recovery verification --- .../simulator/ode_three_comp_hyd_simulator.py | 8 ++-- tests/rec_trial_tests.py | 44 ++++++++++--------- tests/tte_phase_tests.py | 33 +++++++------- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 69364cb..81a0141 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -18,7 +18,7 @@ def tte(conf: list, p_exp: float, t_max: float = 5000) -> (float, float, float): ODEThreeCompHydSimulator.tte_a2, ODEThreeCompHydSimulator.tte_a3, ODEThreeCompHydSimulator.tte_a4, - ODEThreeCompHydSimulator.tte_a4, + ODEThreeCompHydSimulator.tte_a5, ODEThreeCompHydSimulator.tte_a6] # start with fully reset agent @@ -29,7 +29,7 @@ def tte(conf: list, p_exp: float, t_max: float = 5000) -> (float, float, float): p_exp=p_exp, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point - if t == t_max: + if t == np.inf or t == t_max: return t, h, g # if all phases complete full exhaustion is reached @@ -717,10 +717,10 @@ def optimize(func, initial_guess, max_steps): raise UserWarning("initial guess for func is not positive") # while maximal precision is not reached - while step_size > 0.000001: + while step_size > 0.0000001: t_p = t # increase t until function turns negative - while t <= max_steps and func(t) >= 0: + while t <= max_steps + 1 and func(t) >= 0: t_p = t t += step_size diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index c016741..68bc4e2 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -8,12 +8,15 @@ import numpy as np -def rec_trial_procedure(p_exp, p_rec, t_max, hz, eps, conf, agent, log_level=0): +def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_level=0): # Start with first time to exhaustion bout - t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) + t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, t_max=t_max) - if t == np.inf: + if t == np.inf or int(t) == int(t_max): + logging.info("Exhaustion not reached during TTE") return + if t >= t_max: + raise UserWarning("should never happen {} > {}".format(t, t_max)) # double-check with discrete agent for _ in range(int(round(t * hz))): @@ -44,7 +47,7 @@ def rec_trial_procedure(p_exp, p_rec, t_max, hz, eps, conf, agent, log_level=0): t_p = t # get estimated time of phase end - t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) + t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_rec, conf=conf) # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) # double-check with discrete agent @@ -65,7 +68,7 @@ def rec_trial_procedure(p_exp, p_rec, t_max, hz, eps, conf, agent, log_level=0): return -def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, +def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, t_max: float = 240, hz: int = 250, eps: float = 0.001): """ creates random agents and tests the discretised against the differential one @@ -81,7 +84,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_max=t_max, + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=example_conf, agent=agent, log_level=2) @@ -92,24 +95,25 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") p_exp = 260 - t_max = 180 + t_rec = 180 p_rec = 0 + t_max = 5000 # estimations per second for discrete agent hz = 250 # required precision of discrete to differential agent - eps = 0.0001 + eps = 0.001 # a configuration - # c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, - # 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] - # agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], - # m_ans=c[3], m_anf=c[4], the=c[5], - # gam=c[6], phi=c[7]) - # ThreeCompVisualisation(agent) - # - # rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_max=t_max, - # hz=hz, eps=eps, conf=c, - # agent=agent, log_level=2) - - the_loop(p_exp=p_exp, p_rec=p_rec, t_max=t_max, hz=hz, eps=eps) + c = [17530.530747393303, 37625.72364566721, 268.7372285266482, 223.97570400889148, + 7.895654547752743, 0.1954551343626819, 0.224106497474462, 0.01] + agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], + m_ans=c[3], m_anf=c[4], the=c[5], + gam=c[6], phi=c[7]) + ThreeCompVisualisation(agent) + + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + hz=hz, eps=eps, conf=c, + agent=agent, log_level=2) + + the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 1d5b106..6fa408c 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -17,39 +17,39 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): max_time = 5000 # A1 - t1, ht1, gt1 = ODEThreeCompHydSimulator.tte_a1(p=p, conf=conf) - if t1 == np.inf or t1 > max_time: + t1, ht1, gt1 = ODEThreeCompHydSimulator.tte_a1(t1=0, h1=0, g1=0, p_exp=p, t_max=max_time, conf=conf) + if t1 == np.inf or t1 >= max_time: logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return logging.info("A1: {}".format(t1)) - t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t2=t1, h2=ht1, g2=gt1, p_exp=p, conf=conf) + t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t2=t1, h2=ht1, g2=gt1, p_exp=p, t_max=max_time, conf=conf) logging.info("A2: {}".format(t2)) # A3 - t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t3=t2, h3=ht2, g3=gt2, p_exp=p, conf=conf) - if t3 == np.inf or t3 > max_time: + t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t3=t2, h3=ht2, g3=gt2, p_exp=p, t_max=max_time, conf=conf) + if t3 == np.inf or t3 >= max_time: logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) return logging.info("A3: {}".format(t3)) # A4 - t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t4=t3, h4=ht3, g4=gt3, p_exp=p, conf=conf) - if t4 == np.inf or t4 > max_time: + t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t4=t3, h4=ht3, g4=gt3, p_exp=p, t_max=max_time, conf=conf) + if t4 == np.inf or t4 >= max_time: logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) return logging.info("A4: {}".format(t4)) # A5 - t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t5=t4, h5=ht4, g5=gt4, p_exp=p, conf=conf) - if t5 == np.inf or t5 > max_time: + t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t5=t4, h5=ht4, g5=gt4, p_exp=p, t_max=max_time, conf=conf) + if t5 == np.inf or t5 >= max_time: logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) return logging.info("A5: {}".format(t5)) # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t_end=t5, h6=ht5, g6=gt5, p_exp=p, conf=conf) - if t6 == np.inf or t6 > max_time: + t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t6=t5, h6=ht5, g6=gt5, p_exp=p, t_max=max_time, conf=conf) + if t6 == np.inf or t6 >= max_time: logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) return logging.info("A6: {}".format(t6)) @@ -92,7 +92,7 @@ def the_loop(p: float = 350.0, m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) - ThreeCompVisualisation(agent) + # ThreeCompVisualisation(agent) tte_test_procedure(p, hz, eps, example_conf, agent) @@ -103,9 +103,8 @@ def test_one_config(example_conf=None): # just a default value if example_conf is None: - example_conf = [15101.24769778409, 86209.27743067988, 52.71702367096787, - 363.2970828395908, 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] + example_conf = [17530.530747393303, 37625.72364566721, 268.7372285266482, 223.97570400889148, + 7.895654547752743, 0.1954551343626819, 0.224106497474462, 0.01] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], @@ -122,11 +121,11 @@ def test_one_config(example_conf=None): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p = 350 + p = 260 # estimations per second for discrete agent hz = 250 # required precision of discrete to differential agent - eps = 0.005 + eps = 0.001 test_one_config() From e97084dca5a97f2f40e5ca07ea142a68fca1a7f1 Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 21 Sep 2021 18:38:13 +1000 Subject: [PATCH 36/71] begin adaptation of new notation and more flexible tte phases --- .../simulator/ode_three_comp_hyd_simulator.py | 64 ++++++---- tests/configurations.py | 2 +- tests/rec_phase_tests.py | 116 ++++++++++++++++++ tests/rec_trial_tests.py | 90 +++++++------- tests/tte_phase_tests.py | 7 +- 5 files changed, 199 insertions(+), 80 deletions(-) create mode 100644 tests/rec_phase_tests.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 81a0141..e0595b9 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -12,17 +12,17 @@ class ODEThreeCompHydSimulator: eps = 0.000001 @staticmethod - def tte(conf: list, p_exp: float, t_max: float = 5000) -> (float, float, float): + def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): - phases = [ODEThreeCompHydSimulator.tte_a1, - ODEThreeCompHydSimulator.tte_a2, + phases = [ODEThreeCompHydSimulator.lAe, + ODEThreeCompHydSimulator.mAe, ODEThreeCompHydSimulator.tte_a3, ODEThreeCompHydSimulator.tte_a4, ODEThreeCompHydSimulator.tte_a5, ODEThreeCompHydSimulator.tte_a6] # start with fully reset agent - t, h, g = 0, 0, 0 + t, h, g = 0, start_h, start_g # iterate through all phases until end is reached for phase in phases: t, h, g = phase(t, h, g, @@ -36,46 +36,56 @@ def tte(conf: list, p_exp: float, t_max: float = 5000) -> (float, float, float): return t, h, g @staticmethod - def tte_a1(t1: float, h1: float, g1: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] theta = conf[5] phi = conf[7] - if t1 > 0 or h1 > 0 or g1 > 0: - raise UserWarning("TTE expects agent to be fully reset (t=0, h=0, g=0)") + # phase ends at bottom of Ae or top of AnS + h_target = min(theta, 1 - phi) - # TODO: t_max not considered yet - # check if equilibrium in phase A1 - if ((m_ae * min(theta, 1 - phi)) / (p_exp * (1 - phi))) >= 1: - h = (a_anf * (1 - phi)) / m_ae * (1 - np.exp(-(m_ae * np.inf) / (a_anf * (1 - phi)))) * p_exp / a_anf - return np.inf, h, 0 + # Phase A1 is not applicable if fill-level of AnS too low, .. + # ... or Ans not full + if h_s > h_target or g_s > 0: + return t_s, h_s, g_s - # end of phase A1 -> the time when h(t) = min(theta,1-phi) - t1 = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * min(theta, 1 - phi) / (p_exp * (1 - phi)))) + # derived general solution with h(0) = 0 for c1 + def a1_ht(t): + return p_exp * (1 - phi) / m_ae * (1 - np.exp(m_ae * t / (a_anf * (phi - 1)))) - # return t1, h(t1), g(t1) - return t1, min(theta, 1 - phi), 0 + # check if max time is reached in this phase + if a1_ht(t_max) <= h_target: + return t_max, a1_ht(t_max), g_s + else: + # end of phase A1 -> the time when h(t) = min(theta,1-phi) + t_end = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * h_target / (p_exp * (1 - phi)))) + return t_end, h_target, g_s @staticmethod - def tte_a2(t2: float, h2: float, g2: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def mAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] theta = conf[5] phi = conf[7] - # A2 is only applicable if phi is higher than the top of AnS - if phi <= (1 - theta): - return t2, h2, g2 + # this phase is only applicable if h is in-between 1-theta and phi and ... + # ... AnS is full + if not theta >= h_s >= (1 - phi) or g_s > 0: + return t_s, h_s, g_s - # TODO: t_max not considered yet # linear utilization -> no equilibrium possible - t_end = t2 + ((phi - (1 - theta)) * a_anf) / (p_exp - m_ae) + t_end = t_s + ((phi - (1 - theta)) * a_anf) / (p_exp - m_ae) - # return t2, h(t2), g(t2) - return t_end, theta, g2 + # check if max time is reached before phase end + if t_end > t_max: + h_end = h_s + (t_max - t_s) * (p_exp - m_ae) / a_anf + return t_end, h_end, g_s + + else: + return t_end, theta, g_s @staticmethod def tte_a3(t3: float, h3: float, g3: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): @@ -598,9 +608,11 @@ def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: phi = conf[7] # A3 R2 is only applicable if h is above or at pipe exit of Ae... - # ... and h is above g (error of epsilon tolerated) + # ... and h is above g (error of epsilon tolerated)... + # ... and g is not 0 if not h3 <= 1 - phi + ODEThreeCompHydSimulator.eps \ - or not h3 < g3 + theta + ODEThreeCompHydSimulator.eps: + or not h3 < g3 + theta + ODEThreeCompHydSimulator.eps \ + or not g3 > 0.0: return t3, h3, g3 # EQ 16 and 17 substituted in EQ 8 diff --git a/tests/configurations.py b/tests/configurations.py index c8468bf..9de309f 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -61,7 +61,7 @@ # ThreeCompVisualisation(agent) # Start with first time to exhaustion bout - tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf) + tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, start_h=0, start_g=0) # double-check with discrete agent for _ in range(int(round(tte * hz))): diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py new file mode 100644 index 0000000..0ff07ed --- /dev/null +++ b/tests/rec_phase_tests.py @@ -0,0 +1,116 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator + +import logging + +import numpy as np + + +def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_level=0): + # Start with first time to exhaustion bout + t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, t_max=t_max) + + if t == np.inf or int(t) == int(t_max): + logging.info("Exhaustion not reached during TTE") + return + + # double-check with discrete agent + for _ in range(int(round(t * hz))): + agent.set_power(p_exp) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) + + # now iterate through all recovery phases + phases = [ODEThreeCompHydSimulator.rec_a6, + ODEThreeCompHydSimulator.rec_a5, + ODEThreeCompHydSimulator.rec_a4_r1, + ODEThreeCompHydSimulator.rec_a4_r2, + ODEThreeCompHydSimulator.rec_a3_r1, + ODEThreeCompHydSimulator.rec_a3_r2, + ODEThreeCompHydSimulator.rec_a2, + ODEThreeCompHydSimulator.rec_a1] + + # restart time from 0 + t = 0 + + # detailed checks for every phase + for phase in phases: + # save previous time to estimate time difference + t_p = t + + # get estimated time of phase end + t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_rec, conf=conf) + # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) + + # double-check with discrete agent + for _ in range(int(round((t - t_p) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + + # ThreeCompVisualisation(agent) + + assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) + assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) + + if t == t_max: + logging.info("Max recovery reached in {}".format(phase)) + # ThreeCompVisualisation(agent) + return + + +def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, + t_max: float = 240, hz: int = 250, eps: float = 0.001): + """ + creates random agents and tests the discretised against the differential one + """ + + while True: + udp = MultiObjectiveThreeCompUDP(None, None) + + example_conf = udp.create_educated_initial_guess() + logging.info(example_conf) + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], + m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], + gam=example_conf[6], phi=example_conf[7]) + + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + hz=hz, eps=eps, conf=example_conf, + agent=agent, log_level=2) + + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + p_exp = 560 + t_rec = 180 + p_rec = 0 + t_max = 5000 + + # estimations per second for discrete agent + hz = 250 + # required precision of discrete to differential agent + eps = 0.001 + + # a configuration + c = [17530.530747393303, 37625.72364566721, 268.7372285266482, 223.97570400889148, + 7.895654547752743, 0.1954551343626819, 0.224106497474462, 0.01] + agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], + m_ans=c[3], m_anf=c[4], the=c[5], + gam=c[6], phi=c[7]) + ThreeCompVisualisation(agent) + + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + hz=hz, eps=eps, conf=c, + agent=agent, log_level=2) + + the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 68bc4e2..f009e13 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -1,5 +1,6 @@ from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.simulator.three_comp_hyd_simulator import ThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator @@ -10,62 +11,53 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_level=0): # Start with first time to exhaustion bout - t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, t_max=t_max) + tte_1, h, g = ODEThreeCompHydSimulator.tte(conf=conf, + start_h=0, start_g=0, + p_exp=p_exp, t_max=t_max) - if t == np.inf or int(t) == int(t_max): + if tte_1 == np.inf or int(tte_1) == int(t_max): logging.info("Exhaustion not reached during TTE") return - if t >= t_max: - raise UserWarning("should never happen {} > {}".format(t, t_max)) # double-check with discrete agent - for _ in range(int(round(t * hz))): + for _ in range(int(round(tte_1 * hz))): agent.set_power(p_exp) agent.perform_one_step() g_diff = agent.get_g() - g h_diff = agent.get_h() - h - assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) - # ThreeCompVisualisation(agent) - - # now iterate through all recovery phases - phases = [ODEThreeCompHydSimulator.rec_a6, - ODEThreeCompHydSimulator.rec_a5, - ODEThreeCompHydSimulator.rec_a4_r1, - ODEThreeCompHydSimulator.rec_a4_r2, - ODEThreeCompHydSimulator.rec_a3_r1, - ODEThreeCompHydSimulator.rec_a3_r2, - ODEThreeCompHydSimulator.rec_a2, - ODEThreeCompHydSimulator.rec_a1] - - # restart time from 0 - t = 0 - - # detailed checks for every phase - for phase in phases: - # save previous time to estimate time difference - t_p = t - - # get estimated time of phase end - t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_rec, conf=conf) - # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) - - # double-check with discrete agent - for _ in range(int(round((t - t_p) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - g - h_diff = agent.get_h() - h - - # ThreeCompVisualisation(agent) - - assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) - assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) - - if t == t_max: - logging.info("Max recovery reached in {}".format(phase)) - # ThreeCompVisualisation(agent) - return + assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) + + rec, h, g = ODEThreeCompHydSimulator.rec(conf=conf, + start_h=h, start_g=g, + p_rec=p_rec, t_max=t_rec) + + # double-check with discrete agent + for _ in range(int(round(rec * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + + tte_2, h, g = ODEThreeCompHydSimulator.tte(conf=conf, + start_h=h, start_g=g, + p_exp=p_exp, t_max=t_max) + + # double-check with discrete agent + for _ in range(int(round(tte_2 * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + assert abs(g_diff) < eps, "TTE2 g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE2 h is off by {}".format(h_diff) + + rec_ratio = tte_2 / tte_1 * 100.0 + est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, + p_rec=p_rec, t_rec=t_rec) + logging.info("ODE ratio: {} \nEST ratio: {}".format(rec_ratio, est_ratio)) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, @@ -94,7 +86,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 260 + p_exp = 560 t_rec = 180 p_rec = 0 t_max = 5000 @@ -116,4 +108,4 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, hz=hz, eps=eps, conf=c, agent=agent, log_level=2) - the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) + # the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 6fa408c..ccf6379 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -17,13 +17,13 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): max_time = 5000 # A1 - t1, ht1, gt1 = ODEThreeCompHydSimulator.tte_a1(t1=0, h1=0, g1=0, p_exp=p, t_max=max_time, conf=conf) + t1, ht1, gt1 = ODEThreeCompHydSimulator.lAe(t_s=0, h_s=0, g_s=0, p_exp=p, t_max=max_time, conf=conf) if t1 == np.inf or t1 >= max_time: logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) return logging.info("A1: {}".format(t1)) - t2, ht2, gt2 = ODEThreeCompHydSimulator.tte_a2(t2=t1, h2=ht1, g2=gt1, p_exp=p, t_max=max_time, conf=conf) + t2, ht2, gt2 = ODEThreeCompHydSimulator.mAe(t_s=t1, h_s=ht1, g_s=gt1, p_exp=p, t_max=max_time, conf=conf) logging.info("A2: {}".format(t2)) # A3 @@ -103,8 +103,7 @@ def test_one_config(example_conf=None): # just a default value if example_conf is None: - example_conf = [17530.530747393303, 37625.72364566721, 268.7372285266482, 223.97570400889148, - 7.895654547752743, 0.1954551343626819, 0.224106497474462, 0.01] + example_conf = [22960.500530676287, 77373.91670678859, 234.24391186170348, 382.9246247635444, 32.281951614864944, 0.4382785572308323, 0.29408404649271447, 0.18875064978567485] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], From 1a19b0b7d7f7844555c417d102b1a22cf97690e7 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 22 Sep 2021 15:40:24 +1000 Subject: [PATCH 37/71] finish new work bout estimation procedure, add AnS recovery into phases where g is not 0 --- .../simulator/ode_three_comp_hyd_simulator.py | 276 +++++++++++++----- tests/tte_phase_tests.py | 106 ++++--- 2 files changed, 261 insertions(+), 121 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index e0595b9..f56bf09 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -14,12 +14,14 @@ class ODEThreeCompHydSimulator: @staticmethod def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): - phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.mAe, - ODEThreeCompHydSimulator.tte_a3, - ODEThreeCompHydSimulator.tte_a4, - ODEThreeCompHydSimulator.tte_a5, - ODEThreeCompHydSimulator.tte_a6] + phases = [ODEThreeCompHydSimulator.work_lAe, + ODEThreeCompHydSimulator.work_lAe_rAnS, + ODEThreeCompHydSimulator.work_fAe, + ODEThreeCompHydSimulator.work_fAe_rAnS, + ODEThreeCompHydSimulator.work_lAe_lAnS, + ODEThreeCompHydSimulator.work_fAe_lAnS, + ODEThreeCompHydSimulator.work_lAe_fAns, + ODEThreeCompHydSimulator.work_fAe_fAnS] # start with fully reset agent t, h, g = 0, start_h, start_g @@ -36,7 +38,7 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = return t, h, g @staticmethod - def lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def work_lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] @@ -46,33 +48,104 @@ def lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: li # phase ends at bottom of Ae or top of AnS h_target = min(theta, 1 - phi) - # Phase A1 is not applicable if fill-level of AnS too low, .. - # ... or Ans not full + # This phase is not applicable if fill-level of AnF below pipe exit Ae or top of AnS, .. + # ... or AnS not full if h_s > h_target or g_s > 0: return t_s, h_s, g_s # derived general solution with h(0) = 0 for c1 - def a1_ht(t): + def ht(t): return p_exp * (1 - phi) / m_ae * (1 - np.exp(m_ae * t / (a_anf * (phi - 1)))) # check if max time is reached in this phase - if a1_ht(t_max) <= h_target: - return t_max, a1_ht(t_max), g_s + if ht(t_max) <= h_target: + return t_max, ht(t_max), g_s else: # end of phase A1 -> the time when h(t) = min(theta,1-phi) t_end = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * h_target / (p_exp * (1 - phi)))) return t_end, h_target, g_s @staticmethod - def mAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def work_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + float, float, float): a_anf = conf[0] + a_ans = conf[1] m_ae = conf[2] + m_anf = conf[4] theta = conf[5] + gamma = conf[6] phi = conf[7] - # this phase is only applicable if h is in-between 1-theta and phi and ... - # ... AnS is full + # This phase is not applicable if fill-level of AnF below pipe exit Ae, .. + # ... or AnS fill-level is above AnF fill-level ... + # ... or AnS is full + if h_s > 1 - phi or g_s + theta <= h_s or g_s <= 0.0: + return t_s, h_s, g_s + + # TODO: mostly copied from rec A3R2 find ways to combine equations + # EQ 16 and 17 substituted in EQ 8 + a = m_ae / (a_anf * (1 - phi)) + \ + m_anf / (a_ans * (1 - gamma)) + \ + m_anf / (a_anf * (1 - gamma)) + + b = m_ae * m_anf / \ + (a_anf * a_ans * (1 - phi) * (1 - gamma)) + + c = m_anf * (p_exp * (1 - phi) - m_ae * theta) / \ + (a_anf * a_ans * (1 - phi) * (1 - gamma)) + + # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c + r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) + r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) + + # uses Al dt/dl part of EQ(16) == dl/dt of EQ(14) solved for c2 + # and then substituted in EQ(14) and solved for c1 + s_c1 = (c / b - (m_anf * (g_s + theta - h_s)) / (a_ans * r2 * (1 - gamma)) - g_s) / \ + (np.exp(r1 * t_s) * (r1 / r2 - 1)) + + # uses EQ(14) with solution for c1 and solves for c2 + s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) + + def gt(t): + # the general solution for g(t) + return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b + + # substitute into EQ(9) for h + def ht(t): + k1 = a_ans * (1 - gamma) / m_anf * s_c1 * r1 + s_c1 + k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 + return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta + + # find the point where g(t) == 0 + g0 = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t), + initial_guess=t_s, + max_steps=t_max) + + # find the point where fill-levels AnS and AnF are at equal + gtht = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t) + theta - ht(t), + initial_guess=t_s, + max_steps=t_max) + + # find the point where h drops below pipe exit of Ae + h_end = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - ht(t), + initial_guess=t_s, + max_steps=t_max) + + # phase ends at the earliest of these time points + t_end = min(g0, gtht, h_end) + return t_end, ht(t_end), gt(t_end) + + @staticmethod + def work_fAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + + a_anf = conf[0] + m_ae = conf[2] + theta = conf[5] + phi = conf[7] + + # this phase not applicable if h is not in-between 1-theta and phi and ... + # ... AnS is not full if not theta >= h_s >= (1 - phi) or g_s > 0: return t_s, h_s, g_s @@ -88,7 +161,65 @@ def mAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: li return t_end, theta, g_s @staticmethod - def tte_a3(t3: float, h3: float, g3: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def work_fAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + float, float, float): + # TODO: mostly copied from rec A4R2 find ways to combine equations + + a_anf = conf[0] + a_ans = conf[1] + m_ae = conf[2] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # This phase is not applicable if fill-level of AnF above or at pipe exit Ae, .. + # ... or AnS fill-level is above AnF fill-level ... + # ... or AnS is full + if h_s <= 1 - phi - ODEThreeCompHydSimulator.eps or \ + g_s + theta <= h_s or \ + g_s <= 0.0 + ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s + + # if h is above g (flow from AnF into AnS) + a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) + b_hg = (p_exp - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) + + # derivative g'(t4) can be calculated manually + dgt4_hg = - m_anf * (g_s + theta - h_s) / (a_ans * (1 - gamma)) + + # which then allows to derive c1 and c2 + s_c1_gh = ((p_exp - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t_s) + s_c2_gh = (-t_s * b_hg + dgt4_hg) / a_hg - (p_exp - m_ae) / ((a_anf + a_ans) * a_hg) + g_s + + def gt(t): + # general solution for g(t) + return t * (p_exp - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) + + def dgt(t): + # first derivative g'(t) + return (p_exp - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) + + def ht(t): + # EQ(16) with constants for g(t) and g'(t) + return a_ans * (1 - gamma) / m_anf * dgt(t) + gt(t) + theta + + # find the point where g(t) == 0 + g0 = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t), + initial_guess=t_s, + max_steps=t_max) + + # find the point where fill-levels AnS and AnF are at equal + gtht = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t) + theta - ht(t), + initial_guess=t_s, + max_steps=t_max) + + t_end = min(g0, gtht) + return t_end, ht(t_end), gt(t_end) + + @staticmethod + def work_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -98,9 +229,10 @@ def tte_a3(t3: float, h3: float, g3: float, p_exp: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # A3 is only applicable if flow from Ae is not at max - if phi > (1 - theta): - return t3, h3, g3 + # This phase is not applicable h is lower than pipe exit Ae... + # ... or if reflow into AnS is happening + if h_s > (1 - phi) or g_s + theta > h_s + ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s # taken from Equation 11 by Morton 1986 # a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( @@ -121,16 +253,18 @@ def tte_a3(t3: float, h3: float, g3: float, p_exp: float, t_max: float, conf: li r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - # c1 and c2 can be determined with g(t2) = g'(t2) = 0 - s_c1 = -c / (b * (1 - r1 / r2)) * np.exp(-r1 * t3) - s_c2 = s_c1 * np.exp(r1 * t3) * np.exp(-r2 * t3) * -r1 / r2 + # uses Al dt/dl part of EQ(9) == dl/dt of EQ(12) solved for c2 + # and then substituted in EQ(12) and solved for c1 + s_c1 = (g_s - m_ans * (h_s - g_s - theta) / (a_ans * r2 * (1 - theta - gamma)) - c / b) / ( + np.exp(r1 * t_s) * (1 - r1 / r2)) + s_c2 = s_c1 * np.exp(r1 * t_s) * np.exp(-r2 * t_s) * -r1 / r2 - def a3_gt(t): + def gt(t): # the general solution for g(t) return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b # substitute into EQ(9) for h - def a3_ht(t): + def ht(t): k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta @@ -138,16 +272,14 @@ def a3_ht(t): # if phi > gamma, then phase A3 transitions into phase A4 before AnS is empty h_target = 1 - max(phi, gamma) - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a3_ht(t), - initial_guess=t3, + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + initial_guess=t_s, max_steps=t_max) - - # t3 = optimize.fsolve(lambda t: ht3 - a3_ht(t), x0=np.array([t2]))[0] - # use t3 to get g(t3) - return t_end, a3_ht(t_end), a3_gt(t_end) + return t_end, ht(t_end), gt(t_end) @staticmethod - def tte_a4(t4: float, h4: float, g4: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def work_fAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -157,42 +289,45 @@ def tte_a4(t4: float, h4: float, g4: float, p_exp: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # phase A3 is not applicable if gamma is greater or equal to phi - if gamma >= phi: - return t4, h4, g4 + # This phase is not applicable h is not in-between pipe exit Ae and pipe exit AnS... + # ... or if reflow into AnS is happening + if not 1 - phi - ODEThreeCompHydSimulator.eps <= h_s <= 1 - gamma + ODEThreeCompHydSimulator.eps or \ + g_s + theta > h_s + ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s # b/a can be simplified as (p-m_ae)/(a_anf + a_ans) a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) b = (p_exp - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) # derivative g'(t3) can be calculated manually - dgt3 = m_ans * (h4 - g4 - theta) / (a_ans * (1 - theta - gamma)) + dgt3 = m_ans * (h_s - g_s - theta) / (a_ans * (1 - theta - gamma)) # which then allows to derive c1 and c2 - s_c1 = ((p_exp - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t4) - s_c2 = (-t4 * b + dgt3) / a - (p_exp - m_ae) / ((a_anf + a_ans) * a) + g4 + s_c1 = ((p_exp - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t_s) + s_c2 = (-t_s * b + dgt3) / a - (p_exp - m_ae) / ((a_anf + a_ans) * a) + g_s - def a4_gt(t): + def gt(t): # general solution for g(t) return t * (p_exp - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) - def a4_dgt(t): + def dgt(t): # first derivative g'(t) return (p_exp - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) - def a4_ht(t): + def ht(t): # EQ(9) with constants for g(t) and g'(t) - return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta + return a_ans * (1 - theta - gamma) / m_ans * dgt(t) + gt(t) + theta h_target = 1 - gamma - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a4_ht(t), - initial_guess=t4, + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + initial_guess=t_s, max_steps=t_max) - return t_end, a4_ht(t_end), a4_gt(t_end) + return t_end, ht(t_end), gt(t_end) @staticmethod - def tte_a5(t5: float, h5: float, g5: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def work_lAe_fAns(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + float, float, float): a_anf = conf[0] a_ans = conf[1] @@ -202,14 +337,15 @@ def tte_a5(t5: float, h5: float, g5: float, p_exp: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # phase A5 is not applicable if phi is greater or equal to gamma - if phi >= gamma: - return t5, h5, g5 + # this phase is not applicable if phi is greater or equal to gamma... + # ... or h is already below pipe exit Ae + if phi >= gamma or h_s >= 1 - phi: + return t_s, h_s, g_s # g(t4) = g4 can be solved for c - s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) + s_cg = (g_s - (1 - theta - gamma)) * np.exp((m_ans * t_s) / ((1 - theta - gamma) * a_ans)) - def a5_gt(t): + def gt(t): # generalised g(t) for phase A5 return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) @@ -220,24 +356,25 @@ def a5_gt(t): b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) # find c that matches h(t4) = h4 - s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 + s_ch = (h_s + b / ((a + k) * np.exp(k) ** t_s) + g / a) / np.exp(a) ** t_s - def a5_ht(t): + def ht(t): return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a h_target = 1 - phi - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a5_ht(t), - initial_guess=t5, + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + initial_guess=t_s, max_steps=t_max) - return t_end, a5_ht(t_end), a5_gt(t_end) + return t_end, ht(t_end), gt(t_end) @staticmethod - def tte_a6(t6: float, h6: float, g6: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def work_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + float, float, float): """ - Final phase A6 of a time to exhaustion trial. Expects inputs from Phase A5. - :param t6: time at which A5 ended - :param h6: h(t5) - :param g6: g(t5) + Final phase before exhaustion. + :param t_s: time at which A5 ended + :param h_s: h(t5) + :param g_s: g(t5) :param p_exp: constant power output :param t_max: maximal time limit :param conf: configuration of hydraulic model @@ -249,11 +386,16 @@ def tte_a6(t6: float, h6: float, g6: float, p_exp: float, t_max: float, conf: li m_ans = conf[3] theta = conf[5] gamma = conf[6] + phi = conf[7] + + # this phase is not applicable if Ae or AnS are directly at the bottom of the model + if phi == 0.0 or gamma == 0.0: + return t_s, h_s, g_s # g(t5) = gt5 can be solved for c - s_cg = (g6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) + s_cg = (g_s - (1 - theta - gamma)) / np.exp(-m_ans * t_s / ((1 - theta - gamma) * a_ans)) - def a6_gt(t): + def gt(t): # generalised g(t) for phase A6 return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) @@ -264,18 +406,18 @@ def a6_gt(t): ag = (p_exp - m_ae) / a_anf # h(t5) = ht5 can be solved for c - s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + h6 + s_ch = -t_s * ag + ((b * math.exp(-k * t_s)) / k) + h_s - def a6_ht(t): + def ht(t): # generalised h(t) for phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch h_target = 1.0 # find end of phase A6. The time point where h(t_end)=1 - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - a6_ht(t), - initial_guess=t6, + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + initial_guess=t_s, max_steps=t_max) - return t_end, a6_ht(t_end), a6_gt(t_end) + return t_end, ht(t_end), gt(t_end) @staticmethod def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: float = 5000.0) -> ( diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index ccf6379..0b2d499 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -4,66 +4,66 @@ from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator import logging -import warnings - -import numpy as np - - -# warnings.filterwarnings("error") - def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): # all TTE phases max_time = 5000 - # A1 - t1, ht1, gt1 = ODEThreeCompHydSimulator.lAe(t_s=0, h_s=0, g_s=0, p_exp=p, t_max=max_time, conf=conf) - if t1 == np.inf or t1 >= max_time: - logging.info("EQUILIBRIUM IN A1: t: {} h: {} g: {}".format(t1, ht1, gt1)) - return - logging.info("A1: {}".format(t1)) - - t2, ht2, gt2 = ODEThreeCompHydSimulator.mAe(t_s=t1, h_s=ht1, g_s=gt1, p_exp=p, t_max=max_time, conf=conf) - logging.info("A2: {}".format(t2)) - - # A3 - t3, ht3, gt3 = ODEThreeCompHydSimulator.tte_a3(t3=t2, h3=ht2, g3=gt2, p_exp=p, t_max=max_time, conf=conf) - if t3 == np.inf or t3 >= max_time: - logging.info("EQUILIBRIUM IN A3: t: {} h: {} g: {}".format(t3, ht3, gt3)) - return - logging.info("A3: {}".format(t3)) - - # A4 - t4, ht4, gt4 = ODEThreeCompHydSimulator.tte_a4(t4=t3, h4=ht3, g4=gt3, p_exp=p, t_max=max_time, conf=conf) - if t4 == np.inf or t4 >= max_time: - logging.info("EQUILIBRIUM IN A4: t: {} h: {} g: {}".format(t4, ht4, gt4)) - return - logging.info("A4: {}".format(t4)) - - # A5 - t5, ht5, gt5 = ODEThreeCompHydSimulator.tte_a5(t5=t4, h5=ht4, g5=gt4, p_exp=p, t_max=max_time, conf=conf) - if t5 == np.inf or t5 >= max_time: - logging.info("EQUILIBRIUM IN A5: t: {} h: {} g: {}".format(t5, ht5, gt5)) - return - logging.info("A5: {}".format(t5)) - - # A6 - t6, ht6, gt6 = ODEThreeCompHydSimulator.tte_a6(t6=t5, h6=ht5, g6=gt5, p_exp=p, t_max=max_time, conf=conf) - if t6 == np.inf or t6 >= max_time: - logging.info("EQUILIBRIUM IN A6: t: {} h: {} g: {}".format(t6, ht6, gt6)) - return - logging.info("A6: {}".format(t6)) - - ts = [t1, t2, t3, t4, t5, t6] - hts = [ht1, ht2, ht3, ht4, ht5, ht6] - gts = [gt1, gt2, gt3, gt4, gt5, gt6] - + phases = [ODEThreeCompHydSimulator.work_lAe, + ODEThreeCompHydSimulator.work_lAe_rAnS, + ODEThreeCompHydSimulator.work_fAe, + ODEThreeCompHydSimulator.work_fAe_rAnS, + ODEThreeCompHydSimulator.work_lAe_lAnS, + ODEThreeCompHydSimulator.work_fAe_lAnS, + ODEThreeCompHydSimulator.work_lAe_fAns, + ODEThreeCompHydSimulator.work_fAe_fAnS] + + # set initial conditions + h_s = 0 + g_s = 1 - conf[6] - conf[5] + t, h, g = 0, h_s, g_s + ts, hts, gts = [], [], [] + + # display initial state if log level is high enough + if log_level > 0: + agent.set_h(h_s) + agent.set_g(g_s) + ThreeCompVisualisation(agent) + + # iterate through all phases until end is reached + for phase in phases: + t, h, g = phase(t, h, g, p_exp=p, t_max=max_time, conf=conf) + + # display intermediate state if log level is high enough + if log_level > 0: + logging.info("PHASE {}".format(phase)) + agent.set_h(h) + agent.set_g(g) + ThreeCompVisualisation(agent) + + # exit loop if end is reached + if t >= max_time: + logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(phase, t, h, g)) + break + + # keep track of times and states at phase ends + ts.append(t) + hts.append(h) + gts.append(g) + + # now confirm with iterative agent for i, t in enumerate(ts): + # set to initial state agent.reset() + agent.set_h(h_s) + agent.set_g(g_s) + + # simulate tte for _ in range(int(round(t * hz))): agent.set_power(p) agent.perform_one_step() + # estimate estimation differences g_diff = agent.get_g() - gts[i] h_diff = agent.get_h() - hts[i] @@ -103,15 +103,13 @@ def test_one_config(example_conf=None): # just a default value if example_conf is None: - example_conf = [22960.500530676287, 77373.91670678859, 234.24391186170348, 382.9246247635444, 32.281951614864944, 0.4382785572308323, 0.29408404649271447, 0.18875064978567485] + example_conf = [25925.53993526785, 60694.43170965706, 219.8524740824735, 216.83073328159165, + 26.323382867622215, 0.15040127238313122, 0.28924204946747195, 0.1762119589774433] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], gam=example_conf[6], phi=example_conf[7]) - - ThreeCompVisualisation(agent) - tte_test_procedure(p, hz, eps, example_conf, agent, log_level=2) @@ -120,7 +118,7 @@ def test_one_config(example_conf=None): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p = 260 + p = 450 # estimations per second for discrete agent hz = 250 # required precision of discrete to differential agent From 92dbd2a913504a71f0bc17c1f44677f28352417d Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 22 Sep 2021 18:06:47 +1000 Subject: [PATCH 38/71] finish rec trial tests and adjust equations that assume h=0 --- .../simulator/ode_three_comp_hyd_simulator.py | 18 +++-- tests/rec_a3_r2_trial.py | 3 - tests/rec_trial_tests.py | 62 ++++++++++------ tests/tte_phase_tests.py | 73 +++++++------------ 4 files changed, 75 insertions(+), 81 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index f56bf09..b9b8d56 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -50,19 +50,22 @@ def work_lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, con # This phase is not applicable if fill-level of AnF below pipe exit Ae or top of AnS, .. # ... or AnS not full - if h_s > h_target or g_s > 0: + if h_s > h_target or g_s > 0.0 + ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s - # derived general solution with h(0) = 0 for c1 + # constant can be derived from known t_s and h_s + c1 = (h_s - p_exp * (1 - phi) / m_ae) * np.exp(-m_ae * t_s / a_anf * (phi - 1)) + + # general solution for h(t) using c1 def ht(t): - return p_exp * (1 - phi) / m_ae * (1 - np.exp(m_ae * t / (a_anf * (phi - 1)))) + return p_exp * (1 - phi) / m_ae + c1 * np.exp(m_ae * t / (a_anf * (phi - 1))) # check if max time is reached in this phase if ht(t_max) <= h_target: return t_max, ht(t_max), g_s else: # end of phase A1 -> the time when h(t) = min(theta,1-phi) - t_end = -a_anf * (1 - phi) / m_ae * np.log(1 - (m_ae * h_target / (p_exp * (1 - phi)))) + t_end = a_anf * (phi - 1) / m_ae * np.log((h_target - p_exp * (1 - phi) / m_ae) / c1) return t_end, h_target, g_s @staticmethod @@ -80,7 +83,7 @@ def work_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float # This phase is not applicable if fill-level of AnF below pipe exit Ae, .. # ... or AnS fill-level is above AnF fill-level ... # ... or AnS is full - if h_s > 1 - phi or g_s + theta <= h_s or g_s <= 0.0: + if h_s > 1 - phi or g_s + theta <= h_s or g_s <= 0.0 + ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s # TODO: mostly copied from rec A3R2 find ways to combine equations @@ -146,7 +149,7 @@ def work_fAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, con # this phase not applicable if h is not in-between 1-theta and phi and ... # ... AnS is not full - if not theta >= h_s >= (1 - phi) or g_s > 0: + if not theta >= h_s >= (1 - phi) or g_s > 0.0 + ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s # linear utilization -> no equilibrium possible @@ -257,7 +260,8 @@ def work_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float # and then substituted in EQ(12) and solved for c1 s_c1 = (g_s - m_ans * (h_s - g_s - theta) / (a_ans * r2 * (1 - theta - gamma)) - c / b) / ( np.exp(r1 * t_s) * (1 - r1 / r2)) - s_c2 = s_c1 * np.exp(r1 * t_s) * np.exp(-r2 * t_s) * -r1 / r2 + # uses EQ(12) with solution for c1 and solves for c2 + s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) def gt(t): # the general solution for g(t) diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py index e7abba8..d1d85e4 100644 --- a/tests/rec_a3_r2_trial.py +++ b/tests/rec_a3_r2_trial.py @@ -69,19 +69,16 @@ # uses EQ(14) with solution for c1 and solves for c2 s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) - def a3_gt(t): # the general solution for g(t) return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b - # substitute into EQ(9) for h def a3_ht(t): k1 = a_ans * (1 - gamma) / m_anf * s_c1 * r1 + s_c1 k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - # use the quickest possible recovery as the initial guess (assumes h=0) in_c1 = (gt3 + theta) * np.exp(-m_anf * t3 / ((gamma - 1) * a_ans)) in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index f009e13..a8b596a 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -6,16 +6,22 @@ import logging -import numpy as np +def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], + m_ans=conf[3], m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + if log_level > 0: + ThreeCompVisualisation(agent) -def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_level=0): # Start with first time to exhaustion bout tte_1, h, g = ODEThreeCompHydSimulator.tte(conf=conf, start_h=0, start_g=0, p_exp=p_exp, t_max=t_max) - if tte_1 == np.inf or int(tte_1) == int(t_max): + if tte_1 >= t_max: logging.info("Exhaustion not reached during TTE") return @@ -28,6 +34,10 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_le assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) + if log_level > 0: + logging.info("TTE1 {} h: {} g: {}".format(tte_1, h, g)) + ThreeCompVisualisation(agent) + rec, h, g = ODEThreeCompHydSimulator.rec(conf=conf, start_h=h, start_g=g, p_rec=p_rec, t_max=t_rec) @@ -41,23 +51,37 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_le assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + if log_level > 0: + logging.info("REC {} h: {} g: {}".format(rec, h, g)) + ThreeCompVisualisation(agent) + tte_2, h, g = ODEThreeCompHydSimulator.tte(conf=conf, start_h=h, start_g=g, p_exp=p_exp, t_max=t_max) # double-check with discrete agent for _ in range(int(round(tte_2 * hz))): - agent.set_power(p_rec) + agent.set_power(p_exp) agent.perform_one_step() g_diff = agent.get_g() - g h_diff = agent.get_h() - h assert abs(g_diff) < eps, "TTE2 g is off by {}".format(g_diff) assert abs(h_diff) < eps, "TTE2 h is off by {}".format(h_diff) + if log_level > 0: + logging.info("TTE2 {} h: {} g: {}".format(tte_2, h, g)) + ThreeCompVisualisation(agent) + + # simulator step limit needs to be adjusted + ThreeCompHydSimulator.step_limit = 5000 * hz + est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, p_rec=p_rec, t_rec=t_rec) + ThreeCompHydSimulator.step_limit = 5000 / hz + rec_ratio = tte_2 / tte_1 * 100.0 - est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, - p_rec=p_rec, t_rec=t_rec) - logging.info("ODE ratio: {} \nEST ratio: {}".format(rec_ratio, est_ratio)) + diff = abs(rec_ratio - est_ratio) + + logging.info("ODE ratio: {} EST ratio: {}".format(rec_ratio, est_ratio)) + assert diff < eps, "Ratio estimations are too different {}".format(diff) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, @@ -71,14 +95,9 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, example_conf = udp.create_educated_initial_guess() logging.info(example_conf) - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, - hz=hz, eps=eps, conf=example_conf, - agent=agent, log_level=2) + hz=hz, eps=eps, conf=example_conf, log_level=0) if __name__ == "__main__": @@ -92,20 +111,15 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, t_max = 5000 # estimations per second for discrete agent - hz = 250 + hz = 800 # required precision of discrete to differential agent - eps = 0.001 + eps = 0.01 # a configuration - c = [17530.530747393303, 37625.72364566721, 268.7372285266482, 223.97570400889148, - 7.895654547752743, 0.1954551343626819, 0.224106497474462, 0.01] - agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], - m_ans=c[3], m_anf=c[4], the=c[5], - gam=c[6], phi=c[7]) - ThreeCompVisualisation(agent) + c = [5000, 65296.55506048172, 286.92487794015625, 287.8170222313081, 38.355175636958336, + 0.4377894638957991, 0.2437516643675469, 0.48005517184947655] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, - hz=hz, eps=eps, conf=c, - agent=agent, log_level=2) + hz=hz, eps=eps, conf=c, log_level=2) - # the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) + the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 0b2d499..b53cfe5 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -5,10 +5,16 @@ import logging -def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): + +def tte_test_procedure(p, hz, eps, conf, log_level=0): # all TTE phases max_time = 5000 + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], + m_ans=conf[3], m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + phases = [ODEThreeCompHydSimulator.work_lAe, ODEThreeCompHydSimulator.work_lAe_rAnS, ODEThreeCompHydSimulator.work_fAe, @@ -19,10 +25,9 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): ODEThreeCompHydSimulator.work_fAe_fAnS] # set initial conditions - h_s = 0 - g_s = 1 - conf[6] - conf[5] + h_s = 0.0423121637107437 + g_s = 0.061937224354866705 # 1 - conf[6] - conf[5] t, h, g = 0, h_s, g_s - ts, hts, gts = [], [], [] # display initial state if log level is high enough if log_level > 0: @@ -36,9 +41,10 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): # display intermediate state if log level is high enough if log_level > 0: - logging.info("PHASE {}".format(phase)) + logging.info("PHASE {} t {} ".format(phase, t)) agent.set_h(h) agent.set_g(g) + logging.info("ODE".format(phase, t)) ThreeCompVisualisation(agent) # exit loop if end is reached @@ -46,13 +52,7 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(phase, t, h, g)) break - # keep track of times and states at phase ends - ts.append(t) - hts.append(h) - gts.append(g) - - # now confirm with iterative agent - for i, t in enumerate(ts): + # now confirm with iterative agent # set to initial state agent.reset() agent.set_h(h_s) @@ -64,16 +64,17 @@ def tte_test_procedure(p, hz, eps, conf, agent, log_level=0): agent.perform_one_step() # estimate estimation differences - g_diff = agent.get_g() - gts[i] - h_diff = agent.get_h() - hts[i] + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h if log_level >= 2: - print("error phase {}. h is off by {}".format(i + 1, h_diff)) - print("error phase {}. g is off by {}".format(i + 1, g_diff)) - - assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(i + 1, g_diff) - assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(i + 1, h_diff) + logging.info("error phase {}. h is off by {}".format(phase, h_diff)) + logging.info("error phase {}. g is off by {}".format(phase, g_diff)) + logging.info("ITERATIVE".format(phase, t)) + ThreeCompVisualisation(agent) + assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(phase, g_diff) + assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(phase, h_diff) def the_loop(p: float = 350.0, hz: int = 250, @@ -84,33 +85,9 @@ def the_loop(p: float = 350.0, while True: udp = MultiObjectiveThreeCompUDP(None, None) - example_conf = udp.create_educated_initial_guess() logging.info(example_conf) - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) - - # ThreeCompVisualisation(agent) - tte_test_procedure(p, hz, eps, example_conf, agent) - - -def test_one_config(example_conf=None): - """ - tests given configuration and puts out some more details - """ - - # just a default value - if example_conf is None: - example_conf = [25925.53993526785, 60694.43170965706, 219.8524740824735, 216.83073328159165, - 26.323382867622215, 0.15040127238313122, 0.28924204946747195, 0.1762119589774433] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) - tte_test_procedure(p, hz, eps, example_conf, agent, log_level=2) + tte_test_procedure(p, hz, eps, example_conf) if __name__ == "__main__": @@ -118,12 +95,14 @@ def test_one_config(example_conf=None): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p = 450 + p = 560 # estimations per second for discrete agent - hz = 250 + hz = 500 # required precision of discrete to differential agent eps = 0.001 - test_one_config() + example_conf = [5000, 65296.55506048172, 286.92487794015625, 287.8170222313081, 38.355175636958336, + 0.4377894638957991, 0.2437516643675469, 0.48005517184947655] + tte_test_procedure(p, hz, eps, example_conf, log_level=2) the_loop(p=p, hz=hz, eps=eps) From 1f815579f5f09c9bcf6dd3b8d19d07fe4f96379a Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 22 Sep 2021 18:42:37 +1000 Subject: [PATCH 39/71] add computation time comparison --- .../simulator/ode_three_comp_hyd_simulator.py | 27 +++++++ tests/comparison_tests.py | 77 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 tests/comparison_tests.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index b9b8d56..f82688e 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -1,3 +1,4 @@ +import logging import math import numpy as np @@ -10,6 +11,32 @@ class ODEThreeCompHydSimulator: # precision epsilon for threshold checks eps = 0.000001 + max_time = 5000 + + @staticmethod + def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: float) -> float: + + t_max = ODEThreeCompHydSimulator.max_time + + # Start with first time to exhaustion bout + tte_1, h, g = ODEThreeCompHydSimulator.tte(conf=conf, + start_h=0, start_g=0, + p_exp=p_exp, t_max=t_max) + if tte_1 >= t_max: + logging.info("Exhaustion not reached during TTE") + return 0 + + # now recovery + rec, h, g = ODEThreeCompHydSimulator.rec(conf=conf, + start_h=h, start_g=g, + p_rec=p_rec, t_max=t_rec) + + # and work bout two + tte_2, h, g = ODEThreeCompHydSimulator.tte(conf=conf, + start_h=h, start_g=g, + p_exp=p_exp, t_max=t_max) + + return tte_2 / tte_1 * 100.0 @staticmethod def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): diff --git a/tests/comparison_tests.py b/tests/comparison_tests.py new file mode 100644 index 0000000..de9d167 --- /dev/null +++ b/tests/comparison_tests.py @@ -0,0 +1,77 @@ +import time + +import numpy as np +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.simulator.three_comp_hyd_simulator import ThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator + +import logging + + +def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, conf, log_level=0): + # create three component hydraulic agent with example configuration + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], + m_ans=conf[3], m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + + # simulator step limit needs to be adjusted + ThreeCompHydSimulator.step_limit = t_max * hz + est_t0 = time.process_time_ns() + est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, p_rec=p_rec, t_rec=t_rec) + est_t = time.process_time_ns() - est_t0 + ThreeCompHydSimulator.step_limit = t_max / hz + + ode_t0 = time.process_time_ns() + rec_ratio = ODEThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(conf=conf, p_exp=p_exp, p_rec=p_rec, t_rec=t_rec) + ode_t = time.process_time_ns() - ode_t0 + + # rec ratio estimation difference + e_diff = abs(est_ratio - rec_ratio) + # >0 if ODE faster + t_diff = est_t - ode_t + return t_diff, e_diff + + +def the_loop(t_max: float = 5000, hz: int = 250): + """ + creates random agents and tests the discretised against the differential one + """ + + e_results = [] + t_results = [] + n = 1000 + for i in range(n): + udp = MultiObjectiveThreeCompUDP(None, None) + + example_conf = udp.create_educated_initial_guess() + + p_exp = example_conf[3] * 3 * abs(np.random.randn()) + p_rec = example_conf[3] * 0.5 * abs(np.random.randn()) + t_rec = 240 * abs(np.random.randn()) + + logging.info("{}/{} Pwork {} Prec {} Trec {} conf {}".format(i, n, p_exp, p_rec, t_rec, example_conf)) + + try: + t_diff, e_diff = rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + hz=hz, conf=example_conf, log_level=0) + e_results.append(e_diff) + t_results.append(t_diff) + except UserWarning: + continue + + logging.info("\nsuccessful samples: {}\ntime performance: {}\nest error {}".format( + len(t_results), np.mean(t_results), np.mean(e_results))) + + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + # estimations per second for discrete agent + hz = 20 + t_max = 5000 + + the_loop(t_max=t_max, hz=hz) From 3782c005c681c3b4e9ac6cf196feeab42daf9b62 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 23 Sep 2021 17:08:01 +1000 Subject: [PATCH 40/71] equation optimizations and restructuring of recovery phases --- .../evolutionary_fitter/three_comp_tools.py | 2 +- .../simulator/ode_three_comp_hyd_simulator.py | 316 ++++++++++-------- tests/configurations.py | 18 +- tests/rec_a6_trial.py | 5 +- tests/rec_phase_tests.py | 134 +++++--- 5 files changed, 266 insertions(+), 209 deletions(-) diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index d147404..248bbfe 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -14,7 +14,7 @@ "m_ae": [1, 2000], "m_ans": [1, 2000], "m_anf": [1, 2000], - "theta": [0.01, 0.99], + "theta": [0.01, 0.99], # 0.0 and 1.0 are not possible because equations would divide by 0 "gamma": [0.01, 0.99], "phi": [0.01, 0.99] } diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index f82688e..065ec68 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -368,16 +368,20 @@ def work_lAe_fAns(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float gamma = conf[6] phi = conf[7] + # in work phases h is assumed to continuously increase + # and therefore this phase ends at 1-phi + h_target = 1 - phi + # this phase is not applicable if phi is greater or equal to gamma... # ... or h is already below pipe exit Ae - if phi >= gamma or h_s >= 1 - phi: + if phi >= gamma or h_s >= h_target: return t_s, h_s, g_s - # g(t4) = g4 can be solved for c + # g(t_s) = g_s can be solved for c s_cg = (g_s - (1 - theta - gamma)) * np.exp((m_ans * t_s) / ((1 - theta - gamma) * a_ans)) + # generalised g(t) def gt(t): - # generalised g(t) for phase A5 return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) # as defined for EQ(21) @@ -386,13 +390,13 @@ def gt(t): g = p_exp / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - # find c that matches h(t4) = h4 + # find c that matches h(t_s) = h_s s_ch = (h_s + b / ((a + k) * np.exp(k) ** t_s) + g / a) / np.exp(a) ** t_s def ht(t): return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a - h_target = 1 - phi + # find the time at which the phase stops t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), initial_guess=t_s, max_steps=t_max) @@ -419,15 +423,17 @@ def work_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float gamma = conf[6] phi = conf[7] + h_target = 1.0 + # this phase is not applicable if Ae or AnS are directly at the bottom of the model if phi == 0.0 or gamma == 0.0: return t_s, h_s, g_s - # g(t5) = gt5 can be solved for c + # g(t_s) = g_s can be solved for c s_cg = (g_s - (1 - theta - gamma)) / np.exp(-m_ans * t_s / ((1 - theta - gamma) * a_ans)) + # generalised g(t) def gt(t): - # generalised g(t) for phase A6 return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) k = m_ans / ((1 - theta - gamma) * a_ans) @@ -436,14 +442,13 @@ def gt(t): # g = p / a_anf ag = (p_exp - m_ae) / a_anf - # h(t5) = ht5 can be solved for c + # h(t_s) = h_s can be solved for c s_ch = -t_s * ag + ((b * math.exp(-k * t_s)) / k) + h_s + # generalised h(t) def ht(t): - # generalised h(t) for phase A6 return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - h_target = 1.0 # find end of phase A6. The time point where h(t_end)=1 t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), initial_guess=t_s, @@ -454,38 +459,39 @@ def ht(t): def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: float = 5000.0) -> ( float, float, float): - # now iterate through all recovery phases - phases = [ODEThreeCompHydSimulator.rec_a6, - ODEThreeCompHydSimulator.rec_a5, - ODEThreeCompHydSimulator.rec_a4_r1, - ODEThreeCompHydSimulator.rec_a4_r2, - ODEThreeCompHydSimulator.rec_a3_r1, - ODEThreeCompHydSimulator.rec_a3_r2, - ODEThreeCompHydSimulator.rec_a2, - ODEThreeCompHydSimulator.rec_a1] + # all recovery phases in order + phases = [ODEThreeCompHydSimulator.rec_fAe_fAnS, + ODEThreeCompHydSimulator.rec_lAe_fAnS, + ODEThreeCompHydSimulator.rec_fAe_lAnS, + ODEThreeCompHydSimulator.rec_fAe_rAnS, + ODEThreeCompHydSimulator.rec_lAe_lAnS, + ODEThreeCompHydSimulator.rec_lAe_rAnS, + ODEThreeCompHydSimulator.rec_fAe, + ODEThreeCompHydSimulator.rec_lAe] # start time from 0 and given start fill level t = 0 h, g = start_h, start_g - # iterate through all phases until end is reached - for phase in phases: - t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) + # cycle through phases until t_max is reached + while t < t_max: + for phase in phases: + t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) - # if recovery time is reached return fill levels at that point - if t == t_max: - return t, h, g + # if recovery time is reached return fill levels at that point + if t >= t_max: + return t, h, g # if all phases complete full recovery is reached return t, h, g @staticmethod - def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_max: float, conf: list): + def rec_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): """ recovery from exhaustive exercise. - :param t6: time in seconds at which recovery starts - :param h6: depletion state of AnF when recovery starts - :param g6: depletion state of AnS when recovery starts + :param t_s: time in seconds at which recovery starts + :param h_s: depletion state of AnF when recovery starts + :param g_s: depletion state of AnS when recovery starts :param p_rec: constant recovery intensity :param t_max: the maximal recovery time :param conf: hydraulic model configuration @@ -500,52 +506,48 @@ def rec_a6(t6: float, h6: float, g6: float, p_rec: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # no recovery possible - if p_rec >= m_ae: - return np.inf, 1.0, 1.0 - # A6 rec ends either at beginning of A4 or A5 h_target = max(1 - gamma, 1 - phi) # check whether phase is applicable or if h is # already above the end of the phase - if not h6 > h_target: - return t6, h6, g6 + if h_s < h_target - ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s - # g(t6) = g6 can be solved for c - s_cg = (g6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) + # g(t_s) = g_s can be solved for c + s_cg = (g_s - (1 - theta - gamma)) / np.exp(-m_ans * t_s / ((1 - theta - gamma) * a_ans)) - def a6_gt(t): - # generalised g(t) for phase A6 - return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + # generalised g(t) + def gt(t): + return (1 - theta - gamma) + s_cg * np.exp(-m_ans * t / ((1 - theta - gamma) * a_ans)) k = m_ans / ((1 - theta - gamma) * a_ans) # a = -m_ae / a_anf - b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) + b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) # g = p / a_anf ag = (p_rec - m_ae) / a_anf - # h(t6) = 1 can be solved for c - s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + h6 + # h(t_s) = h_s can be solved for c + s_ch = -t_s * ag + b * math.exp(-k * t_s) / k + h_s - def a6_ht(t): - # generalised h(t) for recovery phase A6 - return t * ag - ((b * math.exp(-k * t)) / k) + s_ch + # generalised h(t) + def ht(t): + return t * ag - b * math.exp(-k * t) / k + s_ch # estimate an initial guess that assumes no contribution from g - initial_guess = t6 - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a6_ht(t) - h_target, - initial_guess=initial_guess, + t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - h_target, + initial_guess=t_s, max_steps=t_max) - return t_end, a6_ht(t_end), a6_gt(t_end) + h_end = min(ht(t_end), 1.0) + return t_end, h_end, gt(t_end) @staticmethod - def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_max: float, conf: list): + def rec_lAe_fAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): """ recovery from exhaustive exercise. - :param t5: time in seconds at which recovery starts - :param h5: depletion state of AnF when recovery starts - :param g5: depletion state of AnS when recovery starts + :param t_s: time in seconds at which recovery starts + :param h_s: depletion state of AnF when recovery starts + :param g_s: depletion state of AnS when recovery starts :param p_rec: constant recovery intensity :param t_max: the maximal recovery time :param conf: hydraulic model configuration @@ -560,19 +562,20 @@ def rec_a5(t5: float, h5: float, g5: float, p_rec: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # A5 always ends when h reaches pipe exit of AnS + # in this recovery phase h is assumed to continuously increase + # this recovery phase ends when h reaches pipe exit of AnS h_target = 1 - gamma - # check whether phase is applicable or if h is - # already above the end of the phase - if not h5 > h_target: - return t5, h5, g5 + # this phase is not applicable if phi is greater or equal to gamma... + # ... or if h is already above the end of the phase + if phi >= gamma or h_s <= h_target - ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s - # g(t5) = g5 can be solved for c - s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) + # g(t_s) = g_s can be solved for c + s_cg = (g_s - (1 - theta - gamma)) * np.exp((m_ans * t_s) / ((1 - theta - gamma) * a_ans)) - def a5_gt(t): - # generalised g(t) for phase A5 + # generalised g(t) + def gt(t): return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) # as defined for EQ(21) @@ -581,22 +584,26 @@ def a5_gt(t): g = p_rec / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - # find c that matches h(t5) = h5 - s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 + # find c that matches h(t_s) = h_s + s_ch = (h_s + b / (a + k) * np.exp(-k * t_s) + g / a) * np.exp(-a * t_s) - def a5_ht(t): - return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a + def ht(t): + return -b / (a + k) * np.exp(-k * t) + s_ch * np.exp(a * t) - g / a # find the time at which the phase stops - initial_guess = t5 - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a5_ht(t) - h_target, - initial_guess=initial_guess, + t_ht = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - h_target, + initial_guess=t_s, + max_steps=t_max) + + # phase also ends if h drops back to 1-phi changing lAe into fAe + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: (1 - phi) - ht(t), + initial_guess=t_s, max_steps=t_max) - # return with fill levels at that time - return t_end, a5_ht(t_end), a5_gt(t_end) + t_end = min(t_ht, t_phi) + return t_end, ht(t_end), gt(t_end) @staticmethod - def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_max: float, conf: list): + def rec_fAe_lAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -606,22 +613,24 @@ def rec_a4_r1(t4: float, h4: float, g4: float, p_rec: float, t_max: float, conf: gamma = conf[6] phi = conf[7] - # A4 R1 is only applicable h below pipe exit of Ae ... + # phase full Ae and limited AnS is only applicable h below pipe exit of Ae + # ... and above AnS ... # ... and if g is above h (allows error of epsilon) - if not h4 > 1 - phi \ - or not h4 > g4 + theta + ODEThreeCompHydSimulator.eps: - return t4, h4, g4 + if h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ + or h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ + or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s # if g is above h (flow from AnS into AnF) a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) b_gh = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) # derivative g'(t4) can be calculated manually - dgt4_gh = m_ans * (h4 - g4 - theta) / (a_ans * (1 - theta - gamma)) + dgt4_gh = m_ans * (h_s - g_s - theta) / (a_ans * (1 - theta - gamma)) # ... which then allows to derive c1 and c2 - s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) - s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + g4 + s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t_s) + s_c2_gh = (-t_s * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + g_s def a4_gt(t): # general solution for g(t) @@ -635,25 +644,30 @@ def a4_ht(t): # EQ(9) with constants for g(t) and g'(t) return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - initial_guess = t4 + initial_guess = t_s # phase ends when g drops below h... - tgth = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (theta + a4_gt(t)), - initial_guess=initial_guess, - max_steps=t_max) + t_gth = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (theta + a4_gt(t)), + initial_guess=initial_guess, + max_steps=t_max) - # ...or if h reaches 1-phi - tphi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (1 - phi), - initial_guess=initial_guess, - max_steps=t_max) + # ...or if h reaches 1-phi changing fAe into lAe + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (1 - phi), + initial_guess=initial_guess, + max_steps=t_max) + + # ...or if h drops to 1-gamma changing lAnS into fAnS + t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - a4_ht(t), + initial_guess=initial_guess, + max_steps=t_max) # choose minimal time at which this phase ends - t_end = min(tphi, tgth) + t_end = min(t_phi, t_gth, t_gam) return t_end, a4_ht(t_end), a4_gt(t_end) @staticmethod - def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_max: float, conf: list): + def rec_fAe_rAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -665,20 +679,20 @@ def rec_a4_r2(t4: float, h4: float, g4: float, p_rec: float, t_max: float, conf: # A4 R2 is only applicable if h below pipe exit of Ae... # ... and h is above g (error of epsilon tolerated) - if not h4 > 1 - phi or \ - not h4 < g4 + theta + ODEThreeCompHydSimulator.eps: - return t4, h4, g4 + if h_s <= 1 - phi or \ + h_s >= g_s + theta + ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s # if h is above g simulate flow from AnF into AnS a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) # derivative g'(t4) can be calculated manually from g4, t4, and h4 - dgt4_hg = - m_anf * (g4 + theta - h4) / (a_ans * (1 - gamma)) + dgt4_hg = - m_anf * (g_s + theta - h_s) / (a_ans * (1 - gamma)) # which then allows to derive c1 and c2 - s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t4) - s_c2_gh = (-t4 * b_hg + dgt4_hg) / a_hg - (p_rec - m_ae) / ((a_anf + a_ans) * a_hg) + g4 + s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t_s) + s_c2_gh = (-t_s * b_hg + dgt4_hg) / a_hg - (p_rec - m_ae) / ((a_anf + a_ans) * a_hg) + g_s def a4_gt(t): # general solution for g(t) @@ -696,12 +710,12 @@ def a4_ht(t): # A4 ends if AnS is completely refilled t_fill = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_gt(t), - initial_guess=t4, + initial_guess=t_s, max_steps=t_max) # A4 also ends by surpassing 1 - phi t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - h_target, - initial_guess=t4, + initial_guess=t_s, max_steps=t_max) # choose minimal time at which phase ends @@ -710,7 +724,7 @@ def a4_ht(t): return t_end, a4_ht(t_end), a4_gt(t_end) @staticmethod - def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: list): + def rec_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -720,11 +734,13 @@ def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: gamma = conf[6] phi = conf[7] - # A3 R1 is only applicable if h is above or at pipe exit of Ae... + # lAe and lAnS is only applicable if h is above or at pipe exit of Ae... + # ... and above or at pipe exit AnS ... # ... and g is above h (error of epsilon tolerated) - if not h3 <= 1 - phi + ODEThreeCompHydSimulator.eps \ - or not h3 > g3 + theta + ODEThreeCompHydSimulator.eps: - return t3, h3, g3 + if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ + or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ + or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s # my simplified form a = m_ae / (a_anf * (1 - phi)) + \ @@ -745,11 +761,11 @@ def rec_a3_r1(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: # uses Al dt/dl part of EQ(8) solved for c2 # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) # and then substituted in EQ(14) and solved for c1 - s_c1 = (c / b + (m_ans * (h3 - g3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) - g3) / \ - (np.exp(r1 * t3) * (r1 / r2 - 1)) + s_c1 = (c / b + (m_ans * (h_s - g_s - theta)) / (a_ans * r2 * (1 - theta - gamma)) - g_s) / \ + (np.exp(r1 * t_s) * (r1 / r2 - 1)) # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (g3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) + s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) def a3_gt(t): # the general solution for g(t) @@ -761,16 +777,26 @@ def a3_ht(t): k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - # phase A3 R1 only ends if h(t) rises above g(t) - # find the point where h(t) == g(t) - # As h(t3) is assumed to be below g(t3) and AnS is limited, this point must be reached - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_ht(t) - (a3_gt(t) + theta), - initial_guess=t3, + # find the point where h(t) == g(t) ... + t_gh = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_ht(t) - (a3_gt(t) + theta), + initial_guess=t_s, + max_steps=t_max) + + # ... or where h(t) drops back to 1 - gamma, changing lAnS to fAnS + t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - a3_ht(t), + initial_guess=t_s, max_steps=t_max) + + # ... or where h(t) drops back to 1 - phi, changing lAe to fAe + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - a3_ht(t), + initial_guess=t_s, + max_steps=t_max) + + t_end = min(t_gh, t_gam, t_phi) return t_end, a3_ht(t_end), a3_gt(t_end) @staticmethod - def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: list): + def rec_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -783,10 +809,10 @@ def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: # A3 R2 is only applicable if h is above or at pipe exit of Ae... # ... and h is above g (error of epsilon tolerated)... # ... and g is not 0 - if not h3 <= 1 - phi + ODEThreeCompHydSimulator.eps \ - or not h3 < g3 + theta + ODEThreeCompHydSimulator.eps \ - or not g3 > 0.0: - return t3, h3, g3 + if not h_s <= 1 - phi + ODEThreeCompHydSimulator.eps \ + or not h_s < g_s + theta + ODEThreeCompHydSimulator.eps \ + or not g_s > 0.0: + return t_s, h_s, g_s # EQ 16 and 17 substituted in EQ 8 a = m_ae / (a_anf * (1 - phi)) + \ @@ -805,11 +831,11 @@ def rec_a3_r2(t3: float, h3: float, g3: float, p_rec: float, t_max: float, conf: # uses Al dt/dl part of EQ(16) == dl/dt of EQ(14) solved for c2 # and then substituted in EQ(14) and solved for c1 - s_c1 = (c / b - (m_anf * (g3 + theta - h3)) / (a_ans * r2 * (1 - gamma)) - g3) / \ - (np.exp(r1 * t3) * (r1 / r2 - 1)) + s_c1 = (c / b - (m_anf * (g_s + theta - h_s)) / (a_ans * r2 * (1 - gamma)) - g_s) / \ + (np.exp(r1 * t_s) * (r1 / r2 - 1)) # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (g3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) + s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) def a3_gt(t): # the general solution for g(t) @@ -827,62 +853,53 @@ def a3_ht(t): else: # find the point where g(t) == 0 t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_gt(t), - initial_guess=t3, + initial_guess=t_s, max_steps=t_max) return t_end, a3_ht(t_end), a3_gt(t_end) @staticmethod - def rec_a2(t2: float, h2: float, g2: float, p_rec: float, t_max: float, conf: list): + def rec_fAe(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] m_ae = conf[2] phi = conf[7] - # Recovery phase A2 is only applicable if h is below pipe exit of Ae and AnS is full - if 1 - phi > h2 or g2 > ODEThreeCompHydSimulator.eps: - return t2, h2, g2 + # full Ae recovery is only applicable if h is below pipe exit of Ae and AnS is full + if 1 - phi > h_s or g_s > ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s def a2_ht(t): - return h2 - (t - t2) * (m_ae - p_rec) / a_anf + return h_s - (t - t_s) * (m_ae - p_rec) / a_anf # the total duration of recovery phase A2 from t2 on - t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + t2 + t_end = (h_s - 1 + phi) * a_anf / (m_ae - p_rec) + t_s # check if targeted recovery time is before phase end time t_end = min(t_end, t_max) - return t_end, a2_ht(t_end), g2 + return t_end, a2_ht(t_end), g_s @staticmethod - def rec_a1(t1: float, h1: float, g1: float, p_rec: float, t_max: float, conf: list): + def rec_lAe(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): a_anf = conf[0] m_ae = conf[2] phi = conf[7] - def a1_ht(t): - return (h1 - p_rec * (1 - phi) / m_ae) * \ - np.exp(m_ae * (t1 - t) / (a_anf * (1 - phi))) + \ + # pure lAe recovery is only possible if AnS is full and fill-level AnF above AnS + if h_s > g_s or g_s > ODEThreeCompHydSimulator.eps: + return t_s, h_s, g_s + + def ht(t): + return (h_s - p_rec * (1 - phi) / m_ae) * \ + np.exp(m_ae * (t_s - t) / (a_anf * (1 - phi))) + \ p_rec * (1 - phi) / m_ae # full recovery can't be reached as log(inf) only approximates 0 # a1_ht(max rec time) is the most recovery possible - target_h = a1_ht(t_max) - - return t_max, target_h, g1 - - # # return min(h) if even after the maximal recovery time h>epsilon - # if target_h > ODEThreeCompHydSimulator.eps: - # return t_rec, target_h, g1 - # - # # otherwise return time when h(t) reaches approximately 0 (epsilon) - # t_end = a_anf * (1 - phi) / - m_ae * ( - # np.log(target_h - p_rec * (1 - phi) / m_ae) - ( - # np.log(h1 - p_rec * (1 - phi) / m_ae) + m_ae * t1 / (a_anf * (1 - phi)) - # ) - # ) - # - # return t_end, a1_ht(t_end), g1 + target_h = ht(t_max) + + return t_max, target_h, g_s @staticmethod def optimize(func, initial_guess, max_steps): @@ -899,7 +916,10 @@ def optimize(func, initial_guess, max_steps): # check if initial guess conforms to underlying optimizer assumption t = initial_guess if func(t) < 0: - raise UserWarning("initial guess for func is not positive") + if func(t + ODEThreeCompHydSimulator.eps) >= 0: + t += ODEThreeCompHydSimulator.eps + else: + raise UserWarning("initial guess for func is not positive") # while maximal precision is not reached while step_size > 0.0000001: diff --git a/tests/configurations.py b/tests/configurations.py index 9de309f..454501d 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -25,6 +25,16 @@ 252.71702367096787, 363.2970828395908, # m_ae, m_ans 380.27073086773415, 0.64892228099402588, # m_anf, theta 0.1580228306857272, 0.6580228306857272] # gamma, phi +# a configuration where AnS has size of 1.0 +e = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.0, # m_anf, theta + 0.0, 0.6580228306857272] # gamma, phi +# a configuration where AnF has size of 1.0 +f = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.3, # m_anf, theta + 0.3, 0.0] # gamma, phi if __name__ == "__main__": # set logging level to highest level @@ -35,10 +45,10 @@ eps = 0.001 # required precision # setting combinations - p_exps = [260, 300, 421, 681] - rec_times = [10, 180, 240, 1800, 3600] - p_recs = [0, 13, 57, 130, 247] - configs = [a, b, c, d] + p_exps = [260, 681] + rec_times = [10, 240, 3600] + p_recs = [0, 247] + configs = [a, b, c, d, e, f] combs = list(itertools.product(p_exps, rec_times, p_recs, configs)) diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py index 305e8c8..afcaf4e 100644 --- a/tests/rec_a6_trial.py +++ b/tests/rec_a6_trial.py @@ -17,13 +17,14 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") p_exp = 350 - p_rec = 100 + p_rec = 247 t_max = 5000 # estimations per second for discrete agent hz = 250 - conf = configurations.d + conf = [5604.966588001499, 54499.44673416602, 155.82060702780947, 105.26777135234472, 28.623478621476917, + 0.2314852496176266, 0.35438323467786853, 0.5949904604992432] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 0ff07ed..391878b 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -8,14 +8,35 @@ import numpy as np -def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_level=0): +def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, hz: int, eps: float, + conf: list, log_level: int = 0): + """ + Conducts a TTE and follows it up with a recovery at given conditions. Estimation results of the ODE integral + estimations are compared to differential results of the interative agent. + :param p_exp: intensity for TTE + :param p_rec: intensity for recovery + :param t_rec: duration of recovery + :param t_max: maximal duration of exercise or recovery bout + :param hz: delta t for iterative agent + :param eps: error tolerance for difference between differential and integral agent estimations + :param conf: configuration of hydraulic model to investigate + :param log_level: amount of detail about results to be logged + """ + + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], + m_ans=conf[3], m_anf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) + if log_level > 0: + logging.info("Agent to be examined") + ThreeCompVisualisation(agent) + # Start with first time to exhaustion bout - t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, t_max=t_max) + t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, start_h=0, start_g=0, conf=conf, t_max=t_max) - if t == np.inf or int(t) == int(t_max): + if t >= t_max: logging.info("Exhaustion not reached during TTE") return - + # double-check with discrete agent for _ in range(int(round(t * hz))): agent.set_power(p_exp) @@ -25,44 +46,60 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, agent, log_le assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) - # now iterate through all recovery phases - phases = [ODEThreeCompHydSimulator.rec_a6, - ODEThreeCompHydSimulator.rec_a5, - ODEThreeCompHydSimulator.rec_a4_r1, - ODEThreeCompHydSimulator.rec_a4_r2, - ODEThreeCompHydSimulator.rec_a3_r1, - ODEThreeCompHydSimulator.rec_a3_r2, - ODEThreeCompHydSimulator.rec_a2, - ODEThreeCompHydSimulator.rec_a1] + # display fill levels at exhaustion + if log_level > 0: + logging.info("[SUCCESS] Agent after TTE at {}".format(p_exp)) + ThreeCompVisualisation(agent) + + # all recovery phases in order + phases = [ODEThreeCompHydSimulator.rec_fAe_fAnS, + ODEThreeCompHydSimulator.rec_lAe_fAnS, + ODEThreeCompHydSimulator.rec_fAe_lAnS, + ODEThreeCompHydSimulator.rec_fAe_rAnS, + ODEThreeCompHydSimulator.rec_lAe_lAnS, + ODEThreeCompHydSimulator.rec_lAe_rAnS, + ODEThreeCompHydSimulator.rec_fAe, + ODEThreeCompHydSimulator.rec_lAe] # restart time from 0 t = 0 - # detailed checks for every phase - for phase in phases: - # save previous time to estimate time difference - t_p = t + # cycles through phases until t_max is reached + while t < t_rec: + # detailed checks for every phase + for phase in phases: + # save previous time to estimate time difference + t_p = t - # get estimated time of phase end - t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_rec, conf=conf) - # logging.info("{}\nt {}\nh {}\ng {}".format(phase, t, h, g)) + # get estimated time of phase end + t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_rec, conf=conf) - # double-check with discrete agent - for _ in range(int(round((t - t_p) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - g - h_diff = agent.get_h() - h + # double-check with discrete agent + for _ in range(int(round((t - t_p) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h - # ThreeCompVisualisation(agent) + if log_level > 1: + logging.info("[intermediate result] {}\n" + "t {}\n" + "Diff h {}\n" + "Diff g {}".format(phase, t, h_diff, g_diff)) + ThreeCompVisualisation(agent) - assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) - assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) + assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) + assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) - if t == t_max: - logging.info("Max recovery reached in {}".format(phase)) - # ThreeCompVisualisation(agent) - return + if t >= t_rec: + logging.info("Max recovery reached in {}".format(phase)) + # ThreeCompVisualisation(agent) + break + + # display fill levels after recovery + if log_level > 0: + logging.info("[SUCCESS] Agent after recovery at {} for {}".format(p_rec, t_rec)) + ThreeCompVisualisation(agent) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, @@ -71,19 +108,13 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, creates random agents and tests the discretised against the differential one """ + # create random examples for all eternity while True: udp = MultiObjectiveThreeCompUDP(None, None) - example_conf = udp.create_educated_initial_guess() logging.info(example_conf) - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=example_conf[0], a_ans=example_conf[1], m_ae=example_conf[2], - m_ans=example_conf[3], m_anf=example_conf[4], the=example_conf[5], - gam=example_conf[6], phi=example_conf[7]) - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, - hz=hz, eps=eps, conf=example_conf, - agent=agent, log_level=2) + hz=hz, eps=eps, conf=example_conf) if __name__ == "__main__": @@ -91,26 +122,21 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 560 - t_rec = 180 - p_rec = 0 + p_exp = 681 + t_rec = 240 + p_rec = 247 t_max = 5000 # estimations per second for discrete agent - hz = 250 + hz = 400 # required precision of discrete to differential agent eps = 0.001 # a configuration - c = [17530.530747393303, 37625.72364566721, 268.7372285266482, 223.97570400889148, - 7.895654547752743, 0.1954551343626819, 0.224106497474462, 0.01] - agent = ThreeCompHydAgent(hz=hz, a_anf=c[0], a_ans=c[1], m_ae=c[2], - m_ans=c[3], m_anf=c[4], the=c[5], - gam=c[6], phi=c[7]) - ThreeCompVisualisation(agent) - + c = [8829.88215919767, 82481.84418801148, 49.81378596925795, + 303.3894463054843, 35.98410481440918, 0.3635160544196956, + 0.12797386833725893, 0.5904013251667336] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, - hz=hz, eps=eps, conf=c, - agent=agent, log_level=2) + hz=hz, eps=eps, conf=c, log_level=2) the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) From 47123e8ecb2f104ad25a7fc81e08115ff8058d71 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 27 Sep 2021 12:00:57 +1000 Subject: [PATCH 41/71] finish first version of ODE simulations and complete preparations for evolutionary application --- .../simulator/ode_three_comp_hyd_simulator.py | 5 +++-- tests/comparison_tests.py | 2 +- tests/rec_phase_tests.py | 11 +++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 065ec68..c02b7a9 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -885,9 +885,10 @@ def rec_lAe(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf a_anf = conf[0] m_ae = conf[2] phi = conf[7] + theta = conf[5] # pure lAe recovery is only possible if AnS is full and fill-level AnF above AnS - if h_s > g_s or g_s > ODEThreeCompHydSimulator.eps: + if h_s > g_s + theta or g_s > ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s def ht(t): @@ -911,7 +912,7 @@ def optimize(func, initial_guess, max_steps): """ # start precision - step_size = 1000.0 + step_size = 10.0 # check if initial guess conforms to underlying optimizer assumption t = initial_guess diff --git a/tests/comparison_tests.py b/tests/comparison_tests.py index de9d167..ae4f777 100644 --- a/tests/comparison_tests.py +++ b/tests/comparison_tests.py @@ -71,7 +71,7 @@ def the_loop(t_max: float = 5000, hz: int = 250): format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # estimations per second for discrete agent - hz = 20 + hz = 2 t_max = 5000 the_loop(t_max=t_max, hz=hz) diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 391878b..aa860f3 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -122,9 +122,9 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 681 - t_rec = 240 - p_rec = 247 + p_exp = 1564.0873818561276 + t_rec = 292.13680140868223 + p_rec = 34.48319621014474 t_max = 5000 # estimations per second for discrete agent @@ -133,9 +133,8 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, eps = 0.001 # a configuration - c = [8829.88215919767, 82481.84418801148, 49.81378596925795, - 303.3894463054843, 35.98410481440918, 0.3635160544196956, - 0.12797386833725893, 0.5904013251667336] + c = [5000, 59569.291128903234, 289.31173195530033, 392.8945983599032, 32.828298294434475, + 0.29340973592585345, 0.28556577283926454, 0.5543380822791537] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) From cb30f960119be54ce132ffd4274a6d2b12f44d3b Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 6 Oct 2021 17:42:44 +1100 Subject: [PATCH 42/71] Move over changes from evo branch --- .../simulator/ode_three_comp_hyd_simulator.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index c02b7a9..b555c74 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -23,8 +23,8 @@ def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: fl start_h=0, start_g=0, p_exp=p_exp, t_max=t_max) if tte_1 >= t_max: - logging.info("Exhaustion not reached during TTE") - return 0 + # Exhaustion not reached during TTE + return 200 # now recovery rec, h, g = ODEThreeCompHydSimulator.rec(conf=conf, @@ -58,7 +58,7 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = p_exp=p_exp, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point - if t == np.inf or t == t_max: + if t == np.nan or t >= t_max: return t, h, g # if all phases complete full exhaustion is reached @@ -260,8 +260,9 @@ def work_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float phi = conf[7] # This phase is not applicable h is lower than pipe exit Ae... - # ... or if reflow into AnS is happening - if h_s > (1 - phi) or g_s + theta > h_s + ODEThreeCompHydSimulator.eps: + # ... or if reflow into AnS is happening ... + # ... or if AnS flow is at full + if h_s > (1 - phi) or g_s + theta > h_s + ODEThreeCompHydSimulator.eps or h_s > (1 - gamma): return t_s, h_s, g_s # taken from Equation 11 by Morton 1986 @@ -434,7 +435,7 @@ def work_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float # generalised g(t) def gt(t): - return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) + return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) k = m_ans / ((1 - theta - gamma) * a_ans) # a = -m_ae / a_anf @@ -443,11 +444,11 @@ def gt(t): ag = (p_exp - m_ae) / a_anf # h(t_s) = h_s can be solved for c - s_ch = -t_s * ag + ((b * math.exp(-k * t_s)) / k) + h_s + s_ch = -t_s * ag + ((b * np.exp(-k * t_s)) / k) + h_s # generalised h(t) def ht(t): - return t * ag - ((b * math.exp(-k * t)) / k) + s_ch + return t * ag - ((b * np.exp(-k * t)) / k) + s_ch # find end of phase A6. The time point where h(t_end)=1 t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), @@ -479,7 +480,7 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point - if t >= t_max: + if t == np.nan or t >= t_max: return t, h, g # if all phases complete full recovery is reached From 044e499ea9e0e8b7c1ca0e6783ab93dc53b571cb Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 7 Oct 2021 11:48:01 +1100 Subject: [PATCH 43/71] generalize lAe to rec and tte together --- .../simulator/ode_three_comp_hyd_simulator.py | 61 +++++++------------ tests/configurations.py | 4 +- tests/rec_phase_tests.py | 14 ++--- tests/rec_trial_tests.py | 6 +- tests/tte_phase_tests.py | 13 ++-- 5 files changed, 41 insertions(+), 57 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index b555c74..746dc10 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -41,7 +41,7 @@ def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: fl @staticmethod def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): - phases = [ODEThreeCompHydSimulator.work_lAe, + phases = [ODEThreeCompHydSimulator.lAe, ODEThreeCompHydSimulator.work_lAe_rAnS, ODEThreeCompHydSimulator.work_fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, @@ -54,8 +54,7 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = t, h, g = 0, start_h, start_g # iterate through all phases until end is reached for phase in phases: - t, h, g = phase(t, h, g, - p_exp=p_exp, t_max=t_max, conf=conf) + t, h, g = phase(t, h, g, p_exp, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point if t == np.nan or t >= t_max: @@ -65,7 +64,10 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = return t, h, g @staticmethod - def work_lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> (float, float, float): + """ + The phase l_Ae where only Ae contributes and flow through p_Ae is limited by liquid pressure. + """ a_anf = conf[0] m_ae = conf[2] @@ -73,27 +75,31 @@ def work_lAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, con phi = conf[7] # phase ends at bottom of Ae or top of AnS - h_target = min(theta, 1 - phi) + h_bottom = min(theta, 1 - phi) # This phase is not applicable if fill-level of AnF below pipe exit Ae or top of AnS, .. - # ... or AnS not full - if h_s > h_target or g_s > 0.0 + ODEThreeCompHydSimulator.eps: + # ... or AnS not full ... + # ... or pipe exit of Ae is at the top of the model + if h_s >= h_bottom + ODEThreeCompHydSimulator.eps \ + or g_s > ODEThreeCompHydSimulator.eps \ + or phi == 0: return t_s, h_s, g_s # constant can be derived from known t_s and h_s - c1 = (h_s - p_exp * (1 - phi) / m_ae) * np.exp(-m_ae * t_s / a_anf * (phi - 1)) + c1 = (h_s - p * (1 - phi) / m_ae) * np.exp(-m_ae * t_s / (a_anf * (phi - 1))) # general solution for h(t) using c1 - def ht(t): - return p_exp * (1 - phi) / m_ae + c1 * np.exp(m_ae * t / (a_anf * (phi - 1))) + ht_max = p * (1 - phi) / m_ae + c1 * np.exp(m_ae * t_max / (a_anf * (phi - 1))) + # ht_max = ODEThreeCompHydSimulator.lAe_ht(t_max, p, c1, a_anf, m_ae, phi) # check if max time is reached in this phase - if ht(t_max) <= h_target: - return t_max, ht(t_max), g_s + # for example this will always happen during recovery as in h, log(inf) only approximates 0 + if ht_max <= h_bottom: + return t_max, ht_max, g_s else: # end of phase A1 -> the time when h(t) = min(theta,1-phi) - t_end = a_anf * (phi - 1) / m_ae * np.log((h_target - p_exp * (1 - phi) / m_ae) / c1) - return t_end, h_target, g_s + t_end = a_anf * (phi - 1) / m_ae * np.log((h_bottom - p * (1 - phi) / m_ae) / c1) + return t_end, h_bottom, g_s @staticmethod def work_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( @@ -468,7 +474,7 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f ODEThreeCompHydSimulator.rec_lAe_lAnS, ODEThreeCompHydSimulator.rec_lAe_rAnS, ODEThreeCompHydSimulator.rec_fAe, - ODEThreeCompHydSimulator.rec_lAe] + ODEThreeCompHydSimulator.lAe] # start time from 0 and given start fill level t = 0 @@ -477,7 +483,7 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f # cycle through phases until t_max is reached while t < t_max: for phase in phases: - t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_max, conf=conf) + t, h, g = phase(t, h, g, p_rec, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point if t == np.nan or t >= t_max: @@ -880,29 +886,6 @@ def a2_ht(t): return t_end, a2_ht(t_end), g_s - @staticmethod - def rec_lAe(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - - a_anf = conf[0] - m_ae = conf[2] - phi = conf[7] - theta = conf[5] - - # pure lAe recovery is only possible if AnS is full and fill-level AnF above AnS - if h_s > g_s + theta or g_s > ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - def ht(t): - return (h_s - p_rec * (1 - phi) / m_ae) * \ - np.exp(m_ae * (t_s - t) / (a_anf * (1 - phi))) + \ - p_rec * (1 - phi) / m_ae - - # full recovery can't be reached as log(inf) only approximates 0 - # a1_ht(max rec time) is the most recovery possible - target_h = ht(t_max) - - return t_max, target_h, g_s - @staticmethod def optimize(func, initial_guess, max_steps): """ diff --git a/tests/configurations.py b/tests/configurations.py index 454501d..c9a6ca4 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -30,7 +30,7 @@ 252.71702367096787, 363.2970828395908, # m_ae, m_ans 380.27073086773415, 0.0, # m_anf, theta 0.0, 0.6580228306857272] # gamma, phi -# a configuration where AnF has size of 1.0 +# a configuration where Ae has size of 1.0 f = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 380.27073086773415, 0.3, # m_anf, theta @@ -48,7 +48,7 @@ p_exps = [260, 681] rec_times = [10, 240, 3600] p_recs = [0, 247] - configs = [a, b, c, d, e, f] + configs = [a, b, c, d] # e, f] combs = list(itertools.product(p_exps, rec_times, p_recs, configs)) diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index aa860f3..9cbc237 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -59,7 +59,7 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, ODEThreeCompHydSimulator.rec_lAe_lAnS, ODEThreeCompHydSimulator.rec_lAe_rAnS, ODEThreeCompHydSimulator.rec_fAe, - ODEThreeCompHydSimulator.rec_lAe] + ODEThreeCompHydSimulator.lAe] # restart time from 0 t = 0 @@ -72,7 +72,7 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, t_p = t # get estimated time of phase end - t, h, g = phase(t, h, g, p_rec=p_rec, t_max=t_rec, conf=conf) + t, h, g = phase(t, h, g, p_rec, t_max=t_rec, conf=conf) # double-check with discrete agent for _ in range(int(round((t - t_p) * hz))): @@ -122,9 +122,9 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 1564.0873818561276 - t_rec = 292.13680140868223 - p_rec = 34.48319621014474 + p_exp = 560 + t_rec = 180 + p_rec = 0 t_max = 5000 # estimations per second for discrete agent @@ -133,8 +133,8 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, eps = 0.001 # a configuration - c = [5000, 59569.291128903234, 289.31173195530033, 392.8945983599032, 32.828298294434475, - 0.29340973592585345, 0.28556577283926454, 0.5543380822791537] + c = [8307.733355384593, 83908.04796664482, 174.9214061687359, 413.34459434994994, 29.1778756437821, + 0.3486671398769143, 0.01, 0.803980915503534] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index a8b596a..07b0e02 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -111,13 +111,13 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, t_max = 5000 # estimations per second for discrete agent - hz = 800 + hz = 2000 # required precision of discrete to differential agent eps = 0.01 # a configuration - c = [5000, 65296.55506048172, 286.92487794015625, 287.8170222313081, 38.355175636958336, - 0.4377894638957991, 0.2437516643675469, 0.48005517184947655] + c = [8307.733355384593, 83908.04796664482, 174.9214061687359, 413.34459434994994, 29.1778756437821, + 0.35, 0.01, 0.8] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index b53cfe5..bf3f399 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -15,7 +15,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): m_ans=conf[3], m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - phases = [ODEThreeCompHydSimulator.work_lAe, + phases = [ODEThreeCompHydSimulator.lAe, ODEThreeCompHydSimulator.work_lAe_rAnS, ODEThreeCompHydSimulator.work_fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, @@ -25,8 +25,8 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): ODEThreeCompHydSimulator.work_fAe_fAnS] # set initial conditions - h_s = 0.0423121637107437 - g_s = 0.061937224354866705 # 1 - conf[6] - conf[5] + h_s = 0.015047877356186012 + g_s = 0.12055688716004659 # 1 - conf[6] - conf[5] t, h, g = 0, h_s, g_s # display initial state if log level is high enough @@ -37,7 +37,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): # iterate through all phases until end is reached for phase in phases: - t, h, g = phase(t, h, g, p_exp=p, t_max=max_time, conf=conf) + t, h, g = phase(t, h, g, p, t_max=max_time, conf=conf) # display intermediate state if log level is high enough if log_level > 0: @@ -101,8 +101,9 @@ def the_loop(p: float = 350.0, # required precision of discrete to differential agent eps = 0.001 - example_conf = [5000, 65296.55506048172, 286.92487794015625, 287.8170222313081, 38.355175636958336, - 0.4377894638957991, 0.2437516643675469, 0.48005517184947655] + example_conf = [8307.733355384593, 83908.04796664482, 174.9214061687359, + 413.34459434994994, 29.1778756437821, + 0.3486671398769143, 0.01, 0.803980915503534] tte_test_procedure(p, hz, eps, example_conf, log_level=2) the_loop(p=p, hz=hz, eps=eps) From cbab340693230f2d401092b109ce15a46af9e6da Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 7 Oct 2021 14:58:56 +1100 Subject: [PATCH 44/71] combine lAe_rAn --- .../simulator/ode_three_comp_hyd_simulator.py | 77 +++---------------- tests/rec_phase_tests.py | 2 +- tests/tte_phase_tests.py | 12 +-- 3 files changed, 16 insertions(+), 75 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 746dc10..5503cfd 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -42,7 +42,7 @@ def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: fl def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.work_lAe_rAnS, + ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.work_fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, ODEThreeCompHydSimulator.work_lAe_lAnS, @@ -102,7 +102,7 @@ def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) return t_end, h_bottom, g_s @staticmethod - def work_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( float, float, float): a_anf = conf[0] @@ -115,11 +115,14 @@ def work_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float # This phase is not applicable if fill-level of AnF below pipe exit Ae, .. # ... or AnS fill-level is above AnF fill-level ... - # ... or AnS is full - if h_s > 1 - phi or g_s + theta <= h_s or g_s <= 0.0 + ODEThreeCompHydSimulator.eps: + # ... or AnS is full ... + # ... or pipe exit of Ae is at the top of the model + if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ + or h_s >= g_s + theta + ODEThreeCompHydSimulator.eps \ + or g_s <= ODEThreeCompHydSimulator.eps \ + or phi == 0: return t_s, h_s, g_s - # TODO: mostly copied from rec A3R2 find ways to combine equations # EQ 16 and 17 substituted in EQ 8 a = m_ae / (a_anf * (1 - phi)) + \ m_anf / (a_ans * (1 - gamma)) + \ @@ -472,8 +475,8 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f ODEThreeCompHydSimulator.rec_fAe_lAnS, ODEThreeCompHydSimulator.rec_fAe_rAnS, ODEThreeCompHydSimulator.rec_lAe_lAnS, - ODEThreeCompHydSimulator.rec_lAe_rAnS, ODEThreeCompHydSimulator.rec_fAe, + ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.lAe] # start time from 0 and given start fill level @@ -802,68 +805,6 @@ def a3_ht(t): t_end = min(t_gh, t_gam, t_phi) return t_end, a3_ht(t_end), a3_gt(t_end) - @staticmethod - def rec_lAe_rAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # A3 R2 is only applicable if h is above or at pipe exit of Ae... - # ... and h is above g (error of epsilon tolerated)... - # ... and g is not 0 - if not h_s <= 1 - phi + ODEThreeCompHydSimulator.eps \ - or not h_s < g_s + theta + ODEThreeCompHydSimulator.eps \ - or not g_s > 0.0: - return t_s, h_s, g_s - - # EQ 16 and 17 substituted in EQ 8 - a = m_ae / (a_anf * (1 - phi)) + \ - m_anf / (a_ans * (1 - gamma)) + \ - m_anf / (a_anf * (1 - gamma)) - - b = m_ae * m_anf / \ - (a_anf * a_ans * (1 - phi) * (1 - gamma)) - - c = m_anf * (p_rec * (1 - phi) - m_ae * theta) / \ - (a_anf * a_ans * (1 - phi) * (1 - gamma)) - - # solutions for l''(t) + a*l'(t) + b*l(t) = c - r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) - r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - - # uses Al dt/dl part of EQ(16) == dl/dt of EQ(14) solved for c2 - # and then substituted in EQ(14) and solved for c1 - s_c1 = (c / b - (m_anf * (g_s + theta - h_s)) / (a_ans * r2 * (1 - gamma)) - g_s) / \ - (np.exp(r1 * t_s) * (r1 / r2 - 1)) - - # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) - - def a3_gt(t): - # the general solution for g(t) - return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b - - # substitute into EQ(9) for h - def a3_ht(t): - k1 = a_ans * (1 - gamma) / m_anf * s_c1 * r1 + s_c1 - k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 - return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - - # Recovery phase A3R2 only ends if AnS is refilled - if a3_gt(t_max) > 0: - return t_max, a3_ht(t_max), a3_gt(t_max) - else: - # find the point where g(t) == 0 - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_gt(t), - initial_guess=t_s, - max_steps=t_max) - return t_end, a3_ht(t_end), a3_gt(t_end) - @staticmethod def rec_fAe(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 9cbc237..61582e2 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -57,8 +57,8 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, ODEThreeCompHydSimulator.rec_fAe_lAnS, ODEThreeCompHydSimulator.rec_fAe_rAnS, ODEThreeCompHydSimulator.rec_lAe_lAnS, - ODEThreeCompHydSimulator.rec_lAe_rAnS, ODEThreeCompHydSimulator.rec_fAe, + ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.lAe] # restart time from 0 diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index bf3f399..869a661 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -16,7 +16,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): gam=conf[6], phi=conf[7]) phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.work_lAe_rAnS, + ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.work_fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, ODEThreeCompHydSimulator.work_lAe_lAnS, @@ -25,8 +25,8 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): ODEThreeCompHydSimulator.work_fAe_fAnS] # set initial conditions - h_s = 0.015047877356186012 - g_s = 0.12055688716004659 # 1 - conf[6] - conf[5] + h_s = 0 + g_s = 0 # 1 - conf[6] - conf[5] t, h, g = 0, h_s, g_s # display initial state if log level is high enough @@ -101,9 +101,9 @@ def the_loop(p: float = 350.0, # required precision of discrete to differential agent eps = 0.001 - example_conf = [8307.733355384593, 83908.04796664482, 174.9214061687359, - 413.34459434994994, 29.1778756437821, - 0.3486671398769143, 0.01, 0.803980915503534] + example_conf = [16440.760749341924, 39844.68845444773, 205.95359029932902, + 248.79961885239118, 25.86948744313801, 0.4773848980363584, + 0.453981977895603, 0.31563995769439257] tte_test_procedure(p, hz, eps, example_conf, log_level=2) the_loop(p=p, hz=hz, eps=eps) From 0c39f3d7fec5434224baec5aaa51992efbb46440 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 7 Oct 2021 15:55:54 +1100 Subject: [PATCH 45/71] combine fAe --- .../simulator/ode_three_comp_hyd_simulator.py | 49 ++++++++----------- tests/rec_a2_trial.py | 22 +++------ tests/rec_phase_tests.py | 6 +-- tests/tte_phase_tests.py | 7 ++- 4 files changed, 34 insertions(+), 50 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 5503cfd..0c8c742 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -43,7 +43,7 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = phases = [ODEThreeCompHydSimulator.lAe, ODEThreeCompHydSimulator.lAe_rAnS, - ODEThreeCompHydSimulator.work_fAe, + ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, ODEThreeCompHydSimulator.work_lAe_lAnS, ODEThreeCompHydSimulator.work_fAe_lAnS, @@ -176,7 +176,7 @@ def ht(t): return t_end, ht(t_end), gt(t_end) @staticmethod - def work_fAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> (float, float, float): + def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> (float, float, float): a_anf = conf[0] m_ae = conf[2] @@ -185,17 +185,30 @@ def work_fAe(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, con # this phase not applicable if h is not in-between 1-theta and phi and ... # ... AnS is not full - if not theta >= h_s >= (1 - phi) or g_s > 0.0 + ODEThreeCompHydSimulator.eps: + if theta + ODEThreeCompHydSimulator.eps < h_s < 1 - phi - ODEThreeCompHydSimulator.eps or \ + g_s > ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s + # the first derivative. Change in ht + ht_p = (p - m_ae) / a_anf + + if ht_p > 0: + # expenditure + h_target = theta + elif ht_p < 0: + # recovery ends at 1 - phi + h_target = 1 - phi + else: + # no change + h_target = h_s + # linear utilization -> no equilibrium possible - t_end = t_s + ((phi - (1 - theta)) * a_anf) / (p_exp - m_ae) + t_end = (h_target - h_s) * a_anf / (p - m_ae) + t_s # check if max time is reached before phase end if t_end > t_max: - h_end = h_s + (t_max - t_s) * (p_exp - m_ae) / a_anf + h_end = h_s + (t_max - t_s) * ht_p return t_end, h_end, g_s - else: return t_end, theta, g_s @@ -475,7 +488,7 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f ODEThreeCompHydSimulator.rec_fAe_lAnS, ODEThreeCompHydSimulator.rec_fAe_rAnS, ODEThreeCompHydSimulator.rec_lAe_lAnS, - ODEThreeCompHydSimulator.rec_fAe, + ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.lAe] @@ -805,28 +818,6 @@ def a3_ht(t): t_end = min(t_gh, t_gam, t_phi) return t_end, a3_ht(t_end), a3_gt(t_end) - @staticmethod - def rec_fAe(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - - a_anf = conf[0] - m_ae = conf[2] - phi = conf[7] - - # full Ae recovery is only applicable if h is below pipe exit of Ae and AnS is full - if 1 - phi > h_s or g_s > ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - def a2_ht(t): - return h_s - (t - t_s) * (m_ae - p_rec) / a_anf - - # the total duration of recovery phase A2 from t2 on - t_end = (h_s - 1 + phi) * a_anf / (m_ae - p_rec) + t_s - - # check if targeted recovery time is before phase end time - t_end = min(t_end, t_max) - - return t_end, a2_ht(t_end), g_s - @staticmethod def optimize(func, initial_guess, max_steps): """ diff --git a/tests/rec_a2_trial.py b/tests/rec_a2_trial.py index cc67c9a..7dd5551 100644 --- a/tests/rec_a2_trial.py +++ b/tests/rec_a2_trial.py @@ -4,6 +4,7 @@ import logging import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation if __name__ == "__main__": @@ -29,25 +30,18 @@ m_ae=conf[2], m_ans=conf[3], m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) + phi = conf[7] t2 = 0 h2 = 0.5 g2 = 0.0 - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - - def a2_ht(t): - return h2 - t * (m_ae - p_rec) / a_anf - - t_end = (h2 - 1 + phi) * a_anf / (m_ae - p_rec) + t_end, _, _ = ODEThreeCompHydSimulator.fAe(t_s=t2, + h_s=h2, + g_s=g2, + p=p_rec, + t_max=1000, + conf=conf) # check in simulation agent.reset() diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 61582e2..7b8c861 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -57,7 +57,7 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, ODEThreeCompHydSimulator.rec_fAe_lAnS, ODEThreeCompHydSimulator.rec_fAe_rAnS, ODEThreeCompHydSimulator.rec_lAe_lAnS, - ODEThreeCompHydSimulator.rec_fAe, + ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.lAe] @@ -133,8 +133,8 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, eps = 0.001 # a configuration - c = [8307.733355384593, 83908.04796664482, 174.9214061687359, 413.34459434994994, 29.1778756437821, - 0.3486671398769143, 0.01, 0.803980915503534] + c = [21704.77778915587, 61925.84797188902, 212.76772005473063, 140.0897845828814, 32.4028329961532, + 0.3217431159932008, 0.2683727457040581, 0.7190401470030847] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 869a661..11b8591 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -17,7 +17,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): phases = [ODEThreeCompHydSimulator.lAe, ODEThreeCompHydSimulator.lAe_rAnS, - ODEThreeCompHydSimulator.work_fAe, + ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, ODEThreeCompHydSimulator.work_lAe_lAnS, ODEThreeCompHydSimulator.work_fAe_lAnS, @@ -101,9 +101,8 @@ def the_loop(p: float = 350.0, # required precision of discrete to differential agent eps = 0.001 - example_conf = [16440.760749341924, 39844.68845444773, 205.95359029932902, - 248.79961885239118, 25.86948744313801, 0.4773848980363584, - 0.453981977895603, 0.31563995769439257] + example_conf = [21704.77778915587, 61925.84797188902, 212.76772005473063, 140.0897845828814, 32.4028329961532, + 0.3217431159932008, 0.2683727457040581, 0.7190401470030847] tte_test_procedure(p, hz, eps, example_conf, log_level=2) the_loop(p=p, hz=hz, eps=eps) From a57fdd2d3991e9c87453853f4addd2c0e24c2542 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 7 Oct 2021 17:19:10 +1100 Subject: [PATCH 46/71] combine lAe_lAn and apply some fAe fixes --- .../simulator/ode_three_comp_hyd_simulator.py | 78 +++++++++++++------ tests/{rec_a2_trial.py => rec_fAe_trial.py} | 34 ++++---- tests/rec_phase_tests.py | 16 ++-- tests/rec_trial_tests.py | 10 +-- tests/tte_phase_tests.py | 4 +- 5 files changed, 85 insertions(+), 57 deletions(-) rename tests/{rec_a2_trial.py => rec_fAe_trial.py} (60%) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 0c8c742..cb3043a 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -42,10 +42,10 @@ def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: fl def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.lAe_rAnS, + ODEThreeCompHydSimulator.lAe_rAn, ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, - ODEThreeCompHydSimulator.work_lAe_lAnS, + ODEThreeCompHydSimulator.lAe_lAnS, ODEThreeCompHydSimulator.work_fAe_lAnS, ODEThreeCompHydSimulator.work_lAe_fAns, ODEThreeCompHydSimulator.work_fAe_fAnS] @@ -85,24 +85,29 @@ def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) or phi == 0: return t_s, h_s, g_s + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 + # constant can be derived from known t_s and h_s c1 = (h_s - p * (1 - phi) / m_ae) * np.exp(-m_ae * t_s / (a_anf * (phi - 1))) # general solution for h(t) using c1 ht_max = p * (1 - phi) / m_ae + c1 * np.exp(m_ae * t_max / (a_anf * (phi - 1))) - # ht_max = ODEThreeCompHydSimulator.lAe_ht(t_max, p, c1, a_anf, m_ae, phi) # check if max time is reached in this phase # for example this will always happen during recovery as in h, log(inf) only approximates 0 if ht_max <= h_bottom: - return t_max, ht_max, g_s + return t_p + t_max, ht_max, g_s else: # end of phase A1 -> the time when h(t) = min(theta,1-phi) t_end = a_anf * (phi - 1) / m_ae * np.log((h_bottom - p * (1 - phi) / m_ae) / c1) - return t_end, h_bottom, g_s + return t_p + t_end, h_bottom, g_s @staticmethod - def lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def lAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( float, float, float): a_anf = conf[0] @@ -123,6 +128,12 @@ def lAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, con or phi == 0: return t_s, h_s, g_s + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 + # EQ 16 and 17 substituted in EQ 8 a = m_ae / (a_anf * (1 - phi)) + \ m_anf / (a_ans * (1 - gamma)) + \ @@ -173,11 +184,14 @@ def ht(t): # phase ends at the earliest of these time points t_end = min(g0, gtht, h_end) - return t_end, ht(t_end), gt(t_end) + return t_p + t_end, ht(t_end), gt(t_end) @staticmethod def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> (float, float, float): - + """ + The phase f_Ae where only Ae contributes and flow through p_Ae is at maximal capacity m_Ae. This phase is linear + and does not allow an equilibrium because power p is assumed to be constant. + """ a_anf = conf[0] m_ae = conf[2] theta = conf[5] @@ -185,8 +199,9 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) # this phase not applicable if h is not in-between 1-theta and phi and ... # ... AnS is not full - if theta + ODEThreeCompHydSimulator.eps < h_s < 1 - phi - ODEThreeCompHydSimulator.eps or \ - g_s > ODEThreeCompHydSimulator.eps: + if h_s > theta + ODEThreeCompHydSimulator.eps \ + or h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ + or g_s > ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s # the first derivative. Change in ht @@ -210,7 +225,7 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) h_end = h_s + (t_max - t_s) * ht_p return t_end, h_end, g_s else: - return t_end, theta, g_s + return t_end, h_target, g_s @staticmethod def work_fAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( @@ -270,7 +285,7 @@ def ht(t): return t_end, ht(t_end), gt(t_end) @staticmethod - def work_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def lAe_lAnS(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( float, float, float): a_anf = conf[0] @@ -281,10 +296,12 @@ def work_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float gamma = conf[6] phi = conf[7] - # This phase is not applicable h is lower than pipe exit Ae... - # ... or if reflow into AnS is happening ... - # ... or if AnS flow is at full - if h_s > (1 - phi) or g_s + theta > h_s + ODEThreeCompHydSimulator.eps or h_s > (1 - gamma): + # lAe and lAnS is only applicable if h is above or at pipe exit of Ae... + # ... and above or at pipe exit AnS ... + # ... and g is above h (error of epsilon tolerated) + if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ + or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ + or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s # taken from Equation 11 by Morton 1986 @@ -299,18 +316,23 @@ def work_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float b = m_ae * m_ans / \ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - c = m_ans * (p_exp * (1 - phi) - m_ae * theta) / \ + c = m_ans * (p * (1 - phi) - m_ae * theta) / \ (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) + # uses Al dt/dl part of EQ(8) solved for c2 + # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) + # and then substituted in EQ(14) and solved for c1 + # ... or ... # uses Al dt/dl part of EQ(9) == dl/dt of EQ(12) solved for c2 # and then substituted in EQ(12) and solved for c1 s_c1 = (g_s - m_ans * (h_s - g_s - theta) / (a_ans * r2 * (1 - theta - gamma)) - c / b) / ( np.exp(r1 * t_s) * (1 - r1 / r2)) # uses EQ(12) with solution for c1 and solves for c2 + # ... or uses EQ(14) with solution for c1 and solves for c2 s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) def gt(t): @@ -323,12 +345,20 @@ def ht(t): k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - # if phi > gamma, then phase A3 transitions into phase A4 before AnS is empty - h_target = 1 - max(phi, gamma) + # find the point where h(t) == g(t) ... + t_gh = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - (gt(t) + theta), + initial_guess=t_s, + max_steps=t_max) - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), - initial_guess=t_s, - max_steps=t_max) + # ... or where h(t) drops back to 1 - gamma, changing lAnS to fAnS + t_g = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - ht(t), + initial_guess=t_s, + max_steps=t_max) + # ... or where h(t) drops back to 1 - phi, changing lAe to fAe + t_p = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - ht(t), + initial_guess=t_s, + max_steps=t_max) + t_end = min(t_gh, t_g, t_p) return t_end, ht(t_end), gt(t_end) @staticmethod @@ -487,9 +517,9 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f ODEThreeCompHydSimulator.rec_lAe_fAnS, ODEThreeCompHydSimulator.rec_fAe_lAnS, ODEThreeCompHydSimulator.rec_fAe_rAnS, - ODEThreeCompHydSimulator.rec_lAe_lAnS, + ODEThreeCompHydSimulator.lAe_lAnS, + ODEThreeCompHydSimulator.lAe_rAn, ODEThreeCompHydSimulator.fAe, - ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.lAe] # start time from 0 and given start fill level diff --git a/tests/rec_a2_trial.py b/tests/rec_fAe_trial.py similarity index 60% rename from tests/rec_a2_trial.py rename to tests/rec_fAe_trial.py index 7dd5551..3cb2a80 100644 --- a/tests/rec_a2_trial.py +++ b/tests/rec_fAe_trial.py @@ -12,17 +12,15 @@ logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 350 + p_exp = 260 p_rec = 0 # estimations per second for discrete agent - hz = 250 + hz = 1000 # a D configuration - conf = [15101.24769778409, 86209.27743067988, # anf, ans - 252.71702367096787, 363.2970828395908, # m_ae, m_ans - 38.27073086773415, 0.64892228099402588, # m_anf, theta - 0.1580228306857272, 0.6580228306857272] # gamma, phi + conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, + 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, @@ -32,16 +30,16 @@ gam=conf[6], phi=conf[7]) phi = conf[7] - t2 = 0 - h2 = 0.5 - g2 = 0.0 + t2 = 2643.143819400002 + h2 = 0.025100624266976845 + g2 = 4.883399240540598e-12 - t_end, _, _ = ODEThreeCompHydSimulator.fAe(t_s=t2, - h_s=h2, - g_s=g2, - p=p_rec, - t_max=1000, - conf=conf) + t_end, h_end, g_end = ODEThreeCompHydSimulator.fAe(t_s=t2, + h_s=h2, + g_s=g2, + p=p_rec, + t_max=5000, + conf=conf) # check in simulation agent.reset() @@ -50,12 +48,12 @@ ThreeCompVisualisation(agent) agent.set_power(p_rec) - for _ in range(int(t_end * agent.hz)): + for _ in range(int((t_end - t2) * agent.hz)): agent.perform_one_step() logging.info("predicted time: {} \n" "diff h: {}\n" "diff g: {}".format(t_end, - (1 - phi) - agent.get_h(), - 0 - agent.get_g())) + h_end - agent.get_h(), + g_end - agent.get_g())) ThreeCompVisualisation(agent) diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 7b8c861..9d3d58d 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -56,9 +56,9 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, ODEThreeCompHydSimulator.rec_lAe_fAnS, ODEThreeCompHydSimulator.rec_fAe_lAnS, ODEThreeCompHydSimulator.rec_fAe_rAnS, - ODEThreeCompHydSimulator.rec_lAe_lAnS, + ODEThreeCompHydSimulator.lAe_rAn, + ODEThreeCompHydSimulator.lAe_lAnS, ODEThreeCompHydSimulator.fAe, - ODEThreeCompHydSimulator.lAe_rAnS, ODEThreeCompHydSimulator.lAe] # restart time from 0 @@ -84,8 +84,9 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, if log_level > 1: logging.info("[intermediate result] {}\n" "t {}\n" + "h {} g {}\n" "Diff h {}\n" - "Diff g {}".format(phase, t, h_diff, g_diff)) + "Diff g {}".format(phase, t, h, g, h_diff, g_diff)) ThreeCompVisualisation(agent) assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) @@ -122,19 +123,18 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 560 - t_rec = 180 + p_exp = 260 + t_rec = 3600 p_rec = 0 t_max = 5000 - # estimations per second for discrete agent hz = 400 # required precision of discrete to differential agent eps = 0.001 # a configuration - c = [21704.77778915587, 61925.84797188902, 212.76772005473063, 140.0897845828814, 32.4028329961532, - 0.3217431159932008, 0.2683727457040581, 0.7190401470030847] + c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, + 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 07b0e02..00a23c3 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -105,9 +105,9 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 560 - t_rec = 180 - p_rec = 0 + p_exp = 681 + t_rec = 3600 + p_rec = 247 t_max = 5000 # estimations per second for discrete agent @@ -116,8 +116,8 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, eps = 0.01 # a configuration - c = [8307.733355384593, 83908.04796664482, 174.9214061687359, 413.34459434994994, 29.1778756437821, - 0.35, 0.01, 0.8] + c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, + 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 11b8591..9007241 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -16,10 +16,10 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): gam=conf[6], phi=conf[7]) phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.lAe_rAnS, + ODEThreeCompHydSimulator.lAe_rAn, ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.work_fAe_rAnS, - ODEThreeCompHydSimulator.work_lAe_lAnS, + ODEThreeCompHydSimulator.lAe_lAnS, ODEThreeCompHydSimulator.work_fAe_lAnS, ODEThreeCompHydSimulator.work_lAe_fAns, ODEThreeCompHydSimulator.work_fAe_fAnS] From 1eaf44cbb114da26c31e5e31af42559a0cb913b9 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 11 Oct 2021 17:55:48 +1100 Subject: [PATCH 47/71] [WIP] new phase tests and detailed test procedure --- .../simulator/ode_three_comp_hyd_simulator.py | 410 +++++++++++++----- tests/phase_fAe.py | 76 ++++ tests/phase_lAe.py | 42 ++ tests/phase_lAe_rAn.py | 49 +++ tests/rec_a1_trial.py | 95 ---- tests/rec_a3_r1_trial.py | 115 ----- tests/rec_fAe_trial.py | 59 --- tests/rec_phase_tests.py | 10 +- tests/rec_trial_tests.py | 12 +- tests/tte_phase_tests.py | 92 ++-- 10 files changed, 538 insertions(+), 422 deletions(-) create mode 100644 tests/phase_fAe.py create mode 100644 tests/phase_lAe.py create mode 100644 tests/phase_lAe_rAn.py delete mode 100644 tests/rec_a1_trial.py delete mode 100644 tests/rec_a3_r1_trial.py delete mode 100644 tests/rec_fAe_trial.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index cb3043a..f97b0c1 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -41,30 +41,59 @@ def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: fl @staticmethod def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): - phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.lAe_rAn, - ODEThreeCompHydSimulator.fAe, - ODEThreeCompHydSimulator.work_fAe_rAnS, - ODEThreeCompHydSimulator.lAe_lAnS, - ODEThreeCompHydSimulator.work_fAe_lAnS, - ODEThreeCompHydSimulator.work_lAe_fAns, - ODEThreeCompHydSimulator.work_fAe_fAnS] - # start with fully reset agent t, h, g = 0, start_h, start_g - # iterate through all phases until end is reached - for phase in phases: - t, h, g = phase(t, h, g, p_exp, t_max=t_max, conf=conf) + + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + while t < t_max: + # first distinguish between fAe and lAe + if h >= 1 - phi: + # fAe + if h <= theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe + # fAe_rAnS + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe_rAn + # fAe_lAnS + elif h >= g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_lAn + # fAe_fAnS + elif h >= 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, + phi)) + else: + # lAr + if h <= theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe_rAn + elif h >= g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_lAn + elif h >= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, + phi)) + + # iterate through all phases until end is reached + t, h, g = func(t, h, g, p_exp, t_max=t_max, conf=conf) # if recovery time is reached return fill levels at that point - if t == np.nan or t >= t_max: + if t == np.nan: return t, h, g # if all phases complete full exhaustion is reached return t, h, g @staticmethod - def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> (float, float, float): + def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): """ The phase l_Ae where only Ae contributes and flow through p_Ae is limited by liquid pressure. """ @@ -80,10 +109,10 @@ def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) # This phase is not applicable if fill-level of AnF below pipe exit Ae or top of AnS, .. # ... or AnS not full ... # ... or pipe exit of Ae is at the top of the model - if h_s >= h_bottom + ODEThreeCompHydSimulator.eps \ - or g_s > ODEThreeCompHydSimulator.eps \ - or phi == 0: - return t_s, h_s, g_s + # if h_s > h_bottom + ODEThreeCompHydSimulator.eps \ + # or g_s > ODEThreeCompHydSimulator.eps \ + # or phi == 0: + # return t_s, h_s, g_s # in some exp equations values exceed float capacity if t gets too large # Therefore, t_s is set to 0 and added later @@ -100,15 +129,20 @@ def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) # check if max time is reached in this phase # for example this will always happen during recovery as in h, log(inf) only approximates 0 if ht_max <= h_bottom: - return t_p + t_max, ht_max, g_s + return t_p + t_max, ht_max, g_s, None else: - # end of phase A1 -> the time when h(t) = min(theta,1-phi) + # end of phase lAe -> the time when h(t) = min(theta,1-phi) t_end = a_anf * (phi - 1) / m_ae * np.log((h_bottom - p * (1 - phi) / m_ae) / c1) - return t_p + t_end, h_bottom, g_s + if h_bottom == theta: + # phase transfers into lAe_lAn + func = ODEThreeCompHydSimulator.lAe_lAn + else: + # phase transfers int fAe + func = ODEThreeCompHydSimulator.fAe + return t_p + t_end, h_bottom, g_s, func @staticmethod - def lAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( - float, float, float): + def lAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -122,13 +156,13 @@ def lAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf # ... or AnS fill-level is above AnF fill-level ... # ... or AnS is full ... # ... or pipe exit of Ae is at the top of the model - if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ - or h_s >= g_s + theta + ODEThreeCompHydSimulator.eps \ - or g_s <= ODEThreeCompHydSimulator.eps \ - or phi == 0: - return t_s, h_s, g_s + # if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ + # or h_s > g_s + theta + ODEThreeCompHydSimulator.eps \ + # or g_s < ODEThreeCompHydSimulator.eps \ + # or phi == 0: + # return t_s, h_s, g_s - # in some exp equations values exceed float capacity if t gets too large + # in some exp equations values exceed float value range if t gets too large # Therefore, t_s is set to 0 and added later t_p = t_s t_max = t_max - t_s @@ -142,7 +176,7 @@ def lAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf b = m_ae * m_anf / \ (a_anf * a_ans * (1 - phi) * (1 - gamma)) - c = m_anf * (p_exp * (1 - phi) - m_ae * theta) / \ + c = m_anf * (p * (1 - phi) - m_ae * theta) / \ (a_anf * a_ans * (1 - phi) * (1 - gamma)) # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c @@ -184,7 +218,18 @@ def ht(t): # phase ends at the earliest of these time points t_end = min(g0, gtht, h_end) - return t_p + t_end, ht(t_end), gt(t_end) + + # determine what the current phase lAe_rAn transitions into + if t_end >= t_max: + func = None + elif t_end == g0: + func = ODEThreeCompHydSimulator.lAe + elif t_end == gtht: + func = ODEThreeCompHydSimulator.lAe_lAn + else: + func = ODEThreeCompHydSimulator.fAe_rAn + + return t_p + t_end, ht(t_end), gt(t_end), func @staticmethod def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> (float, float, float): @@ -199,10 +244,10 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) # this phase not applicable if h is not in-between 1-theta and phi and ... # ... AnS is not full - if h_s > theta + ODEThreeCompHydSimulator.eps \ - or h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ - or g_s > ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s + # if h_s > theta + ODEThreeCompHydSimulator.eps \ + # or h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ + # or g_s > ODEThreeCompHydSimulator.eps: + # return t_s, h_s, g_s # the first derivative. Change in ht ht_p = (p - m_ae) / a_anf @@ -210,27 +255,28 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) if ht_p > 0: # expenditure h_target = theta + func = ODEThreeCompHydSimulator.fAe_lAn elif ht_p < 0: # recovery ends at 1 - phi h_target = 1 - phi + func = ODEThreeCompHydSimulator.lAe else: # no change - h_target = h_s + return t_max, h_s, g_s, None - # linear utilization -> no equilibrium possible + # linear utilization -> no equilibrium possible t_end = (h_target - h_s) * a_anf / (p - m_ae) + t_s # check if max time is reached before phase end if t_end > t_max: h_end = h_s + (t_max - t_s) * ht_p - return t_end, h_end, g_s + return t_max, h_end, g_s, None else: - return t_end, h_target, g_s + return t_end, h_target, g_s, func @staticmethod - def work_fAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def fAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( float, float, float): - # TODO: mostly copied from rec A4R2 find ways to combine equations a_anf = conf[0] a_ans = conf[1] @@ -240,24 +286,30 @@ def work_fAe_rAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float gamma = conf[6] phi = conf[7] - # This phase is not applicable if fill-level of AnF above or at pipe exit Ae, .. + # This phase only starts if is not applicable if fill-level of AnF above or at pipe exit Ae, .. # ... or AnS fill-level is above AnF fill-level ... # ... or AnS is full - if h_s <= 1 - phi - ODEThreeCompHydSimulator.eps or \ - g_s + theta <= h_s or \ - g_s <= 0.0 + ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s + # if h_s < 1 - phi - ODEThreeCompHydSimulator.eps or \ + # h_s > g_s + theta + ODEThreeCompHydSimulator.eps or \ + # g_s < ODEThreeCompHydSimulator.eps: + # return t_s, h_s, g_s + + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 # if h is above g (flow from AnF into AnS) a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) b_hg = (p_exp - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) - # derivative g'(t4) can be calculated manually - dgt4_hg = - m_anf * (g_s + theta - h_s) / (a_ans * (1 - gamma)) + # derivative g'(t) can be calculated manually + dgt_hg = - m_anf * (g_s + theta - h_s) / (a_ans * (1 - gamma)) # which then allows to derive c1 and c2 - s_c1_gh = ((p_exp - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t_s) - s_c2_gh = (-t_s * b_hg + dgt4_hg) / a_hg - (p_exp - m_ae) / ((a_anf + a_ans) * a_hg) + g_s + s_c1_gh = ((p_exp - m_ae) / (a_anf + a_ans) - dgt_hg) * np.exp(a_hg * t_s) + s_c2_gh = (-t_s * b_hg + dgt_hg) / a_hg - (p_exp - m_ae) / ((a_anf + a_ans) * a_hg) + g_s def gt(t): # general solution for g(t) @@ -271,21 +323,37 @@ def ht(t): # EQ(16) with constants for g(t) and g'(t) return a_ans * (1 - gamma) / m_anf * dgt(t) + gt(t) + theta - # find the point where g(t) == 0 - g0 = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t), - initial_guess=t_s, - max_steps=t_max) - - # find the point where fill-levels AnS and AnF are at equal - gtht = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t) + theta - ht(t), + # find the point where AnS is full g(t) == 0 + t_g0 = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t), initial_guess=t_s, max_steps=t_max) - t_end = min(g0, gtht) - return t_end, ht(t_end), gt(t_end) + # find the point where fill-levels AnS and AnF are at equal + t_gtht = ODEThreeCompHydSimulator.optimize(func=lambda t: gt(t) + theta - ht(t), + initial_guess=t_s, + max_steps=t_max) + + # ... fAe_rAn also ends by surpassing 1 - phi + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - (1 - phi), + initial_guess=t_s, + max_steps=t_max) + + t_end = min(t_g0, t_gtht, t_phi) + + # determine what the current phase fAe_rAn transitions into + if t_end >= t_max: + func = None + elif t_end == t_g0: + func = ODEThreeCompHydSimulator.fAe + elif t_end == t_gtht: + func = ODEThreeCompHydSimulator.fAe_lAn + else: + func = ODEThreeCompHydSimulator.lAe_rAn + + return t_end + t_p, ht(t_end), gt(t_end), func @staticmethod - def lAe_lAnS(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( + def lAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( float, float, float): a_anf = conf[0] @@ -299,10 +367,16 @@ def lAe_lAnS(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: l # lAe and lAnS is only applicable if h is above or at pipe exit of Ae... # ... and above or at pipe exit AnS ... # ... and g is above h (error of epsilon tolerated) - if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ - or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ - or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s + # if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ + # or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ + # or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: + # return t_s, h_s, g_s + + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 # taken from Equation 11 by Morton 1986 # a = (m_ae * a_ans * (1 - theta - gamma) + m_ans * (a_anf + a_ans) * (1 - phi)) / ( @@ -350,19 +424,32 @@ def ht(t): initial_guess=t_s, max_steps=t_max) - # ... or where h(t) drops back to 1 - gamma, changing lAnS to fAnS - t_g = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - ht(t), - initial_guess=t_s, - max_steps=t_max) + # ... or where h(t) reaches 1 - gamma, changing lAn to fAn + t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - ht(t), + initial_guess=t_s, + max_steps=t_max) + # ... or where h(t) drops back to 1 - phi, changing lAe to fAe - t_p = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - ht(t), - initial_guess=t_s, - max_steps=t_max) - t_end = min(t_gh, t_g, t_p) - return t_end, ht(t_end), gt(t_end) + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - ht(t), + initial_guess=t_s, + max_steps=t_max) + + t_end = min(t_gh, t_gam, t_phi) + + # Determine what the current phase lAe_lAn transitions into + if t_end >= t_max: + func = None + elif t_end == t_gh: + func = ODEThreeCompHydSimulator.lAe_rAn + elif t_end == t_gam: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + func = ODEThreeCompHydSimulator.fAe_lAn + + return t_end + t_p, ht(t_end), gt(t_end), func @staticmethod - def work_fAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def fAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( float, float, float): a_anf = conf[0] @@ -373,45 +460,85 @@ def work_fAe_lAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float gamma = conf[6] phi = conf[7] - # This phase is not applicable h is not in-between pipe exit Ae and pipe exit AnS... - # ... or if reflow into AnS is happening - if not 1 - phi - ODEThreeCompHydSimulator.eps <= h_s <= 1 - gamma + ODEThreeCompHydSimulator.eps or \ - g_s + theta > h_s + ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s + # phase full Ae and limited AnS is only applicable h below pipe exit of Ae + # ... and above pipe exist of AnS ... + # ... and if g is above h (allows error of epsilon) + # if h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ + # or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ + # or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: + # return t_s, h_s, g_s - # b/a can be simplified as (p-m_ae)/(a_anf + a_ans) - a = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - b = (p_exp - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 - # derivative g'(t3) can be calculated manually - dgt3 = m_ans * (h_s - g_s - theta) / (a_ans * (1 - theta - gamma)) + # if g is above h (flow from AnS into AnF) + a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) + b_gh = (p - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - # which then allows to derive c1 and c2 - s_c1 = ((p_exp - m_ae) / (a_anf + a_ans) - dgt3) * np.exp(a * t_s) - s_c2 = (-t_s * b + dgt3) / a - (p_exp - m_ae) / ((a_anf + a_ans) * a) + g_s + # derivative g'(t) can be calculated manually + dgt_gh = m_ans * (h_s - g_s - theta) / (a_ans * (1 - theta - gamma)) + + # ... which then allows to derive c1 and c2 + s_c1_gh = ((p - m_ae) / (a_anf + a_ans) - dgt_gh) * np.exp(a_gh * t_s) + s_c2_gh = (-t_s * b_gh + dgt_gh) / a_gh - (p - m_ae) / ((a_anf + a_ans) * a_gh) + g_s def gt(t): # general solution for g(t) - return t * (p_exp - m_ae) / (a_anf + a_ans) + s_c2 + s_c1 / a * np.exp(-a * t) + return t * (p - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) def dgt(t): # first derivative g'(t) - return (p_exp - m_ae) / (a_anf + a_ans) - s_c1 * np.exp(-a * t) + return (p - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_gh * t) def ht(t): # EQ(9) with constants for g(t) and g'(t) return a_ans * (1 - theta - gamma) / m_ans * dgt(t) + gt(t) + theta - h_target = 1 - gamma + # phase ends when g == h... + t_gth = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - (theta + gt(t)), + initial_guess=t_s, + max_steps=t_max) - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + # ...or if h drops to 1-gamma changing lAn into fAn + t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - ht(t), initial_guess=t_s, max_steps=t_max) - return t_end, ht(t_end), gt(t_end) + + # ...or if h reaches 1-phi changing fAe into lAe + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - (1 - phi), + initial_guess=t_s, + max_steps=t_max) + + # choose minimal time at which this phase ends + t_end = min(t_phi, t_gth, t_gam) + + # Determine what the current phase fAe_lAn transitions into + if t_end >= t_max: + func = None + elif t_end == t_gth: + func = ODEThreeCompHydSimulator.fAe_rAn + elif t_end == t_gam: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + func = ODEThreeCompHydSimulator.lAe_lAn + + return t_end + t_p, ht(t_end), gt(t_end), func @staticmethod - def work_lAe_fAns(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def lAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( float, float, float): + """ + :param t_s: time in seconds at which phase starts + :param h_s: depletion state of AnF when phase starts + :param g_s: depletion state of AnS when phase starts + :param p: constant intensity + :param t_max: the maximal recovery time + :param conf: hydraulic model configuration + :return: + """ a_anf = conf[0] a_ans = conf[1] @@ -427,8 +554,8 @@ def work_lAe_fAns(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float # this phase is not applicable if phi is greater or equal to gamma... # ... or h is already below pipe exit Ae - if phi >= gamma or h_s >= h_target: - return t_s, h_s, g_s + # if phi >= gamma or h_s >= h_target: + # return t_s, h_s, g_s # g(t_s) = g_s can be solved for c s_cg = (g_s - (1 - theta - gamma)) * np.exp((m_ans * t_s) / ((1 - theta - gamma) * a_ans)) @@ -440,7 +567,7 @@ def gt(t): # as defined for EQ(21) k = m_ans / ((1 - theta - gamma) * a_ans) a = -m_ae / ((1 - phi) * a_anf) - g = p_exp / a_anf + g = p / a_anf b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) # find c that matches h(t_s) = h_s @@ -450,23 +577,37 @@ def ht(t): return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a # find the time at which the phase stops - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - 1 - gamma, initial_guess=t_s, max_steps=t_max) - return t_end, ht(t_end), gt(t_end) + + # phase also ends if h drops back to 1-phi changing lAe into fAe + t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: (1 - phi) - ht(t), + initial_guess=t_s, + max_steps=t_max) + t_end = min(t_gam, t_phi) + + # Determine what the current phase lAe_lAn transitions into + if t_end >= t_max: + func = None + elif t_end == t_gam: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + func = ODEThreeCompHydSimulator.fAe_fAn + + return t_end, ht(t_end), gt(t_end), func @staticmethod - def work_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def fAe_fAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( float, float, float): """ - Final phase before exhaustion. - :param t_s: time at which A5 ended - :param h_s: h(t5) - :param g_s: g(t5) + :param t_s: + :param h_s: + :param g_s: :param p_exp: constant power output :param t_max: maximal time limit :param conf: configuration of hydraulic model - :return: [t_end: time until h=1, h(t_end)=1, g(t_end)] + :return: """ a_anf = conf[0] a_ans = conf[1] @@ -479,7 +620,12 @@ def work_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float h_target = 1.0 # this phase is not applicable if Ae or AnS are directly at the bottom of the model - if phi == 0.0 or gamma == 0.0: + # if phi == 0.0 or gamma == 0.0: + # return t_s, h_s, g_s + + # check whether phase is applicable or if h is + # already at the end of the phase + if abs(h_s - h_target) < ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s # g(t_s) = g_s can be solved for c @@ -502,11 +648,29 @@ def gt(t): def ht(t): return t * ag - ((b * np.exp(-k * t)) / k) + s_ch - # find end of phase A6. The time point where h(t_end)=1 - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: h_target - ht(t), + # expenditure: find the time point where h(t_end)=1 + t_exp = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - ht(t), + initial_guess=t_s, + max_steps=t_max) + # recovery + h_target = max(1 - gamma, 1 - phi) + t_rec = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - h_target, initial_guess=t_s, max_steps=t_max) - return t_end, ht(t_end), gt(t_end) + + t_end = min(t_exp, t_rec) + # Determine what the current phase fAe_fAn transitions into + if t_end >= t_max: + func = None + elif t_end == t_rec: + if h_target == 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_lAn + else: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + func = None + + return t_end, ht(t_end), gt(t_end), func @staticmethod def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: float = 5000.0) -> ( @@ -515,9 +679,9 @@ def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: f # all recovery phases in order phases = [ODEThreeCompHydSimulator.rec_fAe_fAnS, ODEThreeCompHydSimulator.rec_lAe_fAnS, - ODEThreeCompHydSimulator.rec_fAe_lAnS, - ODEThreeCompHydSimulator.rec_fAe_rAnS, - ODEThreeCompHydSimulator.lAe_lAnS, + ODEThreeCompHydSimulator.fAe_lAn, + ODEThreeCompHydSimulator.fAe_rAn, + ODEThreeCompHydSimulator.lAe_lAn, ODEThreeCompHydSimulator.lAe_rAn, ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.lAe] @@ -563,8 +727,8 @@ def rec_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, h_target = max(1 - gamma, 1 - phi) # check whether phase is applicable or if h is - # already above the end of the phase - if h_s < h_target - ODEThreeCompHydSimulator.eps: + # already at the end of the phase + if abs(h_s - h_target) < ODEThreeCompHydSimulator.eps: return t_s, h_s, g_s # g(t_s) = g_s can be solved for c @@ -859,17 +1023,29 @@ def optimize(func, initial_guess, max_steps): # start precision step_size = 10.0 + step_min = 0.0000001 # check if initial guess conforms to underlying optimizer assumption t = initial_guess if func(t) < 0: - if func(t + ODEThreeCompHydSimulator.eps) >= 0: - t += ODEThreeCompHydSimulator.eps + # if not, the value might just be slightly off. + # Check if an increase fixes it + check = step_size + if func(t + check) >= 0: + # find minimal step size that fixes it + while func(t + check) >= 0: + check = check / 10.0 + t += check else: - raise UserWarning("initial guess for func is not positive") + logging.warning("initial guess for func is not positive. Tried with \n" + "f(t) = {} \n " + "f(t+check) = {}".format( + func(t), + func(t + check))) + return max_steps # while maximal precision is not reached - while step_size > 0.0000001: + while step_size > step_min: t_p = t # increase t until function turns negative while t <= max_steps + 1 and func(t) >= 0: diff --git a/tests/phase_fAe.py b/tests/phase_fAe.py new file mode 100644 index 0000000..28824b0 --- /dev/null +++ b/tests/phase_fAe.py @@ -0,0 +1,76 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + + +def test(func, h, g, p, conf, t=0, hz=500, t_max=2000, log=2): + # get results of ODE function + t_end, h_end, g_end, func = func(t_s=t, h_s=h, g_s=g, p=p, t_max=t_max, conf=conf) + + # confirm with iterative agent + agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], m_ans=conf[3], + m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) + agent.reset() + agent.set_g(g) + agent.set_h(h) + agent.set_power(p) + + # visual check + if log > 1: + ThreeCompVisualisation(agent) + + # run simulation + for _ in range(int((t_end - t) * agent.hz)): + agent.perform_one_step() + + # verify results + assert abs(h_end - agent.get_h()) < 0.0001 + assert abs(g_end - agent.get_g()) < 0.0001 + + # log outputs according to level + if log > 0: + logging.info("predicted time: {} \n " + "assigned next phase: {}".format(t_end, func)) + if log > 1: + ThreeCompVisualisation(agent) + + return func + + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + # estimations per second for discrete agent + + # a D configuration + conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, + 0.4892228099402588, 0.1524379644134216, 0.780228306857272] + + h2 = 0.325100624266976845 + g2 = 0 + + # fAe has three possible ends + func = ODEThreeCompHydSimulator.fAe + logging.info("Func {}".format(func)) + + p = 0 + logging.info("h = 1 - phi") + n_func = test(func, h2, g2, p, conf) + assert n_func == ODEThreeCompHydSimulator.lAe + + p = 280 + logging.info("h = 1 - gamma") + n_func = test(func, h2, g2, p, conf) + assert n_func == ODEThreeCompHydSimulator.fAe_lAn + + logging.info("time limit") + p = 254 + n_func = test(func, h2, g2, p, conf, t_max=100) + assert n_func is None diff --git a/tests/phase_lAe.py b/tests/phase_lAe.py new file mode 100644 index 0000000..d4e0ffd --- /dev/null +++ b/tests/phase_lAe.py @@ -0,0 +1,42 @@ +import logging +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator + +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + h = 0.1 + + # lAe has four possible ends + func = ODEThreeCompHydSimulator.lAe + logging.info("Func {}".format(func)) + + p = 0 + logging.info("h = 0") + n_func = test(func, h, 0, p, conf, t_max=500) + assert n_func is None + + p = 350 + logging.info("h = 1- gamma") + n_func = test(func, h, 0, p, conf) + assert n_func == ODEThreeCompHydSimulator.lAe_lAn + + p = 350 + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.44892228099402588, + 0.3524379644134216, 0.8580228306857272] + logging.info("h = 1 - phi") + n_func = test(func, h, 0, p, conf) + assert n_func == ODEThreeCompHydSimulator.fAe + + p = 10 + logging.info("equilibrium") + n_func = test(func, h, 0, p, conf) + assert n_func is None diff --git a/tests/phase_lAe_rAn.py b/tests/phase_lAe_rAn.py new file mode 100644 index 0000000..8fff21f --- /dev/null +++ b/tests/phase_lAe_rAn.py @@ -0,0 +1,49 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + ht3 = 0.2419771693142728 + gt3 = 0.27416467522715564 + + # lAe_rAn + func = ODEThreeCompHydSimulator.lAe_rAn + logging.info("Func {}".format(func)) + + p = 350 + logging.info("h == g") + n_func = test(func, ht3, gt3, p, conf) + assert n_func == ODEThreeCompHydSimulator.lAe_lAn + + p = 0 + ht3 = 0.1119771693142728 + gt3 = 0.12416467522715564 + logging.info("g == 0") + n_func = test(func, ht3, gt3, p, conf) + assert n_func == ODEThreeCompHydSimulator.lAe + + p = 550 + gt3 = 0.4416467522715564 + logging.info("h == 1 - phi") + n_func = test(func, ht3, gt3, p, conf) + assert n_func == ODEThreeCompHydSimulator.fAe_rAn + + p = 150 + logging.info("Reach max t") + n_func = test(func, ht3, gt3, p, conf) + assert n_func == ODEThreeCompHydSimulator.fAe_rAn diff --git a/tests/rec_a1_trial.py b/tests/rec_a1_trial.py deleted file mode 100644 index 842b7e0..0000000 --- a/tests/rec_a1_trial.py +++ /dev/null @@ -1,95 +0,0 @@ -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 5 - t_rec = 10000 # max time - - # estimations per second for discrete agent - hz = 250 - - conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, - 363.2970828395908, 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - t1 = 5000 - ht1 = 0.016964525316181377 - gt1 = 0.0 - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - - # our general solution to the integral - # s_c1 = (ht1 - p_rec * (1 - phi) / m_ae) / np.exp(- m_ae * t1 / (a_anf * (1 - phi))) - - # full recovery is only possible if p_rec is 0 - # use equation (4) from morton with own addition - # def a1_ht(t): - # return s_c1 * np.exp(- m_ae * t / (a_anf * (1 - phi))) + p_rec * (1 - phi) / m_ae - - # with substituted c1 - def a1_ht(t): - return (ht1 - p_rec * (1 - phi) / m_ae) * \ - np.exp(m_ae * (t1 - t) / (a_anf * (1 - phi))) + \ - p_rec * (1 - phi) / m_ae - - - # full recovery is never reached as log(inf) only approximates 0 - # a1_ht(max rec time) results in target_h - target_h = a1_ht(t_rec) - - # h(t) = 0 is never reached and causes a log(0) estimation. A close approximation is h(t) = 0.0001 - # t0 = a_anf * (1 - phi) / - m_ae * np.log(0.000001 / s_c1 - p_rec * (1 - phi) / (m_ae * s_c1)) - - # return min(h) if even after the maximal recovery time h>epsilon - if target_h > 0.00001: - t0 = t_rec - else: - # with substituted c1 - t0 = a_anf * (1 - phi) / - m_ae * \ - (np.log(0.00001 - p_rec * (1 - phi) / m_ae) - - (np.log(ht1 - p_rec * (1 - phi) / m_ae) + - m_ae * t1 / (a_anf * (1 - phi)) - ) - ) - - # check in simulation - agent.reset() - agent.set_g(gt1) - agent.set_h(ht1) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int(t0 * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(t0, - target_h - agent.get_h(), - 0 - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_a3_r1_trial.py b/tests/rec_a3_r1_trial.py deleted file mode 100644 index 0b71ea3..0000000 --- a/tests/rec_a3_r1_trial.py +++ /dev/null @@ -1,115 +0,0 @@ -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 100 - t_max = 5000 # max rec time - - # estimations per second for discrete agent - hz = 250 - - conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, - 363.2970828395908, 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - t3 = 0 - ht3 = 0.5419771693142728 - gt3 = 0.07416467522715564 - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # my simplified form - a = m_ae / (a_anf * (1 - phi)) + \ - m_ans / (a_ans * (1 - theta - gamma)) + \ - m_ans / (a_anf * (1 - theta - gamma)) - - b = m_ae * m_ans / \ - (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - - # c' for p_rec - c = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ - (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - - # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c - r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) - r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - - # uses Al dt/dl part of EQ(8) solved for c2 - # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) - # and then substituted in EQ(14) and solved for c1 - s_c1 = (c / b + (m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) - gt3) / \ - (np.exp(r1 * t3) * (r1 / r2 - 1)) - - # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) - - - def a3_gt(t): - # the general solution for g(t) - return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b - - - # substitute into EQ(9) for h - def a3_ht(t): - k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 - k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 - return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - - - # find the point where h(t) == g(t) - import time - - t0 = time.process_time_ns() - eq_gh = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_ht(t) - (a3_gt(t) + theta), - initial_guess=t3, - max_steps=t_max) - t1 = time.process_time_ns() - print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) - - t0 = time.process_time_ns() - optimize.fsolve(lambda t: a3_ht(t) - (a3_gt(t) + theta), - x0=np.array([t3])) - t1 = time.process_time_ns() - print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - - # check in simulation - agent.reset() - agent.set_g(gt3) - agent.set_h(ht3) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int(eq_gh * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(eq_gh, - a3_ht(eq_gh) - agent.get_h(), - a3_gt(eq_gh) - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_fAe_trial.py b/tests/rec_fAe_trial.py deleted file mode 100644 index 3cb2a80..0000000 --- a/tests/rec_fAe_trial.py +++ /dev/null @@ -1,59 +0,0 @@ -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 260 - p_rec = 0 - - # estimations per second for discrete agent - hz = 1000 - - # a D configuration - conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, - 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - phi = conf[7] - - t2 = 2643.143819400002 - h2 = 0.025100624266976845 - g2 = 4.883399240540598e-12 - - t_end, h_end, g_end = ODEThreeCompHydSimulator.fAe(t_s=t2, - h_s=h2, - g_s=g2, - p=p_rec, - t_max=5000, - conf=conf) - - # check in simulation - agent.reset() - agent.set_g(g2) - agent.set_h(h2) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int((t_end - t2) * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(t_end, - h_end - agent.get_h(), - g_end - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 9d3d58d..3141c46 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -54,10 +54,10 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, # all recovery phases in order phases = [ODEThreeCompHydSimulator.rec_fAe_fAnS, ODEThreeCompHydSimulator.rec_lAe_fAnS, - ODEThreeCompHydSimulator.rec_fAe_lAnS, - ODEThreeCompHydSimulator.rec_fAe_rAnS, + ODEThreeCompHydSimulator.fAe_lAn, + ODEThreeCompHydSimulator.fAe_rAn, ODEThreeCompHydSimulator.lAe_rAn, - ODEThreeCompHydSimulator.lAe_lAnS, + ODEThreeCompHydSimulator.lAe_lAn, ODEThreeCompHydSimulator.fAe, ODEThreeCompHydSimulator.lAe] @@ -125,7 +125,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, p_exp = 260 t_rec = 3600 - p_rec = 0 + p_rec = 247 t_max = 5000 # estimations per second for discrete agent hz = 400 @@ -134,7 +134,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, # a configuration c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, - 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] + 0.14892228099402588, 0.2580228306857272, 0.2580228306857272] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 00a23c3..5892626 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -105,13 +105,13 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 681 - t_rec = 3600 - p_rec = 247 + p_exp = 500 + t_rec = 240 + p_rec = 0 t_max = 5000 # estimations per second for discrete agent - hz = 2000 + hz = 500 # required precision of discrete to differential agent eps = 0.01 @@ -119,7 +119,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, - hz=hz, eps=eps, conf=c, log_level=2) + # rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + # hz=hz, eps=eps, conf=c, log_level=2) the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 9007241..2c64aa8 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -1,3 +1,4 @@ +import numpy as np from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation @@ -8,22 +9,13 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): # all TTE phases - max_time = 5000 + t_max = 5000 # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], m_ans=conf[3], m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - phases = [ODEThreeCompHydSimulator.lAe, - ODEThreeCompHydSimulator.lAe_rAn, - ODEThreeCompHydSimulator.fAe, - ODEThreeCompHydSimulator.work_fAe_rAnS, - ODEThreeCompHydSimulator.lAe_lAnS, - ODEThreeCompHydSimulator.work_fAe_lAnS, - ODEThreeCompHydSimulator.work_lAe_fAns, - ODEThreeCompHydSimulator.work_fAe_fAnS] - # set initial conditions h_s = 0 g_s = 0 # 1 - conf[6] - conf[5] @@ -35,23 +27,69 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): agent.set_g(g_s) ThreeCompVisualisation(agent) - # iterate through all phases until end is reached - for phase in phases: - t, h, g = phase(t, h, g, p, t_max=max_time, conf=conf) + theta = conf[5] + gamma = conf[6] + phi = conf[7] + func = None + while t < t_max: + if func is None: + # first distinguish between fAe and lAe + if h >= 1 - phi: + # fAe + if h < theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe + # fAe_rAnS + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe_rAn + # fAe_lAnS + elif h > g + theta and h < 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_lAn + # fAe_fAnS + elif h > 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, + phi)) + else: + # lAr + if h < theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe_rAn + elif h > g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_lAn + elif h > 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, + phi)) + + # iterate through all phases until end is reached + t, h, g, n_func = func(t, h, g, p, t_max=t_max, conf=conf) # display intermediate state if log level is high enough if log_level > 0: - logging.info("PHASE {} t {} ".format(phase, t)) + logging.info("PHASE {} t {} ".format(func, t)) agent.set_h(h) agent.set_g(g) - logging.info("ODE".format(phase, t)) + logging.info("ODE".format(func, t)) ThreeCompVisualisation(agent) + func = n_func + if log_level > 0: + logging.info("next PHASE {}".format(func)) + # exit loop if end is reached - if t >= max_time: - logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(phase, t, h, g)) + if t >= t_max: + logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(func, t, h, g)) break + # if recovery time is reached return fill levels at that point + if t == np.nan: + return t, h, g + # now confirm with iterative agent # set to initial state agent.reset() @@ -68,13 +106,17 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): h_diff = agent.get_h() - h if log_level >= 2: - logging.info("error phase {}. h is off by {}".format(phase, h_diff)) - logging.info("error phase {}. g is off by {}".format(phase, g_diff)) - logging.info("ITERATIVE".format(phase, t)) + logging.info("error phase {}. h is off by {}".format(func, h_diff)) + logging.info("error phase {}. g is off by {}".format(func, g_diff)) + logging.info("ITERATIVE".format(func, t)) ThreeCompVisualisation(agent) - assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(phase, g_diff) - assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(phase, h_diff) + assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(func, g_diff) + assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(func, h_diff) + + # if all phases complete full exhaustion is reached + return t, h, g + def the_loop(p: float = 350.0, hz: int = 250, @@ -101,8 +143,8 @@ def the_loop(p: float = 350.0, # required precision of discrete to differential agent eps = 0.001 - example_conf = [21704.77778915587, 61925.84797188902, 212.76772005473063, 140.0897845828814, 32.4028329961532, - 0.3217431159932008, 0.2683727457040581, 0.7190401470030847] - tte_test_procedure(p, hz, eps, example_conf, log_level=2) + example_conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, + 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] + tte_test_procedure(p, hz, eps, example_conf, log_level=1) the_loop(p=p, hz=hz, eps=eps) From 44a6a91c59bd7bdb1839ab89f44b0bbb9b7638bb Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 11 Oct 2021 19:03:06 +1100 Subject: [PATCH 48/71] finish new phase tests and detailed test procedure --- .../simulator/ode_three_comp_hyd_simulator.py | 28 ++-- tests/phase_fAe.py | 11 +- tests/phase_fAe_fAn.py | 52 +++++++ tests/phase_fAe_lAn.py | 53 +++++++ tests/phase_fAe_rAn.py | 49 +++++++ tests/phase_lAe.py | 12 +- tests/phase_lAe_fAn.py | 38 +++++ tests/phase_lAe_lAn.py | 52 +++++++ tests/phase_lAe_rAn.py | 21 +-- tests/rec_a3_r2_trial.py | 132 ------------------ tests/rec_a4_r1_trial.py | 107 -------------- tests/rec_a4_r2_trial.py | 107 -------------- tests/rec_a5_trial.py | 102 -------------- tests/rec_a6_trial.py | 105 -------------- 14 files changed, 278 insertions(+), 591 deletions(-) create mode 100644 tests/phase_fAe_fAn.py create mode 100644 tests/phase_fAe_lAn.py create mode 100644 tests/phase_fAe_rAn.py create mode 100644 tests/phase_lAe_fAn.py create mode 100644 tests/phase_lAe_lAn.py delete mode 100644 tests/rec_a3_r2_trial.py delete mode 100644 tests/rec_a4_r1_trial.py delete mode 100644 tests/rec_a4_r2_trial.py delete mode 100644 tests/rec_a5_trial.py delete mode 100644 tests/rec_a6_trial.py diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index f97b0c1..4d572bf 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -275,7 +275,7 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) return t_end, h_target, g_s, func @staticmethod - def fAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( + def fAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( float, float, float): a_anf = conf[0] @@ -302,22 +302,22 @@ def fAe_rAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf # if h is above g (flow from AnF into AnS) a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) - b_hg = (p_exp - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) + b_hg = (p - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) # derivative g'(t) can be calculated manually dgt_hg = - m_anf * (g_s + theta - h_s) / (a_ans * (1 - gamma)) # which then allows to derive c1 and c2 - s_c1_gh = ((p_exp - m_ae) / (a_anf + a_ans) - dgt_hg) * np.exp(a_hg * t_s) - s_c2_gh = (-t_s * b_hg + dgt_hg) / a_hg - (p_exp - m_ae) / ((a_anf + a_ans) * a_hg) + g_s + s_c1_gh = ((p - m_ae) / (a_anf + a_ans) - dgt_hg) * np.exp(a_hg * t_s) + s_c2_gh = (-t_s * b_hg + dgt_hg) / a_hg - (p - m_ae) / ((a_anf + a_ans) * a_hg) + g_s def gt(t): # general solution for g(t) - return t * (p_exp - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) + return t * (p - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) def dgt(t): # first derivative g'(t) - return (p_exp - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) + return (p - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) def ht(t): # EQ(16) with constants for g(t) and g'(t) @@ -521,15 +521,14 @@ def ht(t): elif t_end == t_gth: func = ODEThreeCompHydSimulator.fAe_rAn elif t_end == t_gam: - func = ODEThreeCompHydSimulator.lAe_fAn + func = ODEThreeCompHydSimulator.fAe_fAn else: func = ODEThreeCompHydSimulator.lAe_lAn return t_end + t_p, ht(t_end), gt(t_end), func @staticmethod - def lAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( - float, float, float): + def lAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): """ :param t_s: time in seconds at which phase starts :param h_s: depletion state of AnF when phase starts @@ -577,7 +576,7 @@ def ht(t): return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a # find the time at which the phase stops - t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - 1 - gamma, + t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - (1 - gamma), initial_guess=t_s, max_steps=t_max) @@ -591,20 +590,19 @@ def ht(t): if t_end >= t_max: func = None elif t_end == t_gam: - func = ODEThreeCompHydSimulator.lAe_fAn + func = ODEThreeCompHydSimulator.lAe_lAn else: func = ODEThreeCompHydSimulator.fAe_fAn return t_end, ht(t_end), gt(t_end), func @staticmethod - def fAe_fAn(t_s: float, h_s: float, g_s: float, p_exp: float, t_max: float, conf: list) -> ( - float, float, float): + def fAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): """ :param t_s: :param h_s: :param g_s: - :param p_exp: constant power output + :param p: constant power output :param t_max: maximal time limit :param conf: configuration of hydraulic model :return: @@ -639,7 +637,7 @@ def gt(t): # a = -m_ae / a_anf b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) # g = p / a_anf - ag = (p_exp - m_ae) / a_anf + ag = (p - m_ae) / a_anf # h(t_s) = h_s can be solved for c s_ch = -t_s * ag + ((b * np.exp(-k * t_s)) / k) + h_s diff --git a/tests/phase_fAe.py b/tests/phase_fAe.py index 28824b0..5660b77 100644 --- a/tests/phase_fAe.py +++ b/tests/phase_fAe.py @@ -47,12 +47,9 @@ def test(func, h, g, p, conf, t=0, hz=500, t_max=2000, log=2): logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - # estimations per second for discrete agent - - # a D configuration conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, 0.4892228099402588, 0.1524379644134216, 0.780228306857272] - + log = 0 h2 = 0.325100624266976845 g2 = 0 @@ -62,15 +59,15 @@ def test(func, h, g, p, conf, t=0, hz=500, t_max=2000, log=2): p = 0 logging.info("h = 1 - phi") - n_func = test(func, h2, g2, p, conf) + n_func = test(func, h2, g2, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.lAe p = 280 logging.info("h = 1 - gamma") - n_func = test(func, h2, g2, p, conf) + n_func = test(func, h2, g2, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.fAe_lAn logging.info("time limit") p = 254 - n_func = test(func, h2, g2, p, conf, t_max=100) + n_func = test(func, h2, g2, p, conf, t_max=100, log=log) assert n_func is None diff --git a/tests/phase_fAe_fAn.py b/tests/phase_fAe_fAn.py new file mode 100644 index 0000000..c329ae2 --- /dev/null +++ b/tests/phase_fAe_fAn.py @@ -0,0 +1,52 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [15101.24769778409, 486209.27743067988, 252.71702367096787, + 363.2970828395908, 43.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + log = 0 + h2 = 0.825100624266976845 + g2 = 0 + + # fAe_fAn has four possible ends + func = ODEThreeCompHydSimulator.fAe_fAn + logging.info("Func {}".format(func)) + + p = 600 + logging.info("h = 1") + n_func = test(func, h2, g2, p, conf, log=log) + assert n_func is None + + p = 0 + logging.info("h = 1 - gamma") + n_func = test(func, h2, g2, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe_lAn + + p = 0 + conf = [15101.24769778409, 486209.27743067988, 252.71702367096787, + 363.2970828395908, 43.27073086773415, 0.14892228099402588, + 0.5524379644134216, 0.3580228306857272] + logging.info("h = 1 - phi") + n_func = test(func, h2, g2, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.lAe_fAn + + p = 200 + conf = [151010.24769778409, 486209.27743067988, 252.71702367096787, + 363.2970828395908, 43.27073086773415, 0.14892228099402588, + 0.5524379644134216, 0.3580228306857272] + logging.info("time limit") + n_func = test(func, h2, g2, p, conf, log=log, t_max=10) + assert n_func is None diff --git a/tests/phase_fAe_lAn.py b/tests/phase_fAe_lAn.py new file mode 100644 index 0000000..f02c0f6 --- /dev/null +++ b/tests/phase_fAe_lAn.py @@ -0,0 +1,53 @@ +import math + +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +from tests import configurations +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = configurations.c + + log = 2 + h5 = 1.0 - conf[7] + g5 = 0.1 + + # fAe_lAn has four possible ends + func = ODEThreeCompHydSimulator.fAe_lAn + logging.info("Func {}".format(func)) + + p = 500 + logging.info("h = 1 - gamma") + n_func = test(func, h5, g5, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe_fAn + + p = 100 + h5 = 1.0 - conf[6] + logging.info("h = 1 - phi") + n_func = test(func, h5, g5, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.lAe_lAn + + p = 100 + h5 = 1.0 - conf[6] + g5 = 0.6 + logging.info("h = g") + n_func = test(func, h5, g5, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe_rAn + + p = 255 + h5 = 1.0 - conf[6] + g5 = 0.6 + logging.info("time limit") + n_func = test(func, h5, g5, p, conf, log=log) + assert n_func is None diff --git a/tests/phase_fAe_rAn.py b/tests/phase_fAe_rAn.py new file mode 100644 index 0000000..a7d5897 --- /dev/null +++ b/tests/phase_fAe_rAn.py @@ -0,0 +1,49 @@ +import math + +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +from tests import configurations +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [5604.966588001499, 54499.44673416602, 155.82060702780947, + 105.26777135234472, 28.623478621476917, 0.2314852496176266, + 0.15438323467786853, 0.5949904604992432] + + log = 2 + ht6 = 0.45 + gt6 = 0.5 + + # fAe_rAn has four possible ends + func = ODEThreeCompHydSimulator.fAe_rAn + logging.info("Func {}".format(func)) + + p = 0 + logging.info("h = 1 - phi") + n_func = test(func, ht6, gt6, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.lAe_rAn + + p = 500 + logging.info("h = g") + n_func = test(func, ht6, gt6, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe_lAn + + p = 120 + gt6 = 0.1 + conf = [5604.966588001499, 14499.44673416602, 155.82060702780947, + 105.26777135234472, 128.623478621476917, 0.6314852496176266, + 0.15438323467786853, 0.5949904604992432] + logging.info("g = 0") + n_func = test(func, ht6, gt6, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe diff --git a/tests/phase_lAe.py b/tests/phase_lAe.py index d4e0ffd..05f3ffa 100644 --- a/tests/phase_lAe.py +++ b/tests/phase_lAe.py @@ -11,7 +11,7 @@ conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] - + log = 0 h = 0.1 # lAe has four possible ends @@ -20,12 +20,12 @@ p = 0 logging.info("h = 0") - n_func = test(func, h, 0, p, conf, t_max=500) + n_func = test(func, h, 0, p, conf, t_max=500, log=log) assert n_func is None p = 350 logging.info("h = 1- gamma") - n_func = test(func, h, 0, p, conf) + n_func = test(func, h, 0, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.lAe_lAn p = 350 @@ -33,10 +33,10 @@ 363.2970828395908, 38.27073086773415, 0.44892228099402588, 0.3524379644134216, 0.8580228306857272] logging.info("h = 1 - phi") - n_func = test(func, h, 0, p, conf) + n_func = test(func, h, 0, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.fAe p = 10 - logging.info("equilibrium") - n_func = test(func, h, 0, p, conf) + logging.info("time limit") + n_func = test(func, h, 0, p, conf, log=log) assert n_func is None diff --git a/tests/phase_lAe_fAn.py b/tests/phase_lAe_fAn.py new file mode 100644 index 0000000..a29151e --- /dev/null +++ b/tests/phase_lAe_fAn.py @@ -0,0 +1,38 @@ +import logging +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator + +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.4524379644134216, 0.2580228306857272] + + ht4 = 0.6475620355865783 + gt4 = 0.15679831105786776 + + log = 0 + + # lAe_fAn has three possible ends + func = ODEThreeCompHydSimulator.lAe_fAn + logging.info("Func {}".format(func)) + + p = 350 + logging.info("h = 1 - phi") + n_func = test(func, ht4, gt4, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe_fAn + + p = 100 + logging.info("h = 1 - gamma") + n_func = test(func, ht4, gt4, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.lAe_lAn + + p = 230 + gt4 = 0.35 + logging.info("time limit") + n_func = test(func, ht4, gt4, p, conf, log=log, t_max=1000) + assert n_func is None diff --git a/tests/phase_lAe_lAn.py b/tests/phase_lAe_lAn.py new file mode 100644 index 0000000..c5b4f0d --- /dev/null +++ b/tests/phase_lAe_lAn.py @@ -0,0 +1,52 @@ +from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent +from scipy import optimize + +import logging + +import numpy as np +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation + +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [10101.24769778409, 80209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.4580228306857272] + + log = 0 + ht3 = 0.2 + gt3 = 0 + + # lAe_lAn has four possible ends + func = ODEThreeCompHydSimulator.lAe_lAn + logging.info("Func {}".format(func)) + + p = 350 + logging.info("h = 1 - phi") + n_func = test(func, ht3, gt3, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.fAe_lAn + + p = 0 + ht3 = 0.5 + logging.info("h = g") + n_func = test(func, ht3, gt3, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.lAe_rAn + + p = 150 + ht3 = 0.5 + logging.info("time limit") + n_func = test(func, ht3, gt3, p, conf, log=log, t_max=1000) + assert n_func is None + + p = 450 + conf = [10101.24769778409, 80209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 0.1580228306857272] + logging.info("h = 1 - gamma") + n_func = test(func, ht3, gt3, p, conf, log=log) + assert n_func == ODEThreeCompHydSimulator.lAe_fAn diff --git a/tests/phase_lAe_rAn.py b/tests/phase_lAe_rAn.py index 8fff21f..d9434f7 100644 --- a/tests/phase_lAe_rAn.py +++ b/tests/phase_lAe_rAn.py @@ -20,30 +20,31 @@ ht3 = 0.2419771693142728 gt3 = 0.27416467522715564 + log = 0 - # lAe_rAn + # lAe_rAn has four possible outcomes func = ODEThreeCompHydSimulator.lAe_rAn logging.info("Func {}".format(func)) p = 350 - logging.info("h == g") - n_func = test(func, ht3, gt3, p, conf) + logging.info("h = g") + n_func = test(func, ht3, gt3, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.lAe_lAn p = 0 ht3 = 0.1119771693142728 gt3 = 0.12416467522715564 - logging.info("g == 0") - n_func = test(func, ht3, gt3, p, conf) + logging.info("g = 0") + n_func = test(func, ht3, gt3, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.lAe p = 550 gt3 = 0.4416467522715564 - logging.info("h == 1 - phi") - n_func = test(func, ht3, gt3, p, conf) + logging.info("h = 1 - phi") + n_func = test(func, ht3, gt3, p, conf, log=log) assert n_func == ODEThreeCompHydSimulator.fAe_rAn p = 150 - logging.info("Reach max t") - n_func = test(func, ht3, gt3, p, conf) - assert n_func == ODEThreeCompHydSimulator.fAe_rAn + logging.info("time limit") + n_func = test(func, ht3, gt3, p, conf, log=log) + assert n_func is None diff --git a/tests/rec_a3_r2_trial.py b/tests/rec_a3_r2_trial.py deleted file mode 100644 index d1d85e4..0000000 --- a/tests/rec_a3_r2_trial.py +++ /dev/null @@ -1,132 +0,0 @@ -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 0 - t_max = 5000 - - # estimations per second for discrete agent - hz = 250 - - conf = [10101.24769778409, 80209.27743067988, 252.71702367096787, - 363.2970828395908, 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - # End of A3 R1 - t3 = 0 - ht3 = 0.2 - gt3 = 0.2 - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # EQ 16 and 17 substituted in EQ 8 - a = m_ae / (a_anf * (1 - phi)) + \ - m_anf / (a_ans * (1 - gamma)) + \ - m_anf / (a_anf * (1 - gamma)) - - b = m_ae * m_anf / \ - (a_anf * a_ans * (1 - phi) * (1 - gamma)) - - # c = (p_rec - (m_ae * theta) / (1 - phi)) * m_anf / \ - # (a_anf * a_ans * (1 - gamma)) - c = m_anf * (p_rec * (1 - phi) - m_ae * theta) / \ - (a_anf * a_ans * (1 - phi) * (1 - gamma)) - - # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c - r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) - r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - - # uses Al dt/dl part of EQ(16) == dl/dt of EQ(14) solved for c2 - # and then substituted in EQ(14) and solved for c1 - s_c1 = (c / b - (m_anf * (gt3 + theta - ht3)) / (a_ans * r2 * (1 - gamma)) - gt3) / \ - (np.exp(r1 * t3) * (r1 / r2 - 1)) - - # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (gt3 - s_c1 * np.exp(r1 * t3) - c / b) / np.exp(r2 * t3) - - def a3_gt(t): - # the general solution for g(t) - return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b - - # substitute into EQ(9) for h - def a3_ht(t): - k1 = a_ans * (1 - gamma) / m_anf * s_c1 * r1 + s_c1 - k2 = a_ans * (1 - gamma) / m_anf * s_c2 * r2 + s_c2 - return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - - # use the quickest possible recovery as the initial guess (assumes h=0) - in_c1 = (gt3 + theta) * np.exp(-m_anf * t3 / ((gamma - 1) * a_ans)) - in_t = (gamma - 1) * a_ans * (np.log(theta) - np.log(in_c1)) / m_anf - - # find the point where g(t) == 0 - import time - - t0 = time.process_time_ns() - g0 = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_gt(t), - initial_guess=t3, - max_steps=t_max) - t1 = time.process_time_ns() - print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) - - t0 = time.process_time_ns() - optimize.fsolve(lambda t: a3_gt(t), x0=np.array([in_t]))[0] - t1 = time.process_time_ns() - print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - - # check in simulation - agent.reset() - agent.set_g(gt3) - agent.set_h(ht3) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for i in range(int(g0 * agent.hz)): - agent.perform_one_step() - - if i % agent.hz == 0: - test_t = i / agent.hz - logging.info("predicted time: {} \n" - "diff h: {} - {} = {}\n" - "diff g: {} - {} = {}".format(test_t, - a3_ht(test_t), - agent.get_h(), - a3_ht(test_t) - agent.get_h(), - a3_gt(test_t), - agent.get_g(), - a3_gt(test_t) - agent.get_g())) - - logging.info("predicted time: {} \n" - "diff h: {} - {} = {}\n" - "diff g: {} - {} = {}".format(g0, - a3_ht(g0), - agent.get_h(), - a3_ht(g0) - agent.get_h(), - a3_gt(g0), - agent.get_g(), - a3_gt(g0) - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_a4_r1_trial.py b/tests/rec_a4_r1_trial.py deleted file mode 100644 index 48019d8..0000000 --- a/tests/rec_a4_r1_trial.py +++ /dev/null @@ -1,107 +0,0 @@ -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 100 - t_max = 5000 - - # estimations per second for discrete agent - hz = 250 - - conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, - 363.2970828395908, 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - # PHASE A4 - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # derived from tte estimator and - t4 = 0 - ht4 = 0.6475620355865783 - gt4 = 0.15679831105786776 - - # if g is above h (flow from AnS into AnF) - a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - b_gh = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - - # derivative g'(t4) can be calculated manually - dgt4_gh = m_ans * (ht4 - gt4 - theta) / (a_ans * (1 - theta - gamma)) - - # which then allows to derive c1 and c2 - s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t4) - s_c2_gh = (-t4 * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + gt4 - - - def a4_gt(t): - # general solution for g(t) - return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) - - - def a4_dgt(t): - # first derivative g'(t) - return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_gh * t) - - - def a4_ht(t): - # EQ(9) with constants for g(t) and g'(t) - return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - - - ht_end = 1 - phi - - # check if equilibrium in this phase - import time - t0 = time.process_time_ns() - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - ht_end, - initial_guess=0, - max_steps=t_max) - t1 = time.process_time_ns() - print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) - - t0 = time.process_time_ns() - optimize.fsolve(lambda t: a4_ht(t) - ht_end, x0=np.array([0])) - t1 = time.process_time_ns() - print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - - gt_end = a4_gt(t_end) - - agent.reset() - agent.set_g(gt4) - agent.set_h(ht4) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int(t_end * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(t_end, - a4_ht(t_end) - agent.get_h(), - a4_gt(t_end) - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_a4_r2_trial.py b/tests/rec_a4_r2_trial.py deleted file mode 100644 index b2a492a..0000000 --- a/tests/rec_a4_r2_trial.py +++ /dev/null @@ -1,107 +0,0 @@ -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 100 - t_max = 5000 - - # estimations per second for discrete agent - hz = 250 - - conf = [15101.24769778409, 486209.27743067988, 252.71702367096787, - 363.2970828395908, 43.27073086773415, 0.14892228099402588, - 0.3524379644134216, 0.4580228306857272] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - # PHASE A4 - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # derived from tte estimator and - t4 = 0 - h4 = 0.6 - g4 = h4 - theta - - # if h is above g (flow from AnF into AnS) - a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) - b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) - - # derivative g'(t4) can be calculated manually - dgt4_hg = - m_anf * (g4 + theta - h4) / (a_ans * (1 - gamma)) - - # which then allows to derive c1 and c2 - s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t4) - s_c2_gh = (-t4 * b_hg + dgt4_hg) / a_hg - (p_rec - m_ae) / ((a_anf + a_ans) * a_hg) + g4 - - - def a4_gt(t): - # general solution for g(t) - return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) - - - def a4_dgt(t): - # first derivative g'(t) - return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) - - - def a4_ht(t): - # EQ(16) with constants for g(t) and g'(t) - return a_ans * (1 - gamma) / m_anf * a4_dgt(t) + a4_gt(t) + theta - - - ht_end = 1 - phi - # check if equilibrium in this phase - - import time - - t0 = time.process_time_ns() - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - ht_end, - initial_guess=0, - max_steps=t_max) - t1 = time.process_time_ns() - print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) - - t0 = time.process_time_ns() - optimize.fsolve(lambda t: a4_ht(t) - ht_end, x0=np.array([0])) - t1 = time.process_time_ns() - print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - gt_end = a4_gt(t_end) - - agent.reset() - agent.set_g(g4) - agent.set_h(h4) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int(t_end * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(t_end, - a4_ht(t_end) - agent.get_h(), - a4_gt(t_end) - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_a5_trial.py b/tests/rec_a5_trial.py deleted file mode 100644 index 6d0df11..0000000 --- a/tests/rec_a5_trial.py +++ /dev/null @@ -1,102 +0,0 @@ -import math - -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -from tests import configurations - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 100 - t_max = 5000 - - # estimations per second for discrete agent - hz = 250 - - conf = configurations.a - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], - m_ans=conf[3], m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - # PHASE A5 - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - t5 = 0 - h5 = 1.0 - phi - g5 = 0.1 - - # g(t5) = g5 can be solved for c - s_cg = (g5 - (1 - theta - gamma)) * np.exp((m_ans * t5) / ((1 - theta - gamma) * a_ans)) - - - def a5_gt(t): - # generalised g(t) for phase A5 - return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) - - - # as defined for EQ(21) - k = m_ans / ((1 - theta - gamma) * a_ans) - a = -m_ae / ((1 - phi) * a_anf) - g = p_rec / a_anf - b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - - # find c that matches h(t5) = h5 - s_ch = (h5 + b / ((a + k) * np.exp(k) ** t5) + g / a) / np.exp(a) ** t5 - - - def a5_ht(t): - return -b / ((a + k) * np.exp(k) ** t) + s_ch * np.exp(a) ** t - g / a - - - h_target = 1 - gamma - - # estimate an initial guess that assumes no contribution from g - initial_guess = 0 - - import time - - t0 = time.process_time_ns() - rt5 = ODEThreeCompHydSimulator.optimize(func=lambda t: a5_ht(t) - h_target, - initial_guess=initial_guess, - max_steps=t_max) - t1 = time.process_time_ns() - print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) - - t0 = time.process_time_ns() - optimize.fsolve(lambda t: a5_ht(t) - h_target, x0=np.array([initial_guess])) - t1 = time.process_time_ns() - print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - - agent.reset() - agent.set_g(g5) - agent.set_h(h5) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int(rt5 * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(rt5, - a5_ht(rt5) - agent.get_h(), - a5_gt(rt5) - agent.get_g())) - ThreeCompVisualisation(agent) diff --git a/tests/rec_a6_trial.py b/tests/rec_a6_trial.py deleted file mode 100644 index afcaf4e..0000000 --- a/tests/rec_a6_trial.py +++ /dev/null @@ -1,105 +0,0 @@ -import math - -from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent -from scipy import optimize - -import logging - -import numpy as np -from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation - -from tests import configurations - -if __name__ == "__main__": - # set logging level to highest level - logging.basicConfig(level=logging.INFO, - format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - - p_exp = 350 - p_rec = 247 - t_max = 5000 - - # estimations per second for discrete agent - hz = 250 - - conf = [5604.966588001499, 54499.44673416602, 155.82060702780947, 105.26777135234472, 28.623478621476917, - 0.2314852496176266, 0.35438323467786853, 0.5949904604992432] - - # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], - m_ans=conf[3], m_anf=conf[4], the=conf[5], - gam=conf[6], phi=conf[7]) - - # PHASE A6 - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - t6 = 0 - ht6 = 1.0 - gt6 = 0.1 - - # g(t6) = gt6 can be solved for c - s_cg = (gt6 - (1 - theta - gamma)) / np.exp(-m_ans * t6 / ((1 - theta - gamma) * a_ans)) - - - def a6_gt(t): - # generalised g(t) for phase A6 - return (1 - theta - gamma) + s_cg * math.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) - - - k = m_ans / ((1 - theta - gamma) * a_ans) - # a = -m_ae / a_anf - b = (m_ans * s_cg) / ((1 - theta - gamma) * a_anf) - # g = p / a_anf - ag = (p_rec - m_ae) / a_anf - - # h(t6) = 1 can be solved for c - s_ch = -t6 * ag + ((b * math.exp(-k * t6)) / k) + ht6 - - - def a6_ht(t): - # generalised h(t) for recovery phase A6 - return t * ag - ((b * math.exp(-k * t)) / k) + s_ch - - - # A6 rec ends either at beginning of A4 or A5 - h_target = max(1 - gamma, 1 - phi) - - # estimate an initial guess that assumes no contribution from g - initial_guess = 0 - - import time - - t0 = time.process_time_ns() - rt6 = ODEThreeCompHydSimulator.optimize(func=lambda t: a6_ht(t) - h_target, - initial_guess=initial_guess, - max_steps=t_max) - t1 = time.process_time_ns() - print("Time elapsed own: ", t1 - t0) # CPU seconds elapsed (floating point) - - t0 = time.process_time_ns() - optimize.fsolve(lambda t: a6_ht(t) - h_target, x0=np.array([initial_guess])) - t1 = time.process_time_ns() - print("Time elapsed scipy: ", t1 - t0) # CPU seconds elapsed (floating point) - - agent.reset() - agent.set_g(gt6) - agent.set_h(1.0) - ThreeCompVisualisation(agent) - agent.set_power(p_rec) - - for _ in range(int(rt6 * agent.hz)): - agent.perform_one_step() - - logging.info("predicted time: {} \n" - "diff h: {}\n" - "diff g: {}".format(rt6, - a6_ht(rt6) - agent.get_h(), - a6_gt(rt6) - agent.get_g())) - ThreeCompVisualisation(agent) From fd1b8e0e909a6083686be196138c669128dbfc3e Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 11 Oct 2021 20:31:55 +1100 Subject: [PATCH 49/71] update rec phase tests to new structure --- .../simulator/ode_three_comp_hyd_simulator.py | 148 +++++++----------- tests/phase_lAe_lAn.py | 2 +- tests/rec_phase_tests.py | 135 ++++++++++------ tests/tte_phase_tests.py | 8 +- 4 files changed, 151 insertions(+), 142 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 4d572bf..4c54afc 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -48,45 +48,50 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = gamma = conf[6] phi = conf[7] + func = None while t < t_max: - # first distinguish between fAe and lAe - if h >= 1 - phi: - # fAe - if h <= theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe - # fAe_rAnS - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe_rAn - # fAe_lAnS - elif h >= g + theta and h <= 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_lAn - # fAe_fAnS - elif h >= 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_fAn + if func is None: + # first distinguish between fAe and lAe + if h >= 1 - phi: + # fAe + if h <= theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe + # fAe_rAnS + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe_rAn + # fAe_lAnS + elif h >= g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_lAn + # fAe_fAnS + elif h >= 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, + gamma, + phi)) else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, - phi)) - else: - # lAr - if h <= theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe_rAn - elif h >= g + theta and h <= 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_lAn - elif h >= 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, - phi)) + # lAr + if h <= theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe_rAn + elif h >= g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_lAn + elif h >= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, + gamma, + phi)) # iterate through all phases until end is reached - t, h, g = func(t, h, g, p_exp, t_max=t_max, conf=conf) + t, h, g, n_func = func(t, h, g, p_exp, t_max=t_max, conf=conf) + func = n_func # if recovery time is reached return fill levels at that point - if t == np.nan: + if t == np.nan or n_func is None: return t, h, g # if all phases complete full exhaustion is reached @@ -109,10 +114,6 @@ def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): # This phase is not applicable if fill-level of AnF below pipe exit Ae or top of AnS, .. # ... or AnS not full ... # ... or pipe exit of Ae is at the top of the model - # if h_s > h_bottom + ODEThreeCompHydSimulator.eps \ - # or g_s > ODEThreeCompHydSimulator.eps \ - # or phi == 0: - # return t_s, h_s, g_s # in some exp equations values exceed float capacity if t gets too large # Therefore, t_s is set to 0 and added later @@ -156,11 +157,6 @@ def lAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: li # ... or AnS fill-level is above AnF fill-level ... # ... or AnS is full ... # ... or pipe exit of Ae is at the top of the model - # if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ - # or h_s > g_s + theta + ODEThreeCompHydSimulator.eps \ - # or g_s < ODEThreeCompHydSimulator.eps \ - # or phi == 0: - # return t_s, h_s, g_s # in some exp equations values exceed float value range if t gets too large # Therefore, t_s is set to 0 and added later @@ -232,7 +228,7 @@ def ht(t): return t_p + t_end, ht(t_end), gt(t_end), func @staticmethod - def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> (float, float, float): + def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): """ The phase f_Ae where only Ae contributes and flow through p_Ae is at maximal capacity m_Ae. This phase is linear and does not allow an equilibrium because power p is assumed to be constant. @@ -244,10 +240,6 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) # this phase not applicable if h is not in-between 1-theta and phi and ... # ... AnS is not full - # if h_s > theta + ODEThreeCompHydSimulator.eps \ - # or h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ - # or g_s > ODEThreeCompHydSimulator.eps: - # return t_s, h_s, g_s # the first derivative. Change in ht ht_p = (p - m_ae) / a_anf @@ -275,8 +267,7 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) return t_end, h_target, g_s, func @staticmethod - def fAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( - float, float, float): + def fAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -289,10 +280,6 @@ def fAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: li # This phase only starts if is not applicable if fill-level of AnF above or at pipe exit Ae, .. # ... or AnS fill-level is above AnF fill-level ... # ... or AnS is full - # if h_s < 1 - phi - ODEThreeCompHydSimulator.eps or \ - # h_s > g_s + theta + ODEThreeCompHydSimulator.eps or \ - # g_s < ODEThreeCompHydSimulator.eps: - # return t_s, h_s, g_s # in some exp equations values exceed float capacity if t gets too large # Therefore, t_s is set to 0 and added later @@ -353,8 +340,7 @@ def ht(t): return t_end + t_p, ht(t_end), gt(t_end), func @staticmethod - def lAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( - float, float, float): + def lAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -364,13 +350,9 @@ def lAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # lAe and lAnS is only applicable if h is above or at pipe exit of Ae... + # lAe and lAn is only applicable if h is above or at pipe exit of Ae... # ... and above or at pipe exit AnS ... # ... and g is above h (error of epsilon tolerated) - # if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ - # or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ - # or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: - # return t_s, h_s, g_s # in some exp equations values exceed float capacity if t gets too large # Therefore, t_s is set to 0 and added later @@ -433,7 +415,6 @@ def ht(t): t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - ht(t), initial_guess=t_s, max_steps=t_max) - t_end = min(t_gh, t_gam, t_phi) # Determine what the current phase lAe_lAn transitions into @@ -449,8 +430,7 @@ def ht(t): return t_end + t_p, ht(t_end), gt(t_end), func @staticmethod - def fAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list) -> ( - float, float, float): + def fAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): a_anf = conf[0] a_ans = conf[1] @@ -463,10 +443,6 @@ def fAe_lAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: li # phase full Ae and limited AnS is only applicable h below pipe exit of Ae # ... and above pipe exist of AnS ... # ... and if g is above h (allows error of epsilon) - # if h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ - # or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ - # or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: - # return t_s, h_s, g_s # in some exp equations values exceed float capacity if t gets too large # Therefore, t_s is set to 0 and added later @@ -547,14 +523,14 @@ def lAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - # in work phases h is assumed to continuously increase - # and therefore this phase ends at 1-phi - h_target = 1 - phi - # this phase is not applicable if phi is greater or equal to gamma... # ... or h is already below pipe exit Ae - # if phi >= gamma or h_s >= h_target: - # return t_s, h_s, g_s + + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 # g(t_s) = g_s can be solved for c s_cg = (g_s - (1 - theta - gamma)) * np.exp((m_ans * t_s) / ((1 - theta - gamma) * a_ans)) @@ -594,7 +570,7 @@ def ht(t): else: func = ODEThreeCompHydSimulator.fAe_fAn - return t_end, ht(t_end), gt(t_end), func + return t_p + t_end, ht(t_end), gt(t_end), func @staticmethod def fAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): @@ -615,16 +591,13 @@ def fAe_fAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: li gamma = conf[6] phi = conf[7] - h_target = 1.0 - # this phase is not applicable if Ae or AnS are directly at the bottom of the model - # if phi == 0.0 or gamma == 0.0: - # return t_s, h_s, g_s - # check whether phase is applicable or if h is - # already at the end of the phase - if abs(h_s - h_target) < ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s + # in some exp equations values exceed float capacity if t gets too large + # Therefore, t_s is set to 0 and added later + t_p = t_s + t_max = t_max - t_s + t_s = 0 # g(t_s) = g_s can be solved for c s_cg = (g_s - (1 - theta - gamma)) / np.exp(-m_ans * t_s / ((1 - theta - gamma) * a_ans)) @@ -668,7 +641,7 @@ def ht(t): else: func = None - return t_end, ht(t_end), gt(t_end), func + return t_p + t_end, ht(t_end), gt(t_end), func @staticmethod def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: float = 5000.0) -> ( @@ -1033,14 +1006,13 @@ def optimize(func, initial_guess, max_steps): # find minimal step size that fixes it while func(t + check) >= 0: check = check / 10.0 - t += check + t += check * 10.0 # undo the last step else: - logging.warning("initial guess for func is not positive. Tried with \n" - "f(t) = {} \n " - "f(t+check) = {}".format( + raise UserWarning("initial guess for func is not positive. Tried with \n" + "f(t) = {} \n " + "f(t+check) = {}".format( func(t), func(t + check))) - return max_steps # while maximal precision is not reached while step_size > step_min: diff --git a/tests/phase_lAe_lAn.py b/tests/phase_lAe_lAn.py index c5b4f0d..d7dce66 100644 --- a/tests/phase_lAe_lAn.py +++ b/tests/phase_lAe_lAn.py @@ -18,7 +18,7 @@ 363.2970828395908, 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.4580228306857272] - log = 0 + log = 2 ht3 = 0.2 gt3 = 0 diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 3141c46..02cb169 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -8,7 +8,7 @@ import numpy as np -def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, hz: int, eps: float, +def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, hz: int, eps: float, conf: list, log_level: int = 0): """ Conducts a TTE and follows it up with a recovery at given conditions. Estimation results of the ODE integral @@ -51,54 +51,91 @@ def rec_trial_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, logging.info("[SUCCESS] Agent after TTE at {}".format(p_exp)) ThreeCompVisualisation(agent) - # all recovery phases in order - phases = [ODEThreeCompHydSimulator.rec_fAe_fAnS, - ODEThreeCompHydSimulator.rec_lAe_fAnS, - ODEThreeCompHydSimulator.fAe_lAn, - ODEThreeCompHydSimulator.fAe_rAn, - ODEThreeCompHydSimulator.lAe_rAn, - ODEThreeCompHydSimulator.lAe_lAn, - ODEThreeCompHydSimulator.fAe, - ODEThreeCompHydSimulator.lAe] - # restart time from 0 t = 0 + theta = conf[5] + gamma = conf[6] + phi = conf[7] + func = None + # cycles through phases until t_max is reached while t < t_rec: - # detailed checks for every phase - for phase in phases: - # save previous time to estimate time difference - t_p = t - - # get estimated time of phase end - t, h, g = phase(t, h, g, p_rec, t_max=t_rec, conf=conf) - - # double-check with discrete agent - for _ in range(int(round((t - t_p) * hz))): - agent.set_power(p_rec) - agent.perform_one_step() - g_diff = agent.get_g() - g - h_diff = agent.get_h() - h - - if log_level > 1: - logging.info("[intermediate result] {}\n" - "t {}\n" - "h {} g {}\n" - "Diff h {}\n" - "Diff g {}".format(phase, t, h, g, h_diff, g_diff)) - ThreeCompVisualisation(agent) - - assert abs(g_diff) < eps, "{} g is off by {}".format(phase, g_diff) - assert abs(h_diff) < eps, "{} h is off by {}".format(phase, h_diff) - - if t >= t_rec: - logging.info("Max recovery reached in {}".format(phase)) - # ThreeCompVisualisation(agent) - break - - # display fill levels after recovery + + if func is None: + # first distinguish between fAe and lAe + if h >= 1 - phi: + # fAe + if h < theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe + # fAe_rAnS + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe_rAn + # fAe_lAnS + elif h > g + theta and h < 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_lAn + # fAe_fAnS + elif h > 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, + phi)) + else: + # lAr + if h < theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe_rAn + elif h > g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_lAn + elif h > 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, + phi)) + + if log_level > 1: + logging.info("[intermediate result] Try {}".format(func)) + + # save previous time to estimate time difference + t_p = t + + # iterate through all phases until end is reached + t, h, g, n_func = func(t, h, g, p_rec, t_max=t_max, conf=conf) + + # double-check with discrete agent + for _ in range(int(round((t - t_p) * hz))): + agent.set_power(p_rec) + agent.perform_one_step() + g_diff = agent.get_g() - g + h_diff = agent.get_h() - h + + if log_level > 1: + logging.info("[intermediate result] {}\n" + "t {}\n" + "h {} g {}\n" + "Diff h {}\n" + "Diff g {}".format(func, t, h, g, h_diff, g_diff)) + ThreeCompVisualisation(agent) + + assert abs(g_diff) < eps, "{} g is off by {}".format(func, g_diff) + assert abs(h_diff) < eps, "{} h is off by {}".format(func, h_diff) + + if t >= t_rec: + logging.info("Max recovery reached in {}".format(func)) + # ThreeCompVisualisation(agent) + break + + if n_func is None: + logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(func, t, h, g)) + break + + func = n_func + if log_level > 0: + # display fill levels after recovery logging.info("[SUCCESS] Agent after recovery at {} for {}".format(p_rec, t_rec)) ThreeCompVisualisation(agent) @@ -114,7 +151,7 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, udp = MultiObjectiveThreeCompUDP(None, None) example_conf = udp.create_educated_initial_guess() logging.info(example_conf) - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + rec_phase_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=example_conf) @@ -125,17 +162,17 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, p_exp = 260 t_rec = 3600 - p_rec = 247 + p_rec = 50 t_max = 5000 # estimations per second for discrete agent - hz = 400 + hz = 100 # required precision of discrete to differential agent eps = 0.001 # a configuration - c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, - 0.14892228099402588, 0.2580228306857272, 0.2580228306857272] - rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + c = [15609.954911559335, 77867.72175219007, 157.09776309888684, 299.30113671701247, 26.974910527358077, + 0.21094717109761837, 0.4588963905306298, 0.45576180870297783] + rec_phase_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 2c64aa8..86e1ff0 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -82,7 +82,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): logging.info("next PHASE {}".format(func)) # exit loop if end is reached - if t >= t_max: + if t >= t_max or n_func is None: logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(func, t, h, g)) break @@ -137,14 +137,14 @@ def the_loop(p: float = 350.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p = 560 + p = 260 # estimations per second for discrete agent hz = 500 # required precision of discrete to differential agent eps = 0.001 - example_conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, - 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] + example_conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, + 0.14892228099402588, 0.2580228306857272, 0.2580228306857272] tte_test_procedure(p, hz, eps, example_conf, log_level=1) the_loop(p=p, hz=hz, eps=eps) From ae5d9bc69fc5eca75ded22302814dd3142864502 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 11 Oct 2021 21:14:36 +1100 Subject: [PATCH 50/71] finish rec trial tests with new structure --- .../simulator/ode_three_comp_hyd_simulator.py | 451 +++--------------- tests/comparison_tests.py | 12 +- tests/configurations.py | 8 +- tests/rec_phase_tests.py | 2 +- tests/rec_trial_tests.py | 18 +- 5 files changed, 81 insertions(+), 410 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 4c54afc..9aa7e8d 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -11,83 +11,43 @@ class ODEThreeCompHydSimulator: # precision epsilon for threshold checks eps = 0.000001 - max_time = 5000 @staticmethod - def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: float) -> float: - - t_max = ODEThreeCompHydSimulator.max_time + def get_recovery_ratio_wb1_wb2(conf: list, p_exp: float, p_rec: float, t_rec: float, t_max: float = 5000) -> float: # Start with first time to exhaustion bout - tte_1, h, g = ODEThreeCompHydSimulator.tte(conf=conf, - start_h=0, start_g=0, - p_exp=p_exp, t_max=t_max) + tte_1, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=0, start_g=0, + p=p_exp, t_max=t_max) if tte_1 >= t_max: # Exhaustion not reached during TTE return 200 # now recovery - rec, h, g = ODEThreeCompHydSimulator.rec(conf=conf, - start_h=h, start_g=g, - p_rec=p_rec, t_max=t_rec) + rec, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=h, start_g=g, + p=p_rec, t_max=t_rec) # and work bout two - tte_2, h, g = ODEThreeCompHydSimulator.tte(conf=conf, - start_h=h, start_g=g, - p_exp=p_exp, t_max=t_max) + tte_2, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=h, start_g=g, + p=p_exp, t_max=t_max) return tte_2 / tte_1 * 100.0 @staticmethod - def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = 5000) -> (float, float, float): + def constant_power_trial(conf: list, start_h: float, start_g: float, p: float, t_max: float = 5000) -> ( + float, float, float): # start with fully reset agent t, h, g = 0, start_h, start_g - theta = conf[5] - gamma = conf[6] - phi = conf[7] - func = None while t < t_max: if func is None: - # first distinguish between fAe and lAe - if h >= 1 - phi: - # fAe - if h <= theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe - # fAe_rAnS - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe_rAn - # fAe_lAnS - elif h >= g + theta and h <= 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_lAn - # fAe_fAnS - elif h >= 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, - gamma, - phi)) - else: - # lAr - if h <= theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe_rAn - elif h >= g + theta and h <= 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_lAn - elif h >= 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, - gamma, - phi)) - + func = ODEThreeCompHydSimulator.state_to_phase(conf, h, g) # iterate through all phases until end is reached - t, h, g, n_func = func(t, h, g, p_exp, t_max=t_max, conf=conf) + t, h, g, n_func = func(t, h, g, p, t_max=t_max, conf=conf) func = n_func # if recovery time is reached return fill levels at that point @@ -97,6 +57,49 @@ def tte(conf: list, start_h: float, start_g: float, p_exp: float, t_max: float = # if all phases complete full exhaustion is reached return t, h, g + @staticmethod + def state_to_phase(conf: list, h: float, g: float): + + theta = conf[5] + gamma = conf[6] + phi = conf[7] + + # first distinguish between fAe and lAe + if h >= 1 - phi: + # fAe + if h <= theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe + # fAe_rAnS + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.fAe_rAn + # fAe_lAnS + elif h >= g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_lAn + # fAe_fAnS + elif h >= 1 - gamma: + func = ODEThreeCompHydSimulator.fAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, + gamma, + phi)) + else: + # lAr + if h <= theta and g < ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe + elif h < g + theta and g > ODEThreeCompHydSimulator.eps: + func = ODEThreeCompHydSimulator.lAe_rAn + elif h >= g + theta and h <= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_lAn + elif h >= 1 - gamma: + func = ODEThreeCompHydSimulator.lAe_fAn + else: + raise UserWarning( + "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, + gamma, + phi)) + return func + @staticmethod def lAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): """ @@ -643,346 +646,6 @@ def ht(t): return t_p + t_end, ht(t_end), gt(t_end), func - @staticmethod - def rec(conf: list, start_h: float, start_g: float, p_rec: float = 0.0, t_max: float = 5000.0) -> ( - float, float, float): - - # all recovery phases in order - phases = [ODEThreeCompHydSimulator.rec_fAe_fAnS, - ODEThreeCompHydSimulator.rec_lAe_fAnS, - ODEThreeCompHydSimulator.fAe_lAn, - ODEThreeCompHydSimulator.fAe_rAn, - ODEThreeCompHydSimulator.lAe_lAn, - ODEThreeCompHydSimulator.lAe_rAn, - ODEThreeCompHydSimulator.fAe, - ODEThreeCompHydSimulator.lAe] - - # start time from 0 and given start fill level - t = 0 - h, g = start_h, start_g - - # cycle through phases until t_max is reached - while t < t_max: - for phase in phases: - t, h, g = phase(t, h, g, p_rec, t_max=t_max, conf=conf) - - # if recovery time is reached return fill levels at that point - if t == np.nan or t >= t_max: - return t, h, g - - # if all phases complete full recovery is reached - return t, h, g - - @staticmethod - def rec_fAe_fAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - """ - recovery from exhaustive exercise. - :param t_s: time in seconds at which recovery starts - :param h_s: depletion state of AnF when recovery starts - :param g_s: depletion state of AnS when recovery starts - :param p_rec: constant recovery intensity - :param t_max: the maximal recovery time - :param conf: hydraulic model configuration - :return: [rt5 = min(time at which A6 rec ends, t_rec), h(rt5), g(rt5)] - """ - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # A6 rec ends either at beginning of A4 or A5 - h_target = max(1 - gamma, 1 - phi) - - # check whether phase is applicable or if h is - # already at the end of the phase - if abs(h_s - h_target) < ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - # g(t_s) = g_s can be solved for c - s_cg = (g_s - (1 - theta - gamma)) / np.exp(-m_ans * t_s / ((1 - theta - gamma) * a_ans)) - - # generalised g(t) - def gt(t): - return (1 - theta - gamma) + s_cg * np.exp(-m_ans * t / ((1 - theta - gamma) * a_ans)) - - k = m_ans / ((1 - theta - gamma) * a_ans) - # a = -m_ae / a_anf - b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - # g = p / a_anf - ag = (p_rec - m_ae) / a_anf - - # h(t_s) = h_s can be solved for c - s_ch = -t_s * ag + b * math.exp(-k * t_s) / k + h_s - - # generalised h(t) - def ht(t): - return t * ag - b * math.exp(-k * t) / k + s_ch - - # estimate an initial guess that assumes no contribution from g - t_end = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - h_target, - initial_guess=t_s, - max_steps=t_max) - h_end = min(ht(t_end), 1.0) - return t_end, h_end, gt(t_end) - - @staticmethod - def rec_lAe_fAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - """ - recovery from exhaustive exercise. - :param t_s: time in seconds at which recovery starts - :param h_s: depletion state of AnF when recovery starts - :param g_s: depletion state of AnS when recovery starts - :param p_rec: constant recovery intensity - :param t_max: the maximal recovery time - :param conf: hydraulic model configuration - :return: [rt4 = min(time at which A5 rec ends, t_rec), h(rt4), g(rt4)] - """ - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # in this recovery phase h is assumed to continuously increase - # this recovery phase ends when h reaches pipe exit of AnS - h_target = 1 - gamma - - # this phase is not applicable if phi is greater or equal to gamma... - # ... or if h is already above the end of the phase - if phi >= gamma or h_s <= h_target - ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - # g(t_s) = g_s can be solved for c - s_cg = (g_s - (1 - theta - gamma)) * np.exp((m_ans * t_s) / ((1 - theta - gamma) * a_ans)) - - # generalised g(t) - def gt(t): - return (1 - theta - gamma) + s_cg * np.exp((-m_ans * t) / ((1 - theta - gamma) * a_ans)) - - # as defined for EQ(21) - k = m_ans / ((1 - theta - gamma) * a_ans) - a = -m_ae / ((1 - phi) * a_anf) - g = p_rec / a_anf - b = m_ans * s_cg / ((1 - theta - gamma) * a_anf) - - # find c that matches h(t_s) = h_s - s_ch = (h_s + b / (a + k) * np.exp(-k * t_s) + g / a) * np.exp(-a * t_s) - - def ht(t): - return -b / (a + k) * np.exp(-k * t) + s_ch * np.exp(a * t) - g / a - - # find the time at which the phase stops - t_ht = ODEThreeCompHydSimulator.optimize(func=lambda t: ht(t) - h_target, - initial_guess=t_s, - max_steps=t_max) - - # phase also ends if h drops back to 1-phi changing lAe into fAe - t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: (1 - phi) - ht(t), - initial_guess=t_s, - max_steps=t_max) - t_end = min(t_ht, t_phi) - return t_end, ht(t_end), gt(t_end) - - @staticmethod - def rec_fAe_lAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # phase full Ae and limited AnS is only applicable h below pipe exit of Ae - # ... and above AnS ... - # ... and if g is above h (allows error of epsilon) - if h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ - or h_s < 1 - phi - ODEThreeCompHydSimulator.eps \ - or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - # if g is above h (flow from AnS into AnF) - a_gh = (a_anf + a_ans) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - b_gh = (p_rec - m_ae) * m_ans / (a_anf * a_ans * (1 - theta - gamma)) - - # derivative g'(t4) can be calculated manually - dgt4_gh = m_ans * (h_s - g_s - theta) / (a_ans * (1 - theta - gamma)) - - # ... which then allows to derive c1 and c2 - s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_gh) * np.exp(a_gh * t_s) - s_c2_gh = (-t_s * b_gh + dgt4_gh) / a_gh - (p_rec - m_ae) / ((a_anf + a_ans) * a_gh) + g_s - - def a4_gt(t): - # general solution for g(t) - return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_gh * np.exp(-a_gh * t) - - def a4_dgt(t): - # first derivative g'(t) - return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_gh * t) - - def a4_ht(t): - # EQ(9) with constants for g(t) and g'(t) - return a_ans * (1 - theta - gamma) / m_ans * a4_dgt(t) + a4_gt(t) + theta - - initial_guess = t_s - - # phase ends when g drops below h... - t_gth = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (theta + a4_gt(t)), - initial_guess=initial_guess, - max_steps=t_max) - - # ...or if h reaches 1-phi changing fAe into lAe - t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - (1 - phi), - initial_guess=initial_guess, - max_steps=t_max) - - # ...or if h drops to 1-gamma changing lAnS into fAnS - t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - a4_ht(t), - initial_guess=initial_guess, - max_steps=t_max) - - # choose minimal time at which this phase ends - t_end = min(t_phi, t_gth, t_gam) - - return t_end, a4_ht(t_end), a4_gt(t_end) - - @staticmethod - def rec_fAe_rAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_anf = conf[4] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # A4 R2 is only applicable if h below pipe exit of Ae... - # ... and h is above g (error of epsilon tolerated) - if h_s <= 1 - phi or \ - h_s >= g_s + theta + ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - # if h is above g simulate flow from AnF into AnS - a_hg = (a_anf + a_ans) * m_anf / (a_anf * a_ans * (1 - gamma)) - b_hg = (p_rec - m_ae) * m_anf / (a_anf * a_ans * (1 - gamma)) - - # derivative g'(t4) can be calculated manually from g4, t4, and h4 - dgt4_hg = - m_anf * (g_s + theta - h_s) / (a_ans * (1 - gamma)) - - # which then allows to derive c1 and c2 - s_c1_gh = ((p_rec - m_ae) / (a_anf + a_ans) - dgt4_hg) * np.exp(a_hg * t_s) - s_c2_gh = (-t_s * b_hg + dgt4_hg) / a_hg - (p_rec - m_ae) / ((a_anf + a_ans) * a_hg) + g_s - - def a4_gt(t): - # general solution for g(t) - return t * (p_rec - m_ae) / (a_anf + a_ans) + s_c2_gh + s_c1_gh / a_hg * np.exp(-a_hg * t) - - def a4_dgt(t): - # first derivative g'(t) - return (p_rec - m_ae) / (a_anf + a_ans) - s_c1_gh * np.exp(-a_hg * t) - - def a4_ht(t): - # EQ(16) with constants for g(t) and g'(t) - return a_ans * (1 - gamma) / m_anf * a4_dgt(t) + a4_gt(t) + theta - - h_target = 1 - phi - - # A4 ends if AnS is completely refilled - t_fill = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_gt(t), - initial_guess=t_s, - max_steps=t_max) - - # A4 also ends by surpassing 1 - phi - t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: a4_ht(t) - h_target, - initial_guess=t_s, - max_steps=t_max) - - # choose minimal time at which phase ends - t_end = min(t_fill, t_phi) - - return t_end, a4_ht(t_end), a4_gt(t_end) - - @staticmethod - def rec_lAe_lAnS(t_s: float, h_s: float, g_s: float, p_rec: float, t_max: float, conf: list): - - a_anf = conf[0] - a_ans = conf[1] - m_ae = conf[2] - m_ans = conf[3] - theta = conf[5] - gamma = conf[6] - phi = conf[7] - - # lAe and lAnS is only applicable if h is above or at pipe exit of Ae... - # ... and above or at pipe exit AnS ... - # ... and g is above h (error of epsilon tolerated) - if h_s > 1 - phi + ODEThreeCompHydSimulator.eps \ - or h_s > 1 - gamma + ODEThreeCompHydSimulator.eps \ - or h_s < g_s + theta - ODEThreeCompHydSimulator.eps: - return t_s, h_s, g_s - - # my simplified form - a = m_ae / (a_anf * (1 - phi)) + \ - m_ans / (a_ans * (1 - theta - gamma)) + \ - m_ans / (a_anf * (1 - theta - gamma)) - - b = m_ae * m_ans / \ - (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - - # c' for p_rec - c = m_ans * (p_rec * (1 - phi) - m_ae * theta) / \ - (a_anf * a_ans * (1 - phi) * (1 - theta - gamma)) - - # wolfram alpha gave these estimations as solutions for l''(t) + a*l'(t) + b*l(t) = c - r1 = 0.5 * (-np.sqrt(a ** 2 - 4 * b) - a) - r2 = 0.5 * (np.sqrt(a ** 2 - 4 * b) - a) - - # uses Al dt/dl part of EQ(8) solved for c2 - # r1 * c1 * exp(r1*t3) + r2 * c2 * exp(r2*t3) = m_ans * (ht3 - gt3 - theta)) / (a_ans * r2 * (1 - theta - gamma)) - # and then substituted in EQ(14) and solved for c1 - s_c1 = (c / b + (m_ans * (h_s - g_s - theta)) / (a_ans * r2 * (1 - theta - gamma)) - g_s) / \ - (np.exp(r1 * t_s) * (r1 / r2 - 1)) - - # uses EQ(14) with solution for c1 and solves for c2 - s_c2 = (g_s - s_c1 * np.exp(r1 * t_s) - c / b) / np.exp(r2 * t_s) - - def a3_gt(t): - # the general solution for g(t) - return s_c1 * np.exp(r1 * t) + s_c2 * np.exp(r2 * t) + c / b - - # substitute into EQ(9) for h - def a3_ht(t): - k1 = a_ans * (1 - theta - gamma) / m_ans * s_c1 * r1 + s_c1 - k2 = a_ans * (1 - theta - gamma) / m_ans * s_c2 * r2 + s_c2 - return k1 * np.exp(r1 * t) + k2 * np.exp(r2 * t) + c / b + theta - - # find the point where h(t) == g(t) ... - t_gh = ODEThreeCompHydSimulator.optimize(func=lambda t: a3_ht(t) - (a3_gt(t) + theta), - initial_guess=t_s, - max_steps=t_max) - - # ... or where h(t) drops back to 1 - gamma, changing lAnS to fAnS - t_gam = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - gamma - a3_ht(t), - initial_guess=t_s, - max_steps=t_max) - - # ... or where h(t) drops back to 1 - phi, changing lAe to fAe - t_phi = ODEThreeCompHydSimulator.optimize(func=lambda t: 1 - phi - a3_ht(t), - initial_guess=t_s, - max_steps=t_max) - - t_end = min(t_gh, t_gam, t_phi) - return t_end, a3_ht(t_end), a3_gt(t_end) - @staticmethod def optimize(func, initial_guess, max_steps): """ diff --git a/tests/comparison_tests.py b/tests/comparison_tests.py index ae4f777..86fbc41 100644 --- a/tests/comparison_tests.py +++ b/tests/comparison_tests.py @@ -61,8 +61,16 @@ def the_loop(t_max: float = 5000, hz: int = 250): except UserWarning: continue + mean_t = np.mean(t_results) + str_mean_t = "" + if mean_t > 0: + str_mean_t += "ODE" + else: + str_mean_t += "Iterative" + str_mean_t += " is faster with {}".format(mean_t) + logging.info("\nsuccessful samples: {}\ntime performance: {}\nest error {}".format( - len(t_results), np.mean(t_results), np.mean(e_results))) + len(t_results), str_mean_t, np.mean(e_results))) if __name__ == "__main__": @@ -71,7 +79,7 @@ def the_loop(t_max: float = 5000, hz: int = 250): format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # estimations per second for discrete agent - hz = 2 + hz = 5 t_max = 5000 the_loop(t_max=t_max, hz=hz) diff --git a/tests/configurations.py b/tests/configurations.py index c9a6ca4..9f462ee 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -71,7 +71,7 @@ # ThreeCompVisualisation(agent) # Start with first time to exhaustion bout - tte, h_tte, g_tte = ODEThreeCompHydSimulator.tte(p_exp=p_exp, conf=conf, start_h=0, start_g=0) + tte, h_tte, g_tte = ODEThreeCompHydSimulator.constant_power_trial(p=p_exp, conf=conf, start_h=0, start_g=0) # double-check with discrete agent for _ in range(int(round(tte * hz))): @@ -86,9 +86,9 @@ # ThreeCompVisualisation(agent) # Now recovery - rec, h_rec, g_rec = ODEThreeCompHydSimulator.rec(conf=conf, start_h=h_tte, - start_g=g_tte, p_rec=p_rec, - t_max=rec_time) + rec, h_rec, g_rec = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, start_h=h_tte, + start_g=g_tte, p=p_rec, + t_max=rec_time) # double-check with discrete agent for _ in range(int(round(rec * hz))): diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 02cb169..86b080d 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -31,7 +31,7 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, ThreeCompVisualisation(agent) # Start with first time to exhaustion bout - t, h, g = ODEThreeCompHydSimulator.tte(p_exp=p_exp, start_h=0, start_g=0, conf=conf, t_max=t_max) + t, h, g = ODEThreeCompHydSimulator.constant_power_trial(p=p_exp, start_h=0, start_g=0, conf=conf, t_max=t_max) if t >= t_max: logging.info("Exhaustion not reached during TTE") diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 5892626..3342246 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -17,9 +17,9 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): ThreeCompVisualisation(agent) # Start with first time to exhaustion bout - tte_1, h, g = ODEThreeCompHydSimulator.tte(conf=conf, - start_h=0, start_g=0, - p_exp=p_exp, t_max=t_max) + tte_1, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=0, start_g=0, + p=p_exp, t_max=t_max) if tte_1 >= t_max: logging.info("Exhaustion not reached during TTE") @@ -38,9 +38,9 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): logging.info("TTE1 {} h: {} g: {}".format(tte_1, h, g)) ThreeCompVisualisation(agent) - rec, h, g = ODEThreeCompHydSimulator.rec(conf=conf, - start_h=h, start_g=g, - p_rec=p_rec, t_max=t_rec) + rec, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=h, start_g=g, + p=p_rec, t_max=t_rec) # double-check with discrete agent for _ in range(int(round(rec * hz))): @@ -55,9 +55,9 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): logging.info("REC {} h: {} g: {}".format(rec, h, g)) ThreeCompVisualisation(agent) - tte_2, h, g = ODEThreeCompHydSimulator.tte(conf=conf, - start_h=h, start_g=g, - p_exp=p_exp, t_max=t_max) + tte_2, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=h, start_g=g, + p=p_exp, t_max=t_max) # double-check with discrete agent for _ in range(int(round(tte_2 * hz))): From ce9ad1e6d91173160c359966c100992025f772ed Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 13 Oct 2021 18:35:20 +1100 Subject: [PATCH 51/71] add phase rAn and improve next phase determination of most phases --- .../agents/three_comp_hyd_agent.py | 66 ++++---- .../evolutionary_fitter/three_comp_tools.py | 4 +- .../simulator/ode_three_comp_hyd_simulator.py | 68 ++++++-- .../simulator/three_comp_hyd_simulator.py | 34 ++-- tests/comparison_tests.py | 9 +- tests/configurations.py | 160 ++++++++++++++---- tests/phase_fAe.py | 4 +- tests/phase_rAn.py | 25 +++ tests/rec_phase_tests.py | 20 ++- tests/rec_trial_tests.py | 67 +++++--- tests/tte_phase_tests.py | 27 ++- 11 files changed, 350 insertions(+), 134 deletions(-) create mode 100644 tests/phase_rAn.py diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 1e916ab..a32a7e1 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -30,8 +30,7 @@ def __init__(self, hz, a_anf, a_ans, m_ae, m_ans, m_anf, the, gam, phi): # height of vessel AnS self.__height_ans = 1 - self.__theta - self.__gamma - # the AnS tank size constraint that corresponds to - # constraint: theta < 1 - phi + # the AnS tank size cannot be negative if self.__height_ans <= 0: raise UserWarning("AnS has negative height: Theta {} Gamma {} Phi {}".format(the, gam, phi)) @@ -68,6 +67,37 @@ def __str__(self): "{}".format([self.__a_anf, self.__a_ans, self.__m_ae, self.__m_ans, self.__m_anf, self.__theta, self.__gamma, self.__phi]) + def __raise_detailed_error_report(self): + """ + raises a UserWarning exception with info about configuration, fill states, and power demands + """ + raise UserWarning("Unhandled tank fill level state \n" + "gamma: {} \n " + "theta: {} \n " + "phi: {} \n " + "AnF: {} \n " + "AnS: {} \n " + "g: {} \n " + "h: {} \n " + "m_ae: {} \n" + "m_ans: {} \n" + "m_anf: {} \n" + "p_Ae: {} \n" + "p_An: {} \n" + "pow: {} \n".format(self.__gamma, + self.__theta, + self.__phi, + self.__a_anf, + self.__a_ans, + self.__g, + self.__h, + self.__m_ae, + self.__m_ans, + self.__m_anf, + self.__p_ae, + self.__p_an, + self._pow)) + def _estimate_possible_power_output(self): """ Estimates liquid flow to meet set power demands. Exhaustion flag is set and internal tank fill levels and @@ -75,34 +105,6 @@ def _estimate_possible_power_output(self): :return: power output """ - def raise_detailed_error_report(): - raise UserWarning("Unhandled tank fill level state \n" - "gamma: {} \n " - "theta: {} \n " - "phi: {} \n " - "AnF: {} \n " - "AnS: {} \n " - "g: {} \n " - "h: {} \n " - "m_ae: {} \n" - "m_ans: {} \n" - "m_anf: {} \n" - "p_Ae: {} \n" - "p_An: {} \n" - "pow: {} \n".format(self.__gamma, - self.__theta, - self.__phi, - self.__a_anf, - self.__a_ans, - self.__g, - self.__h, - self.__m_ae, - self.__m_ans, - self.__m_anf, - self.__p_ae, - self.__p_an, - self._pow)) - # step 1: drop level in AnF according to power demand # estimate h_{t+1}: scale to hz (delta t) and drop the level of AnF self.__h += self._pow / self.__a_anf / self._hz @@ -119,7 +121,7 @@ def raise_detailed_error_report(): # max contribution R1 = m_ae self.__p_ae = self.__m_ae else: - raise_detailed_error_report() + self.__raise_detailed_error_report() # multiply with delta t1 self.__p_ae = self.__p_ae / self._hz @@ -149,7 +151,7 @@ def raise_detailed_error_report(): # EQ (20) Morton (1986) self.__p_an = self.__m_ans * (self.__height_ans - self.__g) / self.__height_ans else: - raise_detailed_error_report() + self.__raise_detailed_error_report() # This check is added to handle cases where the flow causes level height swaps between AnS and AnF self.__m_flow = ((self.__h - (self.__g + self.__theta)) / ( diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index 248bbfe..c4ecd50 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -159,8 +159,8 @@ def three_comp_two_objective_functions(obj_vars, hz: int, for tte_t, tte_p in ttes.iterate_pairs(): # use the simulator try: - tte = ThreeCompHydSimulator.do_a_tte(agent=three_comp_agent, - p_exp=tte_p) + tte = ThreeCompHydSimulator.tte(agent=three_comp_agent, + p_work=tte_p) except UserWarning: tte = 5000 # square time difference diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index 9aa7e8d..e93011d 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -80,9 +80,8 @@ def state_to_phase(conf: list, h: float, g: float): func = ODEThreeCompHydSimulator.fAe_fAn else: raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, - gamma, - phi)) + "unhandled state with h {} g {} and " + "conf theta {} gamma {} phi {}".format(h, g, theta, gamma, phi)) else: # lAr if h <= theta and g < ODEThreeCompHydSimulator.eps: @@ -95,9 +94,8 @@ def state_to_phase(conf: list, h: float, g: float): func = ODEThreeCompHydSimulator.lAe_fAn else: raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, - gamma, - phi)) + "unhandled state with h {} g {} and " + "conf theta {} gamma {} phi {}".format(h, g, theta, gamma, phi)) return func @staticmethod @@ -254,7 +252,10 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): elif ht_p < 0: # recovery ends at 1 - phi h_target = 1 - phi - func = ODEThreeCompHydSimulator.lAe + if h_target > 0: + func = ODEThreeCompHydSimulator.lAe + else: + func = None # recovery is finished if pipe exit Ae is at 0 else: # no change return t_max, h_s, g_s, None @@ -269,6 +270,39 @@ def fAe(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): else: return t_end, h_target, g_s, func + @staticmethod + def rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): + """ + This recovery phase only occurs in configurations A,B,C,D when AnF is fully refilled + but flow into AnS is still going + """ + a_ans = conf[1] + m_ae = conf[2] + m_anf = conf[4] + theta = conf[5] + gamma = conf[6] + + if p > m_ae: + raise UserWarning("This phase cannot be reached with p {} and m_ae {}".format(p, m_ae)) + + c1 = (g_s + theta) * np.exp(-m_anf * t_s / (a_ans * (gamma - 1))) + + # the time g reaches 0 + if theta > 0: + # only defined for theta > 0 because of log + t_end = np.log(theta / c1) * a_ans * (gamma - 1) / m_anf + else: + # if theta is 0 g will never reach 0 due to exp + t_end = t_max + + # if t_end after t_max, return g(t_max) + if t_end > t_max: + gtmax = c1 * np.exp(m_anf * t_max / (a_ans * (gamma - 1))) - theta + return t_max, h_s, gtmax, None + else: + # otherwise g is 0 + return t_max, h_s, 0, None + @staticmethod def fAe_rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): @@ -338,7 +372,13 @@ def ht(t): elif t_end == t_gtht: func = ODEThreeCompHydSimulator.fAe_lAn else: - func = ODEThreeCompHydSimulator.lAe_rAn + # else the target was 1-phi + if phi == 1: + # if Ae is at the top of AnF + func = ODEThreeCompHydSimulator.rAn + else: + # transition into lAe + func = ODEThreeCompHydSimulator.lAe_rAn return t_end + t_p, ht(t_end), gt(t_end), func @@ -421,8 +461,8 @@ def ht(t): t_end = min(t_gh, t_gam, t_phi) # Determine what the current phase lAe_lAn transitions into - if t_end >= t_max: - func = None + if t_end >= t_max or abs(ht(t_end) - 1) < ODEThreeCompHydSimulator.eps: + func = None # max time is reached or exhaustion is reached elif t_end == t_gh: func = ODEThreeCompHydSimulator.lAe_rAn elif t_end == t_gam: @@ -495,8 +535,8 @@ def ht(t): t_end = min(t_phi, t_gth, t_gam) # Determine what the current phase fAe_lAn transitions into - if t_end >= t_max: - func = None + if t_end >= t_max or abs(ht(t_end) - 1) < ODEThreeCompHydSimulator.eps: + func = None # max time is reached or exhaustion is reached elif t_end == t_gth: func = ODEThreeCompHydSimulator.fAe_rAn elif t_end == t_gam: @@ -566,8 +606,8 @@ def ht(t): t_end = min(t_gam, t_phi) # Determine what the current phase lAe_lAn transitions into - if t_end >= t_max: - func = None + if t_end >= t_max or abs(ht(t_end) - 1) < ODEThreeCompHydSimulator.eps: + func = None # max time is reached or exhaustion is reached elif t_end == t_gam: func = ODEThreeCompHydSimulator.lAe_lAn else: diff --git a/src/threecomphyd/simulator/three_comp_hyd_simulator.py b/src/threecomphyd/simulator/three_comp_hyd_simulator.py index 0a06b9d..d7a8a77 100644 --- a/src/threecomphyd/simulator/three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/three_comp_hyd_simulator.py @@ -11,26 +11,32 @@ class ThreeCompHydSimulator: TTE tests """ - step_limit = 5000 - @staticmethod - def do_a_tte(agent: ThreeCompHydAgent, p_exp, step_function=None): + def tte(agent: ThreeCompHydAgent, p_work: float, start_h: float = 0, + start_g: float = 0, t_max: float = 5000, step_function=None): """ a normal time to exhaustion test - :param agent: - :param p_exp: - :param step_function: + :param agent: iterative agent + :param p_work: constant expenditure intensity for TTE + :param start_h: fill level of AnF at start + :param start_g: fill level of AnS at start + :param t_max: maximal time (steps * hz) until warning "exhaustion not reached" is raised + :param step_function: function of agent to estimate one time step. Default is perform_one_step :return: """ agent.reset() + agent.set_h(start_h) + agent.set_g(start_g) + + step_limit = t_max * agent.hz if step_function is None: step_function = agent.perform_one_step # WB1 Exhaust... - agent.set_power(p_exp) + agent.set_power(p_work) steps = 0 - while not agent.is_exhausted() and steps < ThreeCompHydSimulator.step_limit: + while not agent.is_exhausted() and steps < step_limit: step_function() steps += 1 wb1_t = agent.get_time() @@ -41,7 +47,7 @@ def do_a_tte(agent: ThreeCompHydAgent, p_exp, step_function=None): return wb1_t @staticmethod - def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec): + def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec, t_max=5000): """ Returns recovery ratio of given agent according to WB1 -> RB -> WB2 protocol. Recovery ratio estimations for given exp, rec intensity and time @@ -49,16 +55,19 @@ def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec): :param p_exp: work bout intensity :param p_rec: recovery bout intensity :param t_rec: recovery bout duration + :param t_max: maximal time (steps * hz) until warning "exhaustion not reached" is raised :return: ratio in percent """ hz = agent.hz agent.reset() + step_limit = t_max * hz + # WB1 Exhaust... agent.set_power(p_exp) steps = 0 - while not agent.is_exhausted() and steps < ThreeCompHydSimulator.step_limit: + while not agent.is_exhausted() and steps < step_limit: agent.perform_one_step() steps += 1 wb1_t = agent.get_time() @@ -68,17 +77,18 @@ def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec): # Recover... agent.set_power(p_rec) - for _ in range(0, int(t_rec * hz)): + for _ in range(0, int(round(t_rec * hz))): agent.perform_one_step() rec_t = agent.get_time() # WB2 Exhaust... agent.set_power(p_exp) steps = 0 - while not agent.is_exhausted() and steps < ThreeCompHydSimulator.step_limit: + while not agent.is_exhausted() and steps < step_limit: agent.perform_one_step() steps += 1 wb2_t = agent.get_time() + # return ratio of times as recovery ratio return ((wb2_t - rec_t) / wb1_t) * 100.0 diff --git a/tests/comparison_tests.py b/tests/comparison_tests.py index 86fbc41..598974f 100644 --- a/tests/comparison_tests.py +++ b/tests/comparison_tests.py @@ -4,7 +4,6 @@ from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP from threecomphyd.simulator.three_comp_hyd_simulator import ThreeCompHydSimulator -from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator import logging @@ -17,14 +16,14 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, conf, log_level=0): gam=conf[6], phi=conf[7]) # simulator step limit needs to be adjusted - ThreeCompHydSimulator.step_limit = t_max * hz est_t0 = time.process_time_ns() - est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, p_rec=p_rec, t_rec=t_rec) + est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, p_rec=p_rec, + t_rec=t_rec, t_max=t_max) est_t = time.process_time_ns() - est_t0 - ThreeCompHydSimulator.step_limit = t_max / hz ode_t0 = time.process_time_ns() - rec_ratio = ODEThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(conf=conf, p_exp=p_exp, p_rec=p_rec, t_rec=t_rec) + rec_ratio = ODEThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(conf=conf, p_exp=p_exp, p_rec=p_rec, + t_rec=t_rec, t_max=t_max) ode_t = time.process_time_ns() - ode_t0 # rec ratio estimation difference diff --git a/tests/configurations.py b/tests/configurations.py index 9f462ee..d6e2a1c 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -3,52 +3,131 @@ from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator +from threecomphyd.simulator.three_comp_hyd_simulator import ThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation -# an A configuration +# All configurations with phi = 1.0 a = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 38.27073086773415, 0.14892228099402588, # m_anf, theta - 0.3524379644134216, 0.1580228306857272] # gamma, phi -# a B configuration + 0.3524379644134216, 1.0] # gamma, phi + b = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.0, # m_anf, theta + 0.3524379644134216, 1.0] # gamma, phi + +c = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.5, # m_anf, theta + 0.0, 1.0] # gamma, phi + +d = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.0, # m_anf, theta + 0.0, 1.0] # gamma, phi + +# All configurations with phi = 0 +e = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.3, # m_anf, theta + 0.3, 0.0] # gamma, phi + +f = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.0, # m_anf, theta + 0.3, 0.0] # gamma, phi + +g = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.6, # m_anf, theta + 0.0, 0.0] # gamma, phi + +h = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.0, # m_anf, theta + 0.0, 0.0] # gamma, phi + +# All configurations with phi <= gamma and 1>phi>0 +i = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.0, # m_anf, theta + 0.3524379644134216, 0.1580228306857272] # gamma, phi + +j = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.14892228099402588, # m_anf, theta + 0.3524379644134216, 0.1580228306857272] # gamma, phi + +k = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.0, # m_anf, theta + 0.1580228306857272, 0.1580228306857272] # gamma, phi + +l = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 38.27073086773415, 0.14892228099402588, # m_anf, theta 0.2580228306857272, 0.2580228306857272] # gamma, phi -# a C configuration -c = [15101.24769778409, 86209.27743067988, # anf, ans + +# All configurations with phi > gamma and gamma < 1 +m = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 38.27073086773415, 0.0, # m_anf, theta + 0.1580228306857272, 0.3580228306857272] # gamma, phi + +n = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 38.27073086773415, 0.14892228099402588, # m_anf, theta 0.1580228306857272, 0.3580228306857272] # gamma, phi -# a D configuration -d = [15101.24769778409, 86209.27743067988, # anf, ans + +o = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 1 - 0.6580228306857272, # m_anf, theta + 0.1580228306857272, 0.6580228306857272] # gamma, phi + +p = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 380.27073086773415, 0.64892228099402588, # m_anf, theta 0.1580228306857272, 0.6580228306857272] # gamma, phi -# a configuration where AnS has size of 1.0 -e = [15101.24769778409, 86209.27743067988, # anf, ans + +# Configurations with gamma = 0 +q = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans 380.27073086773415, 0.0, # m_anf, theta 0.0, 0.6580228306857272] # gamma, phi -# a configuration where Ae has size of 1.0 -f = [15101.24769778409, 86209.27743067988, # anf, ans + +r = [15101.24769778409, 86209.27743067988, # anf, ans 252.71702367096787, 363.2970828395908, # m_ae, m_ans - 380.27073086773415, 0.3, # m_anf, theta - 0.3, 0.0] # gamma, phi + 380.27073086773415, 0.2, # m_anf, theta + 0.0, 0.6580228306857272] # gamma, phi + +s = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.6, # m_anf, theta + 0.0, 0.4] # gamma, phi + +t = [15101.24769778409, 86209.27743067988, # anf, ans + 252.71702367096787, 363.2970828395908, # m_ae, m_ans + 380.27073086773415, 0.6, # m_anf, theta + 0.0, 0.6] # gamma, phi if __name__ == "__main__": # set logging level to highest level logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - hz = 250 # delta t - eps = 0.001 # required precision + hz = 1000 # delta t + eps = 0.1 # required precision + t_max = 2000 # setting combinations p_exps = [260, 681] rec_times = [10, 240, 3600] p_recs = [0, 247] - configs = [a, b, c, d] # e, f] + configs = [a, b, c, d, + e, f, g, h, + m, n, o, p, + q, r, s, t] combs = list(itertools.product(p_exps, rec_times, p_recs, configs)) @@ -68,10 +147,10 @@ m_ae=conf[2], m_ans=conf[3], m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - # ThreeCompVisualisation(agent) # Start with first time to exhaustion bout - tte, h_tte, g_tte = ODEThreeCompHydSimulator.constant_power_trial(p=p_exp, conf=conf, start_h=0, start_g=0) + tte, h_tte, g_tte = ODEThreeCompHydSimulator.constant_power_trial(p=p_exp, conf=conf, start_h=0, + start_g=0, t_max=t_max) # double-check with discrete agent for _ in range(int(round(tte * hz))): @@ -79,19 +158,27 @@ agent.perform_one_step() g_diff = agent.get_g() - g_tte h_diff = agent.get_h() - h_tte - assert abs(g_diff) < eps, "TTE1 g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "TTE1 h is off by {}".format(h_diff) - - # logging.info("TTE END t: {} h: {} g: {}".format(tte, abs(h_diff), abs(g_diff))) - # ThreeCompVisualisation(agent) - - # Now recovery - rec, h_rec, g_rec = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, start_h=h_tte, - start_g=g_tte, p=p_rec, + assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) + + # double-check time to exhaustion + try: + c_tte = ThreeCompHydSimulator.tte(agent=agent, p_work=p_exp, t_max=t_max) + except UserWarning: + c_tte = t_max + assert abs(round(tte) - round(c_tte)) < eps, "TTE ODE {} and IT {} is off by {}".format(round(tte), + round(c_tte), + abs(tte - c_tte)) + + # Recovery behaviour + rec, h_rec, g_rec = ODEThreeCompHydSimulator.constant_power_trial(p=p_rec, conf=conf, + start_h=h_tte, start_g=g_tte, t_max=rec_time) - # double-check with discrete agent - for _ in range(int(round(rec * hz))): + agent.reset() + agent.set_h(h_tte) + agent.set_g(g_tte) + for _ in range(int(round(rec_time * hz))): agent.set_power(p_rec) agent.perform_one_step() g_diff = agent.get_g() - g_rec @@ -99,7 +186,18 @@ assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) - # logging.info("REC END t: {} h: {} g: {}".format(rec, abs(h_diff), abs(g_diff))) - # ThreeCompVisualisation(agent) + # Now a full recovery trial + rec_t = ODEThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(conf=conf, p_exp=p_exp, + p_rec=p_rec, t_rec=rec_time, + t_max=t_max) + try: + c_rec_t = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, + p_rec=p_rec, t_rec=rec_time, + t_max=t_max) + except UserWarning: + c_rec_t = 200 + + assert abs(rec_t - c_rec_t) < eps, "Rec trial ODE {} and IT {} are of by {}".format(rec_t, c_rec_t, + abs(rec_t - c_rec_t)) logging.info("PASSED") diff --git a/tests/phase_fAe.py b/tests/phase_fAe.py index 5660b77..f6846aa 100644 --- a/tests/phase_fAe.py +++ b/tests/phase_fAe.py @@ -29,8 +29,8 @@ def test(func, h, g, p, conf, t=0, hz=500, t_max=2000, log=2): agent.perform_one_step() # verify results - assert abs(h_end - agent.get_h()) < 0.0001 - assert abs(g_end - agent.get_g()) < 0.0001 + assert abs(h_end - agent.get_h()) < 0.0001, "{} vs {}".format(h_end, agent.get_h()) + assert abs(g_end - agent.get_g()) < 0.0001, "{} vs {}".format(g_end, agent.get_g()) # log outputs according to level if log > 0: diff --git a/tests/phase_rAn.py b/tests/phase_rAn.py new file mode 100644 index 0000000..c067fe5 --- /dev/null +++ b/tests/phase_rAn.py @@ -0,0 +1,25 @@ +import logging +from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator + +from tests.phase_fAe import test + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + conf = [15101.24769778409, 86209.27743067988, 252.71702367096787, + 363.2970828395908, 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 1.0] + log = 2 + h = 0 + g = 0.3 + + # lAe has four possible ends + func = ODEThreeCompHydSimulator.rAn + logging.info("Func {}".format(func)) + + p = 0 + logging.info("g = 0") + n_func = test(func, h, g, p, conf, t_max=5000, log=log) + assert n_func is None diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 86b080d..7b52092 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -1,5 +1,6 @@ from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.simulator.three_comp_hyd_simulator import ThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator @@ -103,7 +104,7 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, t_p = t # iterate through all phases until end is reached - t, h, g, n_func = func(t, h, g, p_rec, t_max=t_max, conf=conf) + t, h, g, n_func = func(t, h, g, p_rec, t_max=t_rec, conf=conf) # double-check with discrete agent for _ in range(int(round((t - t_p) * hz))): @@ -128,8 +129,9 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, # ThreeCompVisualisation(agent) break + # exit loop if end of iteration is reached if n_func is None: - logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(func, t, h, g)) + logging.info("END IN {}: t: {} h: {} g: {}".format(func, t, h, g)) break func = n_func @@ -161,17 +163,19 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") p_exp = 260 - t_rec = 3600 - p_rec = 50 - t_max = 5000 + t_rec = 240 + p_rec = 10 + t_max = 6000 + # estimations per second for discrete agent - hz = 100 + hz = 500 # required precision of discrete to differential agent eps = 0.001 # a configuration - c = [15609.954911559335, 77867.72175219007, 157.09776309888684, 299.30113671701247, 26.974910527358077, - 0.21094717109761837, 0.4588963905306298, 0.45576180870297783] + c = [15101.24769778409, 86209.27743067988, 252.71702367096788, + 363.2970828395908, 38.27073086773415, 0.0, 0.3524379644134216, 1.0] + rec_phase_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 3342246..bb25ac2 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -21,10 +21,18 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): start_h=0, start_g=0, p=p_exp, t_max=t_max) + # confirm the tte time with an entire iterative simulation + c_tte = ThreeCompHydSimulator.tte(agent, p_work=p_exp, t_max=t_max) + assert abs(c_tte - tte_1) < eps, "TTE1 confirmation error. Difference between " \ + "ODE TTE {} and Iterative TTE {} is {}".format(tte_1, + c_tte, + abs(c_tte - tte_1)) + if tte_1 >= t_max: logging.info("Exhaustion not reached during TTE") return + agent.reset() # double-check with discrete agent for _ in range(int(round(tte_1 * hz))): agent.set_power(p_exp) @@ -41,9 +49,8 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): rec, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, start_h=h, start_g=g, p=p_rec, t_max=t_rec) - # double-check with discrete agent - for _ in range(int(round(rec * hz))): + for _ in range(int(round(t_rec * hz))): agent.set_power(p_rec) agent.perform_one_step() g_diff = agent.get_g() - g @@ -55,33 +62,53 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): logging.info("REC {} h: {} g: {}".format(rec, h, g)) ThreeCompVisualisation(agent) - tte_2, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, - start_h=h, start_g=g, - p=p_exp, t_max=t_max) - + # confirm the tte time with an entire iterative simulation + tte_2, h2, g2 = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, + start_h=h, start_g=g, + p=p_exp, t_max=t_max) + + c_tte = ThreeCompHydSimulator.tte(agent, start_h=h, start_g=g, + p_work=p_exp, t_max=t_max) + assert abs(c_tte - tte_2) < eps, "TTE2 confirmation error. Difference betwen " \ + "ODE TTE {} and Iterative TTE {} is {}".format(tte_2, + c_tte, + abs(c_tte - tte_2)) + + agent.reset() + agent.set_h(h) + agent.set_g(g) # double-check with discrete agent for _ in range(int(round(tte_2 * hz))): agent.set_power(p_exp) agent.perform_one_step() - g_diff = agent.get_g() - g - h_diff = agent.get_h() - h + g_diff = agent.get_g() - g2 + h_diff = agent.get_h() - h2 assert abs(g_diff) < eps, "TTE2 g is off by {}".format(g_diff) assert abs(h_diff) < eps, "TTE2 h is off by {}".format(h_diff) if log_level > 0: - logging.info("TTE2 {} h: {} g: {}".format(tte_2, h, g)) + logging.info("TTE2 {} h: {} g: {}".format(tte_2, h2, g2)) ThreeCompVisualisation(agent) # simulator step limit needs to be adjusted - ThreeCompHydSimulator.step_limit = 5000 * hz - est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, p_rec=p_rec, t_rec=t_rec) - ThreeCompHydSimulator.step_limit = 5000 / hz - + est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, + p_exp=p_exp, + p_rec=p_rec, + t_rec=t_rec, + t_max=t_max) + + est_ratio_2 = ODEThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(conf=conf, + p_exp=p_exp, + p_rec=p_rec, + t_rec=t_rec, + t_max=t_max) rec_ratio = tte_2 / tte_1 * 100.0 diff = abs(rec_ratio - est_ratio) + diff2 = abs(rec_ratio - est_ratio_2) - logging.info("ODE ratio: {} EST ratio: {}".format(rec_ratio, est_ratio)) + logging.info("ODE ratio: {} Iterative ratio: {} ODE est ratio: {}".format(rec_ratio, est_ratio, est_ratio_2)) assert diff < eps, "Ratio estimations are too different {}".format(diff) + assert diff2 < eps, "Ratio estimations are too different {}".format(diff2) def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, @@ -105,21 +132,21 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 500 + p_exp = 681 t_rec = 240 p_rec = 0 t_max = 5000 # estimations per second for discrete agent - hz = 500 + hz = 1000 # required precision of discrete to differential agent eps = 0.01 # a configuration - c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, - 0.14892228099402588, 0.3524379644134216, 0.1580228306857272] + c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, + 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 1.0] - # rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, - # hz=hz, eps=eps, conf=c, log_level=2) + rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, + hz=hz, eps=eps, conf=c, log_level=2) the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 86e1ff0..f256110 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -1,6 +1,7 @@ import numpy as np from threecomphyd.agents.three_comp_hyd_agent import ThreeCompHydAgent from threecomphyd.evolutionary_fitter.three_comp_tools import MultiObjectiveThreeCompUDP +from threecomphyd.simulator.three_comp_hyd_simulator import ThreeCompHydSimulator from threecomphyd.visualiser.three_comp_visualisation import ThreeCompVisualisation from threecomphyd.simulator.ode_three_comp_hyd_simulator import ODEThreeCompHydSimulator @@ -81,14 +82,14 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): if log_level > 0: logging.info("next PHASE {}".format(func)) - # exit loop if end is reached + # exit loop if end of iteration is reached if t >= t_max or n_func is None: - logging.info("EQUILIBRIUM IN {}: t: {} h: {} g: {}".format(func, t, h, g)) + logging.info("END IN {}: t: {} h: {} g: {}".format(func, t, h, g)) break # if recovery time is reached return fill levels at that point if t == np.nan: - return t, h, g + break # now confirm with iterative agent # set to initial state @@ -114,6 +115,13 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): assert abs(g_diff) < eps, "error phase {}. g is off by {}".format(func, g_diff) assert abs(h_diff) < eps, "error phase {}. h is off by {}".format(func, h_diff) + # confirm the tte time with an entire iterative simulation + c_tte = ThreeCompHydSimulator.tte(agent, p_work=p, t_max=t_max) + assert abs(c_tte - t) < eps, "confirmation error. Difference betwen " \ + "ODE TTE {} and Iterative TTE {} is {}".format(t, + c_tte, + abs(c_tte - t)) + # if all phases complete full exhaustion is reached return t, h, g @@ -137,14 +145,17 @@ def the_loop(p: float = 350.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p = 260 + p = 681 # estimations per second for discrete agent hz = 500 # required precision of discrete to differential agent - eps = 0.001 + eps = 0.01 + + example_conf = [15101.24769778409, 86209.27743067988, + 252.71702367096788, 363.2970828395908, + 38.27073086773415, 0.14892228099402588, + 0.3524379644134216, 1.0] - example_conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, 38.27073086773415, - 0.14892228099402588, 0.2580228306857272, 0.2580228306857272] tte_test_procedure(p, hz, eps, example_conf, log_level=1) - the_loop(p=p, hz=hz, eps=eps) + # the_loop(p=p, hz=hz, eps=eps) From 0e2bad22fcd2dff14dbc520f2d086e6f3b9f2278 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 18 Oct 2021 17:41:52 +1100 Subject: [PATCH 52/71] fix phase rAn t_end check and complete entire configurations test script --- .../simulator/ode_three_comp_hyd_simulator.py | 2 +- tests/configurations.py | 39 ++++++++++------- tests/rec_phase_tests.py | 43 ++----------------- tests/rec_trial_tests.py | 19 ++++---- tests/tte_phase_tests.py | 43 ++----------------- 5 files changed, 42 insertions(+), 104 deletions(-) diff --git a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py index e93011d..2ce224a 100644 --- a/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/ode_three_comp_hyd_simulator.py @@ -296,7 +296,7 @@ def rAn(t_s: float, h_s: float, g_s: float, p: float, t_max: float, conf: list): t_end = t_max # if t_end after t_max, return g(t_max) - if t_end > t_max: + if t_end >= t_max: gtmax = c1 * np.exp(m_anf * t_max / (a_ans * (gamma - 1))) - theta return t_max, h_s, gtmax, None else: diff --git a/tests/configurations.py b/tests/configurations.py index d6e2a1c..4705883 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -119,6 +119,7 @@ hz = 1000 # delta t eps = 0.1 # required precision t_max = 2000 + log_level = 0 # setting combinations p_exps = [260, 681] @@ -149,17 +150,14 @@ gam=conf[6], phi=conf[7]) # Start with first time to exhaustion bout - tte, h_tte, g_tte = ODEThreeCompHydSimulator.constant_power_trial(p=p_exp, conf=conf, start_h=0, - start_g=0, t_max=t_max) + tte, h_tte, g_tte = ODEThreeCompHydSimulator.constant_power_trial(p=p_exp, + conf=conf, + start_h=0, + start_g=0, + t_max=t_max) - # double-check with discrete agent - for _ in range(int(round(tte * hz))): - agent.set_power(p_exp) - agent.perform_one_step() - g_diff = agent.get_g() - g_tte - h_diff = agent.get_h() - h_tte - assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) + if log_level > 1: + logging.info("TTE ODE {} with h {} and g {}".format(round(tte), h_tte, g_tte)) # double-check time to exhaustion try: @@ -167,15 +165,26 @@ except UserWarning: c_tte = t_max assert abs(round(tte) - round(c_tte)) < eps, "TTE ODE {} and IT {} is off by {}".format(round(tte), - round(c_tte), - abs(tte - c_tte)) + round(c_tte), + abs(tte - c_tte)) + + # double-check h and g + agent.reset() + for _ in range(int(round(tte * hz))): + agent.set_power(p_exp) + agent.perform_one_step() + g_diff = agent.get_g() - g_tte + h_diff = agent.get_h() - h_tte + assert abs(g_diff) < eps, "TTE g is off by {}".format(g_diff) + assert abs(h_diff) < eps, "TTE h is off by {}".format(h_diff) # Recovery behaviour rec, h_rec, g_rec = ODEThreeCompHydSimulator.constant_power_trial(p=p_rec, conf=conf, start_h=h_tte, start_g=g_tte, t_max=rec_time) + # double-check with discrete agent - agent.reset() + # agent.reset() agent.set_h(h_tte) agent.set_g(g_tte) for _ in range(int(round(rec_time * hz))): @@ -183,8 +192,8 @@ agent.perform_one_step() g_diff = agent.get_g() - g_rec h_diff = agent.get_h() - h_rec - assert abs(g_diff) < eps, "REC g is off by {}".format(g_diff) - assert abs(h_diff) < eps, "REC h is off by {}".format(h_diff) + assert abs(g_diff) < eps, "REC g is off by {}. {} vs {}".format(g_diff, agent.get_g(), g_rec) + assert abs(h_diff) < eps, "REC h is off by {}. {} vs {}".format(h_diff, agent.get_h(), h_rec) # Now a full recovery trial rec_t = ODEThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(conf=conf, p_exp=p_exp, diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 7b52092..3598e2d 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -36,7 +36,6 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, if t >= t_max: logging.info("Exhaustion not reached during TTE") - return # double-check with discrete agent for _ in range(int(round(t * hz))): @@ -54,10 +53,6 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, # restart time from 0 t = 0 - - theta = conf[5] - gamma = conf[6] - phi = conf[7] func = None # cycles through phases until t_max is reached @@ -65,37 +60,7 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, if func is None: # first distinguish between fAe and lAe - if h >= 1 - phi: - # fAe - if h < theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe - # fAe_rAnS - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe_rAn - # fAe_lAnS - elif h > g + theta and h < 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_lAn - # fAe_fAnS - elif h > 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, - phi)) - else: - # lAr - if h < theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe_rAn - elif h > g + theta and h <= 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_lAn - elif h > 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, - phi)) + func = ODEThreeCompHydSimulator.state_to_phase(conf, h, g) if log_level > 1: logging.info("[intermediate result] Try {}".format(func)) @@ -163,9 +128,9 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") p_exp = 260 - t_rec = 240 - p_rec = 10 - t_max = 6000 + t_rec = 10 + p_rec = 0 + t_max = 2000 # estimations per second for discrete agent hz = 500 diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index bb25ac2..06ec6ce 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -20,7 +20,10 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): tte_1, h, g = ODEThreeCompHydSimulator.constant_power_trial(conf=conf, start_h=0, start_g=0, p=p_exp, t_max=t_max) - + if tte_1 >= t_max: + logging.info("Exhaustion not reached during TTE") + return + # confirm the tte time with an entire iterative simulation c_tte = ThreeCompHydSimulator.tte(agent, p_work=p_exp, t_max=t_max) assert abs(c_tte - tte_1) < eps, "TTE1 confirmation error. Difference between " \ @@ -28,10 +31,6 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): c_tte, abs(c_tte - tte_1)) - if tte_1 >= t_max: - logging.info("Exhaustion not reached during TTE") - return - agent.reset() # double-check with discrete agent for _ in range(int(round(tte_1 * hz))): @@ -132,9 +131,9 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p_exp = 681 + p_exp = 260 t_rec = 240 - p_rec = 0 + p_rec = 10 t_max = 5000 # estimations per second for discrete agent @@ -143,10 +142,10 @@ def the_loop(p_exp: float = 350.0, p_rec: float = 100.0, t_rec=180.0, eps = 0.01 # a configuration - c = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, - 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 1.0] + c = [15101.24769778409, 86209.27743067988, 252.71702367096788, + 363.2970828395908, 38.27073086773415, 0.0, 0.3524379644134216, 1.0] rec_trial_procedure(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps, conf=c, log_level=2) - the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) + # the_loop(p_exp=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max, hz=hz, eps=eps) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index f256110..1e6a4f5 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -28,44 +28,11 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): agent.set_g(g_s) ThreeCompVisualisation(agent) - theta = conf[5] - gamma = conf[6] - phi = conf[7] func = None while t < t_max: if func is None: # first distinguish between fAe and lAe - if h >= 1 - phi: - # fAe - if h < theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe - # fAe_rAnS - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.fAe_rAn - # fAe_lAnS - elif h > g + theta and h < 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_lAn - # fAe_fAnS - elif h > 1 - gamma: - func = ODEThreeCompHydSimulator.fAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, - phi)) - else: - # lAr - if h < theta and g < ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe - elif h < g + theta and g > ODEThreeCompHydSimulator.eps: - func = ODEThreeCompHydSimulator.lAe_rAn - elif h > g + theta and h <= 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_lAn - elif h > 1 - gamma: - func = ODEThreeCompHydSimulator.lAe_fAn - else: - raise UserWarning( - "unhandled state with h {} g {} and conf theta {} gamma {} phi {}".format(h, g, theta, gamma, - phi)) + func = ODEThreeCompHydSimulator.state_to_phase(conf, h, g) # iterate through all phases until end is reached t, h, g, n_func = func(t, h, g, p, t_max=t_max, conf=conf) @@ -145,16 +112,14 @@ def the_loop(p: float = 350.0, logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") - p = 681 + p = 260 # estimations per second for discrete agent hz = 500 # required precision of discrete to differential agent eps = 0.01 - example_conf = [15101.24769778409, 86209.27743067988, - 252.71702367096788, 363.2970828395908, - 38.27073086773415, 0.14892228099402588, - 0.3524379644134216, 1.0] + example_conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, + 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 1.0] tte_test_procedure(p, hz, eps, example_conf, log_level=1) From a34379d74d978d2601bbb1e732a6db664bcebeb9 Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 18 Jan 2022 17:02:00 +0100 Subject: [PATCH 53/71] update ODE tests to new labels --- tests/comparison_tests.py | 6 ++++-- tests/configurations.py | 8 ++++---- tests/phase_fAe.py | 7 +++++-- tests/rec_phase_tests.py | 6 ++++-- tests/rec_trial_tests.py | 7 ++++--- tests/tte_phase_tests.py | 7 ++++--- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/tests/comparison_tests.py b/tests/comparison_tests.py index 598974f..d1ada39 100644 --- a/tests/comparison_tests.py +++ b/tests/comparison_tests.py @@ -11,8 +11,10 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, conf, log_level=0): # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], - m_ans=conf[3], m_anf=conf[4], the=conf[5], + agent = ThreeCompHydAgent(hz=hz, + lf=conf[0], ls=conf[1], + m_u=conf[2], m_ls=conf[3], + m_lf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) # simulator step limit needs to be adjusted diff --git a/tests/configurations.py b/tests/configurations.py index 4705883..d9df2e5 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -144,9 +144,9 @@ # create three component hydraulic agent with example configuration agent = ThreeCompHydAgent(hz=hz, - a_anf=conf[0], a_ans=conf[1], - m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], + lf=conf[0], ls=conf[1], + m_u=conf[2], m_ls=conf[3], + m_lf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) # Start with first time to exhaustion bout @@ -157,7 +157,7 @@ t_max=t_max) if log_level > 1: - logging.info("TTE ODE {} with h {} and g {}".format(round(tte), h_tte, g_tte)) + logging.info("TTE ODE {} with h {} and g {}".format(round(tte), h_tte, g_tte)) # double-check time to exhaustion try: diff --git a/tests/phase_fAe.py b/tests/phase_fAe.py index f6846aa..9e6e124 100644 --- a/tests/phase_fAe.py +++ b/tests/phase_fAe.py @@ -13,8 +13,11 @@ def test(func, h, g, p, conf, t=0, hz=500, t_max=2000, log=2): t_end, h_end, g_end, func = func(t_s=t, h_s=h, g_s=g, p=p, t_max=t_max, conf=conf) # confirm with iterative agent - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], m_ans=conf[3], - m_anf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) + agent = ThreeCompHydAgent(hz=hz, + lf=conf[0], ls=conf[1], + m_u=conf[2], m_ls=conf[3], + m_lf=conf[4], the=conf[5], + gam=conf[6], phi=conf[7]) agent.reset() agent.set_g(g) agent.set_h(h) diff --git a/tests/rec_phase_tests.py b/tests/rec_phase_tests.py index 3598e2d..93dd38c 100644 --- a/tests/rec_phase_tests.py +++ b/tests/rec_phase_tests.py @@ -24,8 +24,10 @@ def rec_phase_procedure(p_exp: float, p_rec: float, t_rec: float, t_max: float, :param log_level: amount of detail about results to be logged """ - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], - m_ans=conf[3], m_anf=conf[4], the=conf[5], + agent = ThreeCompHydAgent(hz=hz, + lf=conf[0], ls=conf[1], + m_u=conf[2], m_ls=conf[3], + m_lf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) if log_level > 0: logging.info("Agent to be examined") diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 06ec6ce..8bcefd3 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -9,10 +9,11 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], - m_ans=conf[3], m_anf=conf[4], the=conf[5], + agent = ThreeCompHydAgent(hz=hz, + lf=conf[0], ls=conf[1], + m_u=conf[2], m_ls=conf[3], + m_lf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - if log_level > 0: ThreeCompVisualisation(agent) diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index 1e6a4f5..fc84bb5 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -13,10 +13,11 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): t_max = 5000 # create three component hydraulic agent with example configuration - agent = ThreeCompHydAgent(hz=hz, a_anf=conf[0], a_ans=conf[1], m_ae=conf[2], - m_ans=conf[3], m_anf=conf[4], the=conf[5], + agent = ThreeCompHydAgent(hz=hz, + lf=conf[0], ls=conf[1], + m_u=conf[2], m_ls=conf[3], + m_lf=conf[4], the=conf[5], gam=conf[6], phi=conf[7]) - # set initial conditions h_s = 0 g_s = 0 # 1 - conf[6] - conf[5] From e5203b411ce786af09688a5d77199601e592263a Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 19 Jan 2022 10:24:19 +0100 Subject: [PATCH 54/71] fix remaining updated labels inconsistency --- .../agents/three_comp_hyd_agent.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 96951aa..d2a00a7 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -76,27 +76,27 @@ def __raise_detailed_error_report(self): "gamma: {} \n " "theta: {} \n " "phi: {} \n " - "AnF: {} \n " - "AnS: {} \n " + "LF: {} \n " + "LS: {} \n " "g: {} \n " "h: {} \n " - "m_ae: {} \n" - "m_ans: {} \n" - "m_anf: {} \n" - "p_Ae: {} \n" - "p_An: {} \n" + "m_u: {} \n" + "m_ls: {} \n" + "m_lf: {} \n" + "p_U: {} \n" + "p_L: {} \n" "pow: {} \n".format(self.__gamma, self.__theta, self.__phi, - self.__a_anf, - self.__a_ans, + self.__lf, + self.__ls, self.__g, self.__h, - self.__m_ae, - self.__m_ans, - self.__m_anf, - self.__p_ae, - self.__p_an, + self.__m_u, + self.__m_ls, + self.__m_lf, + self.__p_u, + self.__p_l, self._pow)) def _estimate_possible_power_output(self): From 5338890e89f2a6eab8c8dea9f5b6f9ba7108943b Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 19 Jan 2022 19:21:06 +0100 Subject: [PATCH 55/71] improve hydraulic simulator functions and change w'bal approximation of hyd agent --- example_scripts/model_behaviour_plots.py | 2 +- src/threecomphyd/agents/hyd_agent_basis.py | 20 +-- .../agents/three_comp_hyd_agent.py | 2 +- .../evolutionary_fitter/three_comp_tools.py | 2 +- .../simulator/three_comp_hyd_simulator.py | 167 +++++++++++------- tests/comparison_tests.py | 2 +- tests/configurations.py | 2 +- tests/rec_trial_tests.py | 2 +- 8 files changed, 117 insertions(+), 82 deletions(-) diff --git a/example_scripts/model_behaviour_plots.py b/example_scripts/model_behaviour_plots.py index 6a573fb..3917c4e 100644 --- a/example_scripts/model_behaviour_plots.py +++ b/example_scripts/model_behaviour_plots.py @@ -55,7 +55,7 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): three_comp_agent = ThreeCompHydAgent(hz=1, lf=p[0], ls=p[1], m_u=p[2], m_ls=p[3], m_lf=p[4], the=p[5], gam=p[6], phi=p[7]) - hyd_fitted_times_ext = [ThreeCompHydSimulator.simulate_tte_hydraulic_detail(three_comp_agent, x) for x in + hyd_fitted_times_ext = [ThreeCompHydSimulator.tte_detail(three_comp_agent, x) for x in powers_ext] hyd_powers_ext = powers_ext ax.plot(hyd_fitted_times_ext, hyd_powers_ext, diff --git a/src/threecomphyd/agents/hyd_agent_basis.py b/src/threecomphyd/agents/hyd_agent_basis.py index e0e85f6..e416af1 100644 --- a/src/threecomphyd/agents/hyd_agent_basis.py +++ b/src/threecomphyd/agents/hyd_agent_basis.py @@ -17,12 +17,12 @@ def __init__(self, hz: int): # simulation management parameters self._step = 0 self._hz = hz - self._hz_t = 0 + self._hz_t = 0.0 # power parameters self._pow = 0 - def get_name(self): + def get_name(self) -> str: """ :return: a descriptive name """ @@ -35,14 +35,14 @@ def set_power(self, power: float): """ self._pow = power - def get_power(self): + def get_power(self) -> float: """ :return: power in Watts """ return self._pow @property - def hz(self): + def hz(self) -> int: """ :return: number of obs per second """ @@ -53,17 +53,17 @@ def reset(self): reset internal values to default """ self._step = 0 - self._hz_t = 0 + self._hz_t = 0.0 # power output params self._pow = 0 - def get_time(self): + def get_time(self) -> float: """ :return: time in seconds considering the agent's hz setting """ return self._hz_t - def perform_one_step(self): + def perform_one_step(self) -> float: """ Updates power output and internal W' balance parameters. :return: expended power @@ -86,19 +86,19 @@ def _estimate_possible_power_output(self): """ @abstractmethod - def is_exhausted(self): + def is_exhausted(self) -> bool: """ :return: simply returns the exhausted flag """ @abstractmethod - def is_recovered(self): + def is_recovered(self) -> bool: """ :return: simply returns the recovered flag """ @abstractmethod - def is_equilibrium(self): + def is_equilibrium(self) -> bool: """ :return: if energy storages are at a constant state """ diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 602e6b7..3e31169 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -223,7 +223,7 @@ def get_w_p_ratio(self): """ :return: wp estimation between 0 and 1 for comparison to CP models """ - return (1.0 - self.__h) * ((self.__height_ls - self.__g) / self.__height_ls) + return (1.0 - self.__h) * (1.0 - self.__g) def get_fill_lf(self): """ diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index 7b1d56f..531acee 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -175,7 +175,7 @@ def three_comp_two_objective_functions(obj_vars, hz: int, # use the simulator try: achieved = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(three_comp_agent, - p_exp=p_exp, + p_work=p_exp, p_rec=p_rec, t_rec=t_rec) except UserWarning: diff --git a/src/threecomphyd/simulator/three_comp_hyd_simulator.py b/src/threecomphyd/simulator/three_comp_hyd_simulator.py index 8f889cd..7895d9c 100644 --- a/src/threecomphyd/simulator/three_comp_hyd_simulator.py +++ b/src/threecomphyd/simulator/three_comp_hyd_simulator.py @@ -7,22 +7,22 @@ class ThreeCompHydSimulator: """ - Employs the ThreeComponentHydraulic model to simulate training sessions and - TTE tests + Tailored to the ThreeComponentHydraulic model, this class offers functions to simulate training sessions, + TTE tests, and recovery estimation protocols """ @staticmethod def tte(agent: ThreeCompHydAgent, p_work: float, start_h: float = 0, - start_g: float = 0, t_max: float = 5000, step_function=None): + start_g: float = 0, t_max: float = 5000, step_function=None) -> float: """ - a normal time to exhaustion test - :param agent: iterative agent + simulates a standard time to exhaustion test + :param agent: hydraulic agent :param p_work: constant expenditure intensity for TTE - :param start_h: fill level of AnF at start - :param start_g: fill level of AnS at start - :param t_max: maximal time (steps * hz) until warning "exhaustion not reached" is raised - :param step_function: function of agent to estimate one time step. Default is perform_one_step - :return: + :param start_h: fill level of LF at start + :param start_g: fill level of LS at start + :param t_max: maximal time in seconds until warning "exhaustion not reached" is raised + :param step_function: function of agent to estimate one time step. Default is perform_one_step. + :return: time to exhaustion in seconds """ agent.reset() agent.set_h(start_h) @@ -33,29 +33,30 @@ def tte(agent: ThreeCompHydAgent, p_work: float, start_h: float = 0, if step_function is None: step_function = agent.perform_one_step - # WB1 Exhaust... + # Exhaust... agent.set_power(p_work) steps = 0 while not agent.is_exhausted() and steps < step_limit: step_function() steps += 1 - wb1_t = agent.get_time() + tte = agent.get_time() if not agent.is_exhausted(): raise UserWarning("exhaustion not reached!") - return wb1_t + return tte @staticmethod - def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec, t_max=5000): + def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_work: float, p_rec: float, + t_rec: float, t_max: float = 5000) -> float: """ Returns recovery ratio of given agent according to WB1 -> RB -> WB2 protocol. Recovery ratio estimations for given exp, rec intensity and time :param agent: three component hydraulic agent to use - :param p_exp: work bout intensity + :param p_work: work bout intensity :param p_rec: recovery bout intensity :param t_rec: recovery bout duration - :param t_max: maximal time (steps * hz) until warning "exhaustion not reached" is raised + :param t_max: maximal time in seconds until warning "exhaustion not reached" is raised :return: ratio in percent """ @@ -65,7 +66,7 @@ def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec, t_ step_limit = t_max * hz # WB1 Exhaust... - agent.set_power(p_exp) + agent.set_power(p_work) steps = 0 while not agent.is_exhausted() and steps < step_limit: agent.perform_one_step() @@ -82,7 +83,7 @@ def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec, t_ rec_t = agent.get_time() # WB2 Exhaust... - agent.set_power(p_exp) + agent.set_power(p_work) steps = 0 while not agent.is_exhausted() and steps < step_limit: agent.perform_one_step() @@ -93,21 +94,26 @@ def get_recovery_ratio_wb1_wb2(agent: ThreeCompHydAgent, p_exp, p_rec, t_rec, t_ return ((wb2_t - rec_t) / wb1_t) * 100.0 @staticmethod - def simulate_course_detail(agent: ThreeCompHydAgent, powers, plot: bool = False): + def simulate_course_detail(agent: ThreeCompHydAgent, powers, + step_function=None, plot: bool = False): """ simulates a whole course with given agent :param agent: :param powers: list or array - :param plot: - :return all parameter values throughout the simulation + :param plot: displays a plot of some of the state variables over time + :param step_function: function of agent to estimate one time step. Default is perform_one_step. + :return all state variables throughout for every time step of the course [h, g, lf, ls, p_u, p_l, m_flow, w_p_bal] """ agent.reset() h, g, lf, ls, p_u, p_l, m_flow, w_p_bal = [], [], [], [], [], [], [], [] + if step_function is None: + step_function = agent.perform_one_step + # let the agent simulate the list of power demands for step in powers: - # log all the parameters + # we include values of time 0 h.append(agent.get_h()) g.append(agent.get_g()) lf.append(agent.get_fill_lf()) @@ -119,72 +125,101 @@ def simulate_course_detail(agent: ThreeCompHydAgent, powers, plot: bool = False) # perform current power step agent.set_power(step) - agent.perform_one_step() + step_function() - # plot results + # an investigation and debug plot if you want to if plot is True: - ThreeCompHydSimulator.plot_dynamics(t=np.arange(len(powers)), - p=powers, - anf=lf, - ans=ls, - p_ae=p_u, - p_an=p_l) + ThreeCompHydSimulator.plot_dynamics(t=np.arange(len(powers)), p=powers, + lf=lf, ls=ls, p_u=p_u, p_l=p_l) # return parameters return h, g, lf, ls, p_u, p_l, m_flow, w_p_bal @staticmethod - def simulate_tte_hydraulic_detail(agent: ThreeCompHydAgent, power, plot=False): + def tte_detail(agent: ThreeCompHydAgent, p_work: float, start_h: float = 0, + start_g: float = 0, t_max: float = 5000, step_function=None, + plot: bool = False): """ - returns the time the agent takes till exhaustion at given power + simulates a standard time to exhaustion test and collects all state variables of the hydraulic agent in + every time step. + :param agent: hydraulic agent + :param p_work: constant expenditure intensity for TTE + :param start_h: fill level of LF at start + :param start_g: fill level of LS at start + :param t_max: maximal time in seconds until warning "exhaustion not reached" is raised + :param step_function: function of agent to estimate one time step. Default is perform_one_step. + :param plot: whether state variables over time should be plotted + :return: all state variables all state variables throughout for every time step of the TTE [h, g, lf, ls, p_u, p_l, m_flow, w_p_bal] """ agent.reset() - agent.set_power(power) + agent.set_h(start_h) + agent.set_g(start_g) + step_limit = t_max * agent.hz + + if step_function is None: + step_function = agent.perform_one_step + + # all state variables are logged + t, ps = [], [] + lf, ls, h, g, p_u, p_l, m_flow, w_p_bal = [], [], [], [], [], [], [], [] - t, p, anf, ans, p_ae, p_an, m_flow = [], [], [], [], [], [], [] - # perform steps until agent is exhausted steps = 0 - while agent.is_exhausted() is False and steps < 3000: + agent.set_power(p_work) + + # perform steps until agent is exhausted or step limit is reached + while steps < step_limit: + + # we include values of time 0 t.append(agent.get_time()) - p.append(agent.perform_one_step()) - anf.append(agent.get_fill_lf()) - ans.append(agent.get_fill_ls()) - p_ae.append(agent.get_p_u()) - p_an.append(agent.get_p_l()) + ps.append(agent.get_power()) + h.append(agent.get_h()) + g.append(agent.get_g()) + lf.append(agent.get_fill_lf()) + ls.append(agent.get_fill_ls()) + p_u.append(agent.get_p_u()) + p_l.append(agent.get_p_l()) m_flow.append(agent.get_m_flow()) + w_p_bal.append(agent.get_w_p_ratio()) + + if agent.is_exhausted(): + break + + step_function() steps += 1 + # a investigation and debug plot if you want to if plot is True: - ThreeCompHydSimulator.plot_dynamics(t, p, anf, ans, p_ae, p_an) + ThreeCompHydSimulator.plot_dynamics(t=t, p=ps, lf=lf, ls=ls, p_u=p_u, p_l=p_l) - return agent.get_time() + # return parameters + return h, g, h, g, p_u, p_l, m_flow, w_p_bal @staticmethod - def simulate_tte_with_recovery(agent: ThreeCompHydAgent, exp_p, rec_p, plot=False): + def tte_detail_with_recovery(agent: ThreeCompHydAgent, p_work, p_rec, plot=False): """ The time the agent takes till exhaustion at given power and time till recovery :param agent: agent instance to use - :param exp_p: expenditure intensity - :param rec_p: recovery intensity - :param plot: plot parameter time series + :param p_work: expenditure intensity + :param p_rec: recovery intensity + :param plot: displays a plot of some of the state variables over time :returns: tte, ttr """ agent.reset() - t, p, anf, ans, p_h, p_g, m_flow = [], [], [], [], [], [], [] + t, p, lf, ls, p_u, p_l, m_flow = [], [], [], [], [], [], [] # perform steps until agent is exhausted logging.info("start exhaustion") - agent.set_power(exp_p) + agent.set_power(p_work) steps = 0 while agent.is_exhausted() is False and steps < 10000: t.append(agent.get_time()) p.append(agent.perform_one_step()) - anf.append(agent.get_fill_lf()) - ans.append(agent.get_fill_ls() * agent.height_ls + agent.theta) - p_h.append(agent.get_p_u()) - p_g.append(agent.get_p_l()) + lf.append(agent.get_fill_lf()) + ls.append(agent.get_fill_ls() * agent.height_ls + agent.theta) + p_u.append(agent.get_p_u()) + p_l.append(agent.get_p_l()) m_flow.append(agent.get_m_flow()) steps += 1 # save time @@ -192,15 +227,15 @@ def simulate_tte_with_recovery(agent: ThreeCompHydAgent, exp_p, rec_p, plot=Fals # add recovery at 0 logging.info("start recovery") - agent.set_power(rec_p) + agent.set_power(p_rec) steps = 0 while agent.is_equilibrium() is False and steps < 20000: t.append(agent.get_time()) p.append(agent.perform_one_step()) - anf.append(agent.get_fill_lf()) - ans.append(agent.get_fill_ls() * agent.height_ls + agent.theta) - p_h.append(agent.get_p_u()) - p_g.append(agent.get_p_l()) + lf.append(agent.get_fill_lf()) + ls.append(agent.get_fill_ls() * agent.height_ls + agent.theta) + p_u.append(agent.get_p_u()) + p_l.append(agent.get_p_l()) m_flow.append(agent.get_m_flow()) steps += 1 # save recovery time @@ -208,15 +243,15 @@ def simulate_tte_with_recovery(agent: ThreeCompHydAgent, exp_p, rec_p, plot=Fals # plot the parameter overview if required if plot is True: - ThreeCompHydSimulator.plot_dynamics(t, p, anf, ans, p_h, p_g) + ThreeCompHydSimulator.plot_dynamics(t, p, lf, ls, p_u, p_l) # return time till exhaustion and time till recovery return tte, ttr @staticmethod - def plot_dynamics(t, p, anf, ans, p_ae, p_an): + def plot_dynamics(t, p, lf, ls, p_u, p_l): """ - Debugging plots to look at developed power curves + Debugging plots for state parameters """ # set up plot @@ -224,14 +259,14 @@ def plot_dynamics(t, p, anf, ans, p_ae, p_an): ax = fig.add_subplot(1, 1, 1) # plot liquid flows - ax.plot(t, p, color='tab:blue', label="power") - ax.plot(t, p_ae, color='tab:red', label="flow from Ae") - ax.plot(t, p_an, color='tab:purple', label="flow from AnS to AnF") + ax.plot(t, p, color='tab:green', label="power") + ax.plot(t, p_u, color='tab:cyan', label="flow from U") + ax.plot(t, p_l, color='tab:red', label="flow from LS to LF") # plot tank fill levels ax2 = ax.twinx() - ax2.plot(t, anf, color='tab:green', label="fill level AnF", linestyle="--") - ax2.plot(t, ans, color='tab:orange', label="fill level AnS", linestyle="--") + ax2.plot(t, lf, color='tab:orange', label="fill level LF", linestyle="--") + ax2.plot(t, ls, color='tab:red', label="fill level LS", linestyle="--") # label plot ax.set_xlabel("time (s)") diff --git a/tests/comparison_tests.py b/tests/comparison_tests.py index d1ada39..643ffec 100644 --- a/tests/comparison_tests.py +++ b/tests/comparison_tests.py @@ -19,7 +19,7 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, conf, log_level=0): # simulator step limit needs to be adjusted est_t0 = time.process_time_ns() - est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, p_rec=p_rec, + est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_work=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max) est_t = time.process_time_ns() - est_t0 diff --git a/tests/configurations.py b/tests/configurations.py index d9df2e5..c90a5e6 100644 --- a/tests/configurations.py +++ b/tests/configurations.py @@ -200,7 +200,7 @@ p_rec=p_rec, t_rec=rec_time, t_max=t_max) try: - c_rec_t = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_exp=p_exp, + c_rec_t = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, p_work=p_exp, p_rec=p_rec, t_rec=rec_time, t_max=t_max) except UserWarning: diff --git a/tests/rec_trial_tests.py b/tests/rec_trial_tests.py index 8bcefd3..7f14c44 100644 --- a/tests/rec_trial_tests.py +++ b/tests/rec_trial_tests.py @@ -92,7 +92,7 @@ def rec_trial_procedure(p_exp, p_rec, t_rec, t_max, hz, eps, conf, log_level=0): # simulator step limit needs to be adjusted est_ratio = ThreeCompHydSimulator.get_recovery_ratio_wb1_wb2(agent=agent, - p_exp=p_exp, + p_work=p_exp, p_rec=p_rec, t_rec=t_rec, t_max=t_max) From 805bfd2d205f1aa0f2763fe8dd0a27315eabf290 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 4 Apr 2022 18:05:03 +1000 Subject: [PATCH 56/71] add outline option to overview plot --- .../visualiser/three_comp_visualisation.py | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/threecomphyd/visualiser/three_comp_visualisation.py b/src/threecomphyd/visualiser/three_comp_visualisation.py index 44e070f..521007b 100644 --- a/src/threecomphyd/visualiser/three_comp_visualisation.py +++ b/src/threecomphyd/visualiser/three_comp_visualisation.py @@ -20,7 +20,8 @@ def __init__(self, agent: ThreeCompHydAgent, animated: bool = False, detail_annotations: bool = False, basic_annotations: bool = True, - black_and_white: bool = False): + black_and_white: bool = False, + all_outlines: bool = True): """ Whole visualisation setup using given agent's parameters :param agent: The agent to be visualised @@ -31,10 +32,13 @@ def __init__(self, agent: ThreeCompHydAgent, :param detail_annotations: If true, tank distances and sizes are annotated as well. :param basic_annotations: If true, U, LF, and LS are visible :param black_and_white: If true, the visualisation is in black and white + :param all_outlines: If true, adds outlines around liquid flow arrows and half of U """ # matplotlib fontsize rcParams['font.size'] = 10 + self.__all_outlines = all_outlines + # plot if no axis was assigned if axis is None: fig = plt.figure(figsize=(8, 5)) @@ -160,7 +164,6 @@ def __set_detailed_annotations_layout(self): (u_width + 0.1, phi_o), arrowstyle='-|>', mutation_scale=30, - lw=2, color=self.__u_color) self._ax1.annotate('$\phi$', xy=(u_width / 2, phi_o), @@ -187,7 +190,6 @@ def __set_detailed_annotations_layout(self): (self._ann_lf.get_position()[0], 0.0), arrowstyle='-|>', mutation_scale=30, - lw=2, color=self.__p_color) self._ax1.annotate('$h$', @@ -248,7 +250,6 @@ def __set_detailed_annotations_layout(self): (ls_left, gamma_o), arrowstyle='<|-|>', mutation_scale=30, - lw=2, color=self.__ls_color) self._ax1.annotate('$\\theta$', @@ -314,6 +315,13 @@ def __set_detailed_annotations_layout(self): self._ax1.add_artist(ann_arr_flow) # self._ax1.add_artist(ann_p_an) # self._ax1.add_artist(ann_p_ae) + + + if self.__all_outlines: + self._arr_power_flow.set_edgecolor("black") + self._arr_u_flow.set_edgecolor("black") + self._arr_r2_flow.set_edgecolor("black") + self._ax1.axhline(offset, linestyle='--', color=self.__ann_color) self._ax1.axhline(1 + offset - 0.001, linestyle='--', color=self.__ann_color) self._ax1.add_artist(self._ann_power_flow) @@ -338,7 +346,6 @@ def __set_animation_layout(self): (o_width + 0.1, phi_o), arrowstyle='simple', mutation_scale=0, - ec='white', fc=self.__u_color) self._ann_u_flow = Text(text="flow: ", ha='right', fontsize="large", x=o_width, y=phi_o - 0.05) @@ -347,7 +354,6 @@ def __set_animation_layout(self): (self._ann_lf.get_position()[0], 0.0), arrowstyle='simple', mutation_scale=0, - ec='white', color=self.__p_color) self._ann_power_flow = Text(text="flow: ", ha='center', fontsize="large", x=self._ann_lf.get_position()[0], y=offset - 0.05) @@ -359,7 +365,6 @@ def __set_animation_layout(self): self._arr_r2_l_pos[1], arrowstyle='simple', mutation_scale=0, - ec='white', color=self.__ls_color) self._ann_r2_flow = Text(text="flow: ", ha='left', fontsize="large", x=self._ls.get_x(), y=gamma_o - 0.05) @@ -367,6 +372,11 @@ def __set_animation_layout(self): # information annotation self._ann_time = Text(x=1, y=0.9 + offset, ha="right") + if self.__all_outlines: + self._arr_power_flow.set_edgecolor("black") + self._arr_u_flow.set_edgecolor("black") + self._arr_r2_flow.set_edgecolor("black") + self._ax1.add_artist(self._ann_power_flow) self._ax1.add_artist(self._arr_power_flow) self._ax1.add_artist(self._arr_u_flow) @@ -398,7 +408,7 @@ def __set_basic_layout(self): phi_o = self._agent.phi + offset gamma_o = self._agent.gamma + offset - # S tank + # U tank self._u = Rectangle((0.0, phi_o), 0.05, 1 - self._agent.phi, color=self.__u_color, alpha=0.3) self._u1 = Rectangle((0.05, phi_o), 0.05, 1 - self._agent.phi, color=self.__u_color, alpha=0.6) self._u2 = Rectangle((0.1, phi_o), u_width - 0.1, 1 - self._agent.phi, color=self.__u_color) @@ -409,6 +419,7 @@ def __set_basic_layout(self): x=u_width / 2, y=((1 - self._agent.phi) / 2) + phi_o - 0.02) + # LF vessel self._lf = Rectangle((lf_left, offset), lf_width, 1, fill=False, ec="black") self._h = Rectangle((lf_left, offset), lf_width, 1, color=self.__lf_color) @@ -467,7 +478,7 @@ def update_basic_layout(self, agent): phi_o = agent.phi + offset gamma_o = agent.gamma + offset - # S tank + # U tank self._u.set_bounds(0.0, phi_o, 0.05, 1 - self._agent.phi) self._u1.set_bounds(0.05, phi_o, 0.05, 1 - self._agent.phi) self._u2.set_bounds(0.1, phi_o, u_width - 0.1, 1 - self._agent.phi) From 6a724197c5d47ba907fdbfea3977f8980e1a11db Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 12 Apr 2022 16:48:12 +1000 Subject: [PATCH 57/71] adjust detailed hydraulic overview plots and move h to the other side --- .../visualiser/three_comp_visualisation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/threecomphyd/visualiser/three_comp_visualisation.py b/src/threecomphyd/visualiser/three_comp_visualisation.py index 521007b..e577025 100644 --- a/src/threecomphyd/visualiser/three_comp_visualisation.py +++ b/src/threecomphyd/visualiser/three_comp_visualisation.py @@ -193,8 +193,8 @@ def __set_detailed_annotations_layout(self): color=self.__p_color) self._ax1.annotate('$h$', - xy=(self._ann_lf.get_position()[0] + 0.07, 1 + offset), - xytext=(self._ann_lf.get_position()[0] + 0.07, 1 + offset - 0.30), + xy=(self._ann_lf.get_position()[0] - 0.07, 1 + offset), + xytext=(self._ann_lf.get_position()[0] - 0.07, 1 + offset - 0.30), ha='center', fontsize="xx-large", arrowprops=dict(arrowstyle='-|>', @@ -202,8 +202,8 @@ def __set_detailed_annotations_layout(self): fc=self.__ann_color) ) self._ax1.annotate('$h$', - xy=(self._ann_lf.get_position()[0] + 0.07, 1 + offset - 0.55), - xytext=(self._ann_lf.get_position()[0] + 0.07, 1 + offset - 0.30), + xy=(self._ann_lf.get_position()[0] - 0.07, 0.35 + offset), + xytext=(self._ann_lf.get_position()[0] - 0.07, 1 + offset - 0.30), ha='center', fontsize="xx-large", arrowprops=dict(arrowstyle='-|>', @@ -213,7 +213,7 @@ def __set_detailed_annotations_layout(self): self._h.update(dict(xy=(lf_left, offset), width=lf_width, - height=1 - 0.55, + height=0.35, color=self.__lf_color)) self._ax1.annotate('$g$', From 802f2b2a95058ed4f1590d5c551ab92d33b0022e Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 14 Apr 2022 16:18:02 +1000 Subject: [PATCH 58/71] fix old notation leftovers and add constant high intensity simulation switch --- .../animator/three_comp_interactive_animation.py | 2 +- .../visualiser/three_comp_visualisation.py | 2 +- tests/tte_phase_tests.py | 14 ++++++++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/threecomphyd/animator/three_comp_interactive_animation.py b/src/threecomphyd/animator/three_comp_interactive_animation.py index bc9d5c6..c6d55ee 100644 --- a/src/threecomphyd/animator/three_comp_interactive_animation.py +++ b/src/threecomphyd/animator/three_comp_interactive_animation.py @@ -19,7 +19,7 @@ def __init__(self, agent: ThreeCompHydAgent): ax1 = self._fig.add_subplot(1, 1, 1) # Three comp base vis - ThreeCompVisualisation.__init__(self, axis=ax1, agent=agent, animated=True) + ThreeCompVisualisation.__init__(self, axis=ax1, agent=agent, animated=True, basic_annotations=True) # Power control sim InteractiveAnimation.__init__(self, figure=self._fig, agent=agent) diff --git a/src/threecomphyd/visualiser/three_comp_visualisation.py b/src/threecomphyd/visualiser/three_comp_visualisation.py index 1cf97b8..6c64780 100644 --- a/src/threecomphyd/visualiser/three_comp_visualisation.py +++ b/src/threecomphyd/visualiser/three_comp_visualisation.py @@ -502,7 +502,7 @@ def hide_basic_annotations(self): # update levels self._h.set_height(1 - self._agent.get_h()) - self._g.set_height(self._agent.height_ans - self._agent.get_g()) + self._g.set_height(self._agent.height_ls - self._agent.get_g()) def update_animation_data(self, frame_number): """ diff --git a/tests/tte_phase_tests.py b/tests/tte_phase_tests.py index fc84bb5..3c93559 100644 --- a/tests/tte_phase_tests.py +++ b/tests/tte_phase_tests.py @@ -27,7 +27,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): if log_level > 0: agent.set_h(h_s) agent.set_g(g_s) - ThreeCompVisualisation(agent) + ThreeCompVisualisation(agent, basic_annotations=False) func = None while t < t_max: @@ -44,7 +44,7 @@ def tte_test_procedure(p, hz, eps, conf, log_level=0): agent.set_h(h) agent.set_g(g) logging.info("ODE".format(func, t)) - ThreeCompVisualisation(agent) + ThreeCompVisualisation(agent, basic_annotations=False) func = n_func if log_level > 0: @@ -119,8 +119,14 @@ def the_loop(p: float = 350.0, # required precision of discrete to differential agent eps = 0.01 - example_conf = [15101.24769778409, 86209.27743067988, 252.71702367096788, 363.2970828395908, - 38.27073086773415, 0.14892228099402588, 0.3524379644134216, 1.0] + example_conf = [11220.356531171583, + 21516.06360371364, + 238.17395911833233, + 57.12368620643582, + 6.193546462332192, + 0.242533769054134, + 0.27055182889336115, + 0.23158433943054582] tte_test_procedure(p, hz, eps, example_conf, log_level=1) From cd9f95765a191a86c3261c5fe5f4494d964c4bfc Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 10 May 2022 17:48:01 +1000 Subject: [PATCH 59/71] minor adjustment to plot size in basic visualisation --- src/threecomphyd/visualiser/three_comp_visualisation.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/threecomphyd/visualiser/three_comp_visualisation.py b/src/threecomphyd/visualiser/three_comp_visualisation.py index e577025..f3b3751 100644 --- a/src/threecomphyd/visualiser/three_comp_visualisation.py +++ b/src/threecomphyd/visualiser/three_comp_visualisation.py @@ -34,14 +34,12 @@ def __init__(self, agent: ThreeCompHydAgent, :param black_and_white: If true, the visualisation is in black and white :param all_outlines: If true, adds outlines around liquid flow arrows and half of U """ - # matplotlib fontsize - rcParams['font.size'] = 10 self.__all_outlines = all_outlines # plot if no axis was assigned if axis is None: - fig = plt.figure(figsize=(8, 5)) + fig = plt.figure(figsize=(8, 4.2)) self._ax1 = fig.add_subplot(1, 1, 1) else: fig = None @@ -316,7 +314,6 @@ def __set_detailed_annotations_layout(self): # self._ax1.add_artist(ann_p_an) # self._ax1.add_artist(ann_p_ae) - if self.__all_outlines: self._arr_power_flow.set_edgecolor("black") self._arr_u_flow.set_edgecolor("black") @@ -419,7 +416,6 @@ def __set_basic_layout(self): x=u_width / 2, y=((1 - self._agent.phi) / 2) + phi_o - 0.02) - # LF vessel self._lf = Rectangle((lf_left, offset), lf_width, 1, fill=False, ec="black") self._h = Rectangle((lf_left, offset), lf_width, 1, color=self.__lf_color) From 0163855df8e4650e01a131d65a620abb52d3c325 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 27 Jun 2022 15:09:58 +1000 Subject: [PATCH 60/71] add two tank model agent and animations --- example_scripts/2tm_interactive_simulation.py | 16 +++ ...ation.py => 3tm_interactive_simulation.py} | 0 src/threecomphyd/agents/two_comp_hyd_agent.py | 104 ++++++++++++++ .../animator/two_comp_hyd_animation.py | 129 ++++++++++++++++++ 4 files changed, 249 insertions(+) create mode 100644 example_scripts/2tm_interactive_simulation.py rename example_scripts/{interactive_simulation.py => 3tm_interactive_simulation.py} (100%) create mode 100644 src/threecomphyd/agents/two_comp_hyd_agent.py create mode 100644 src/threecomphyd/animator/two_comp_hyd_animation.py diff --git a/example_scripts/2tm_interactive_simulation.py b/example_scripts/2tm_interactive_simulation.py new file mode 100644 index 0000000..6ab0a1c --- /dev/null +++ b/example_scripts/2tm_interactive_simulation.py @@ -0,0 +1,16 @@ +import logging + +from threecomphyd.agents.two_comp_hyd_agent import TwoCompHydAgent +from threecomphyd.animator.two_comp_hyd_animation import TwoCompHydAnimation + +if __name__ == "__main__": + # set logging level to highest level + logging.basicConfig(level=logging.INFO, + format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") + + # create two component hydraulic agent with example configuration + agent = TwoCompHydAgent(10, w_p=28000, m_u=240, phi=0.5, psi=0.3) + + # run the interactive animation + ani = TwoCompHydAnimation(agent) + ani.run() diff --git a/example_scripts/interactive_simulation.py b/example_scripts/3tm_interactive_simulation.py similarity index 100% rename from example_scripts/interactive_simulation.py rename to example_scripts/3tm_interactive_simulation.py diff --git a/src/threecomphyd/agents/two_comp_hyd_agent.py b/src/threecomphyd/agents/two_comp_hyd_agent.py new file mode 100644 index 0000000..d09884f --- /dev/null +++ b/src/threecomphyd/agents/two_comp_hyd_agent.py @@ -0,0 +1,104 @@ +from threecomphyd.agents.hyd_agent_basis import HydAgentBasis + + +class TwoCompHydAgent(HydAgentBasis): + + def __init__(self, hz, w_p, m_u, phi, psi: float = 0.0): + """ + :param hz: calculations per second + :param w_p: cross sectional area of W' + :param m_u: maximal flow from O to w' + :param phi: phi (distance Ae to bottom) + :param phi: psi (distance W' to top) + """ + super().__init__(hz=hz) + + if psi > 1 - phi: + raise UserWarning("Top of W\' has to be above or at bottom of Ae (psi > 1 - phi must be False)") + + # constants + self.__phi = phi + self.__psi = psi + self.__w_p = w_p # area of vessel W' + self.__m_u = m_u # max flow from O to P (max aerobic energy consumption i.e. VO2max) + + # variable parameters + self.__h = self.__psi # state of depletion of vessel W' + self.__p_o = 0 # flow through R1 (oxygen pipe) + + @property + def w_p(self): + """:return cross sectional area of W'""" + return self.__w_p + + @property + def phi(self): + """:return phi (distance Ae to bottom)""" + return self.__phi + + @property + def psi(self): + """:return psi (distance W' to top)""" + return self.__phi + + @property + def m_u(self): + """:return max flow through R1""" + return self.__m_u + + def get_h(self): + """:return state of depletion of vessel P""" + return self.__h + + def get_p_o(self): + """:return flow through R1""" + return self.__p_o + + def _estimate_possible_power_output(self): + """ + Update internal capacity estimations by one step. + :return: the amount of power that the athlete was able to put out + """ + p = self._pow + + # determine oxygen energy flow (R1) + # level P above pipe exit. Scale contribution according to h level + if self.__psi < self.__h <= (1.0 - self.__phi): + # contribution through R1 scales with maximal flow capacity + self.__p_o = self.__m_u * (self.__h / (1.0 - self.__phi)) + # at maximum rate because level h of P is below pipe exit of O + elif (1.0 - self.__phi) < self.__h <= 1.0: + # above aerobic threshold (?) max contribution R1 = m_o + self.__p_o = self.__m_u + + # the change on level in W' is determined by flow p_o + self.__h += ((p - self.__p_o) / self.__w_p) / self._hz + + # also W' cannot be fuller than full + if self.__h < self.__psi: + self.__h = self.__psi + elif self.__h > 1: + self.__h = 1 + + return self._pow + + def is_exhausted(self): + """ + exhaustion is reached when level in AnF cannot sustain power demand + :return: simply returns the exhausted flag + """ + return self.__h == 1.0 + + def is_recovered(self): + """ + recovery is complete when W' is full again + :return: simply returns the recovered flag + """ + return self.__h == self.__psi + + def is_equilibrium(self): + """ + equilibrium is reached when po meets pow + :return: boolean + """ + return abs(self.__p_o - self._pow) < 0.1 diff --git a/src/threecomphyd/animator/two_comp_hyd_animation.py b/src/threecomphyd/animator/two_comp_hyd_animation.py new file mode 100644 index 0000000..f08376f --- /dev/null +++ b/src/threecomphyd/animator/two_comp_hyd_animation.py @@ -0,0 +1,129 @@ +import math + +import matplotlib.pyplot as plt +from threecomphyd.agents.two_comp_hyd_agent import TwoCompHydAgent + +from threecomphyd.animator.interactive_animation import InteractiveAnimation + +from matplotlib.text import Text +from matplotlib.patches import Rectangle +from matplotlib.patches import FancyArrowPatch +from matplotlib.lines import Line2D + + +class TwoCompHydAnimation(InteractiveAnimation): + """ + creates an animation to visualise power flow within the two component + hydraulics model with exponential expenditure and recovery + """ + + def __init__(self, agent: TwoCompHydAgent): + """ + Whole animation setup using given agent + """ + + # figure layout + fig = plt.figure(figsize=(10, 6)) + self.__ax1 = fig.add_subplot(1, 1, 1) + + self._agent = agent + + offset = 0.2 + phi = self._agent.phi + offset + psi = self._agent.psi + + # oxygen vessel + self.__o = Rectangle((0.0, phi), 0.1, 1 + offset - phi, fc="tab:cyan", alpha=0.3) + self.__o1 = Rectangle((0.1, phi), 0.1, 1 + offset - phi, color='tab:cyan', alpha=0.6) + self.__o2 = Rectangle((0.2, phi), 0.3, 1 + offset - phi, color='tab:cyan') + self.__ann_o = Text(text="Ae", ha='center', fontsize="xx-large", + x=0.25, + y=phi + ((1 + offset - phi) / 2)) + self.__arr_o_flow = FancyArrowPatch((0.5, phi), + (0.6, phi), + arrowstyle='simple', + mutation_scale=0, + ec='white', + fc='tab:cyan') + self.__ann_o_flow = Text(text="flow: ", ha='right', fontsize="large", + x=0.58, y=phi - 0.1) + self.__r1 = Line2D([0.5, 0.6], [phi, phi], color="tab:cyan") + + # anaerobic vessel + self.__a_p = Rectangle((0.6, offset), 0.2, 1 - psi + offset, fill=False, ec="black") + self.__h = Rectangle((0.6, offset), 0.2, 1 - psi + offset, fc='tab:orange') + self.__ann_p = Text(text="W\'", ha='center', fontsize="xx-large", + x=0.7, + y=offset + (1 - psi + offset) / 2) + self.__arr_power_flow = FancyArrowPatch((self.__ann_p.get_position()[0], offset), + (self.__ann_p.get_position()[0], offset - 0.15), + arrowstyle='simple', + mutation_scale=0, + ec='white', + fc='tab:green') + self.__ann_power_flow = Text(text="flow: ", ha='center', fontsize="large", + x=self.__ann_p.get_position()[0] + 0.1, y=offset - 0.1) + + # information annotation + self.__ann_time = Text(x=1, y=1, ha="right") + + super().__init__(figure=fig, agent=agent) + + def _init_layout(self): + """ + format axis and add descriptions + :return: list of artists to draw + """ + self.__ax1.add_artist(self.__o) + self.__ax1.add_artist(self.__o1) + self.__ax1.add_artist(self.__o2) + self.__ax1.add_line(self.__r1) + self.__ax1.add_artist(self.__a_p) + self.__ax1.add_artist(self.__h) + self.__ax1.add_artist(self.__ann_o) + self.__ax1.add_artist(self.__ann_p) + self.__ax1.add_artist(self.__ann_power_flow) + self.__ax1.add_artist(self.__arr_power_flow) + self.__ax1.add_artist(self.__arr_o_flow) + self.__ax1.add_artist(self.__ann_o_flow) + self.__ax1.set_xlim(0, 1) + self.__ax1.set_ylim(0, 1.2) + self.__ax1.set_axis_off() + + self.__ax1.add_artist(self.__ann_time) + + return [] + + def _update_data(self, frame_number): + """ + The function to call at each frame. + :param frame_number: frame number + :return: an iterable of artists + """ + + # perform one step + cur_time = self._agent.get_time() + power = self._agent.perform_one_step() + + # draw some information + self.__ann_time.set_text("agent \n time: {}".format(int(cur_time))) + + # power arrow + self.__ann_power_flow.set_text("power: {}".format(round(power))) + self.__arr_power_flow.set_mutation_scale(math.log(power + 1) * 10) + + p_o = round(self._agent.get_p_o()) + + # oxygen arrow + max_str = "(CP)" if p_o == self._agent.m_u else "" + self.__ann_o_flow.set_text("flow: {} {}".format(p_o, max_str)) + self.__arr_o_flow.set_mutation_scale(math.log(p_o + 1) * 10) + + # update levels + self.__h.set_height(1 - self._agent.get_h()) + + # list of artists to be drawn + return [self.__ann_time, + self.__ann_power_flow, + self.__arr_power_flow, + self.__h] From af221cb845317c588c38043aff617bcb5166f3d3 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 27 Jun 2022 18:25:23 +1000 Subject: [PATCH 61/71] [WIP] first hacky 2TM overview --- example_scripts/2tm_interactive_simulation.py | 2 +- src/threecomphyd/agents/two_comp_hyd_agent.py | 2 +- .../animator/two_comp_hyd_animation.py | 111 +---- .../visualiser/two_comp_visualisation.py | 405 ++++++++++++++++++ 4 files changed, 419 insertions(+), 101 deletions(-) create mode 100644 src/threecomphyd/visualiser/two_comp_visualisation.py diff --git a/example_scripts/2tm_interactive_simulation.py b/example_scripts/2tm_interactive_simulation.py index 6ab0a1c..9394ae8 100644 --- a/example_scripts/2tm_interactive_simulation.py +++ b/example_scripts/2tm_interactive_simulation.py @@ -9,7 +9,7 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # create two component hydraulic agent with example configuration - agent = TwoCompHydAgent(10, w_p=28000, m_u=240, phi=0.5, psi=0.3) + agent = TwoCompHydAgent(10, w_p=28000, m_u=240, phi=0.5, psi=0.1) # run the interactive animation ani = TwoCompHydAnimation(agent) diff --git a/src/threecomphyd/agents/two_comp_hyd_agent.py b/src/threecomphyd/agents/two_comp_hyd_agent.py index d09884f..dbcdbb9 100644 --- a/src/threecomphyd/agents/two_comp_hyd_agent.py +++ b/src/threecomphyd/agents/two_comp_hyd_agent.py @@ -39,7 +39,7 @@ def phi(self): @property def psi(self): """:return psi (distance W' to top)""" - return self.__phi + return self.__psi @property def m_u(self): diff --git a/src/threecomphyd/animator/two_comp_hyd_animation.py b/src/threecomphyd/animator/two_comp_hyd_animation.py index f08376f..d8aafe2 100644 --- a/src/threecomphyd/animator/two_comp_hyd_animation.py +++ b/src/threecomphyd/animator/two_comp_hyd_animation.py @@ -1,20 +1,14 @@ -import math - import matplotlib.pyplot as plt -from threecomphyd.agents.two_comp_hyd_agent import TwoCompHydAgent +from threecomphyd.agents.two_comp_hyd_agent import TwoCompHydAgent from threecomphyd.animator.interactive_animation import InteractiveAnimation +from threecomphyd.visualiser.two_comp_visualisation import TwoCompVisualisation -from matplotlib.text import Text -from matplotlib.patches import Rectangle -from matplotlib.patches import FancyArrowPatch -from matplotlib.lines import Line2D - -class TwoCompHydAnimation(InteractiveAnimation): +class TwoCompHydAnimation(InteractiveAnimation, TwoCompVisualisation): """ creates an animation to visualise power flow within the two component - hydraulics model with exponential expenditure and recovery + hydraulics model """ def __init__(self, agent: TwoCompHydAgent): @@ -23,76 +17,21 @@ def __init__(self, agent: TwoCompHydAgent): """ # figure layout - fig = plt.figure(figsize=(10, 6)) - self.__ax1 = fig.add_subplot(1, 1, 1) - - self._agent = agent + self._fig = plt.figure(figsize=(10, 6)) + ax1 = self._fig.add_subplot(1, 1, 1) - offset = 0.2 - phi = self._agent.phi + offset - psi = self._agent.psi + # Three comp base vis + TwoCompVisualisation.__init__(self, axis=ax1, agent=agent, animated=True) - # oxygen vessel - self.__o = Rectangle((0.0, phi), 0.1, 1 + offset - phi, fc="tab:cyan", alpha=0.3) - self.__o1 = Rectangle((0.1, phi), 0.1, 1 + offset - phi, color='tab:cyan', alpha=0.6) - self.__o2 = Rectangle((0.2, phi), 0.3, 1 + offset - phi, color='tab:cyan') - self.__ann_o = Text(text="Ae", ha='center', fontsize="xx-large", - x=0.25, - y=phi + ((1 + offset - phi) / 2)) - self.__arr_o_flow = FancyArrowPatch((0.5, phi), - (0.6, phi), - arrowstyle='simple', - mutation_scale=0, - ec='white', - fc='tab:cyan') - self.__ann_o_flow = Text(text="flow: ", ha='right', fontsize="large", - x=0.58, y=phi - 0.1) - self.__r1 = Line2D([0.5, 0.6], [phi, phi], color="tab:cyan") - - # anaerobic vessel - self.__a_p = Rectangle((0.6, offset), 0.2, 1 - psi + offset, fill=False, ec="black") - self.__h = Rectangle((0.6, offset), 0.2, 1 - psi + offset, fc='tab:orange') - self.__ann_p = Text(text="W\'", ha='center', fontsize="xx-large", - x=0.7, - y=offset + (1 - psi + offset) / 2) - self.__arr_power_flow = FancyArrowPatch((self.__ann_p.get_position()[0], offset), - (self.__ann_p.get_position()[0], offset - 0.15), - arrowstyle='simple', - mutation_scale=0, - ec='white', - fc='tab:green') - self.__ann_power_flow = Text(text="flow: ", ha='center', fontsize="large", - x=self.__ann_p.get_position()[0] + 0.1, y=offset - 0.1) - - # information annotation - self.__ann_time = Text(x=1, y=1, ha="right") - - super().__init__(figure=fig, agent=agent) + # Power control sim + InteractiveAnimation.__init__(self, figure=self._fig, agent=agent) def _init_layout(self): """ format axis and add descriptions :return: list of artists to draw """ - self.__ax1.add_artist(self.__o) - self.__ax1.add_artist(self.__o1) - self.__ax1.add_artist(self.__o2) - self.__ax1.add_line(self.__r1) - self.__ax1.add_artist(self.__a_p) - self.__ax1.add_artist(self.__h) - self.__ax1.add_artist(self.__ann_o) - self.__ax1.add_artist(self.__ann_p) - self.__ax1.add_artist(self.__ann_power_flow) - self.__ax1.add_artist(self.__arr_power_flow) - self.__ax1.add_artist(self.__arr_o_flow) - self.__ax1.add_artist(self.__ann_o_flow) - self.__ax1.set_xlim(0, 1) - self.__ax1.set_ylim(0, 1.2) - self.__ax1.set_axis_off() - - self.__ax1.add_artist(self.__ann_time) - - return [] + pass def _update_data(self, frame_number): """ @@ -100,30 +39,4 @@ def _update_data(self, frame_number): :param frame_number: frame number :return: an iterable of artists """ - - # perform one step - cur_time = self._agent.get_time() - power = self._agent.perform_one_step() - - # draw some information - self.__ann_time.set_text("agent \n time: {}".format(int(cur_time))) - - # power arrow - self.__ann_power_flow.set_text("power: {}".format(round(power))) - self.__arr_power_flow.set_mutation_scale(math.log(power + 1) * 10) - - p_o = round(self._agent.get_p_o()) - - # oxygen arrow - max_str = "(CP)" if p_o == self._agent.m_u else "" - self.__ann_o_flow.set_text("flow: {} {}".format(p_o, max_str)) - self.__arr_o_flow.set_mutation_scale(math.log(p_o + 1) * 10) - - # update levels - self.__h.set_height(1 - self._agent.get_h()) - - # list of artists to be drawn - return [self.__ann_time, - self.__ann_power_flow, - self.__arr_power_flow, - self.__h] + TwoCompVisualisation.update_animation_data(self, frame_number=frame_number) diff --git a/src/threecomphyd/visualiser/two_comp_visualisation.py b/src/threecomphyd/visualiser/two_comp_visualisation.py new file mode 100644 index 0000000..20ae32d --- /dev/null +++ b/src/threecomphyd/visualiser/two_comp_visualisation.py @@ -0,0 +1,405 @@ +import math + +import matplotlib.pyplot as plt +from matplotlib.lines import Line2D + +from matplotlib.text import Text +from matplotlib.patches import Rectangle +from matplotlib.patches import FancyArrowPatch +from matplotlib import rcParams +from threecomphyd.agents.two_comp_hyd_agent import TwoCompHydAgent + + +class TwoCompVisualisation: + """ + Basis to visualise power flow within the two tank hydraulics model as an animation or simulation + """ + + def __init__(self, agent: TwoCompHydAgent, + axis: plt.axis = None, + animated: bool = False, + detail_annotations: bool = False, + basic_annotations: bool = True, + black_and_white: bool = False, + all_outlines: bool = True): + """ + Whole visualisation setup using given agent's parameters + :param agent: The agent to be visualised + :param axis: If set, the visualisation will be drawn using the provided axis object. + Useful for animations or to display multiple models in one plot + :param animated: If true the visualisation is set up to deal with frame updates. + See animation script for more details. + :param detail_annotations: If true, tank distances and sizes are annotated as well. + :param basic_annotations: If true, U, LF, and LS are visible + :param black_and_white: If true, the visualisation is in black and white + :param all_outlines: If true, adds outlines around liquid flow arrows and half of U + """ + + self.__all_outlines = all_outlines + + # plot if no axis was assigned + if axis is None: + fig = plt.figure(figsize=(6, 4.2)) + self._ax1 = fig.add_subplot(1, 1, 1) + else: + fig = None + self._ax1 = axis + + if black_and_white: + self.__ae_color = (0.7, 0.7, 0.7) + self.__w_p_color = (0.5, 0.5, 0.5) + self.__ann_color = (0, 0, 0) + self.__p_color = (0.5, 0.5, 0.5) + + elif not black_and_white: + self.__ae_color = "tab:cyan" + self.__w_p_color = "tab:orange" + self.__ann_color = "tab:blue" + self.__p_color = "tab:green" + + # basic parameters for setup + self._animated = animated + self.__detail_annotations = detail_annotations + self._agent = agent + self.__offset = 0.2 + + # U tank with three stripes + self.__width_ae = 0.5 + self._ae = None + self._ae1 = None + self._ae2 = None + self._r1 = None # line marking flow from Ae to W' + self._ann_ae = None # Ae annotation + + # W' tank + self._w_p = None + self._h = None # fill state + self._ann_w_p = None # annotation + + # finish the basic layout + self.__set_basic_layout() + self.update_basic_layout(agent) + + # now the animation components + if self._animated: + # R1 flow + self._arr_r1_flow = None + self._ann_r1_flow = None + + # flow out of tap + self._arr_power_flow = None + self._ann_power_flow = None + + # time information annotation + self._ann_time = None + + self.__set_animation_layout() + self._ax1.add_artist(self._ann_time) + + # basic annotations are U, LF, and LS + if not basic_annotations: + self.hide_basic_annotations() + + # add layout for detailed annotations + # detail annotation add greek letters for distances and positions + if self.__detail_annotations: + self.__set_detailed_annotations_layout() + self._ax1.set_xlim(0, 1.05) + self._ax1.set_ylim(0, 1.2) + else: + self._ax1.set_xlim(0, 1.0) + self._ax1.set_ylim(0, 1.2) + + if self.__detail_annotations and self._animated: + raise UserWarning("Detailed annotations and animation cannot be combined") + + self._ax1.set_axis_off() + + # display plot if no axis object was assigned + if fig is not None: + plt.subplots_adjust(left=0.02, bottom=0.02, right=0.98, top=0.98) + plt.show() + plt.close(fig) + + def __set_detailed_annotations_layout(self): + """ + Adds components required for a detailed + annotations view with denoted positions and distances + """ + + ae_width = self.__width_ae + w_p_left = self._w_p.get_x() + w_p_width = self._w_p.get_width() + + # some offset to the bottom + offset = self.__offset + phi_o = self._agent.phi + offset + + rcParams['text.usetex'] = True + + self._ann_r1_flow = Text(text="$CP$", ha='right', fontsize="xx-large", x=ae_width + 0.09, + y=phi_o - 0.08) + self._arr_r1_flow = FancyArrowPatch((ae_width, phi_o), + (ae_width + 0.1, phi_o), + arrowstyle='-|>', + mutation_scale=30, + color=self.__ae_color) + self._ax1.annotate('$\phi$', + xy=(ae_width / 2, phi_o), + xytext=(ae_width / 2, (phi_o - offset) / 2 + offset - 0.015), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + self._ax1.annotate('$\phi$', + xy=(ae_width / 2, offset), + xytext=(ae_width / 2, (phi_o - offset) / 2 + offset - 0.015), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + + self._ax1.annotate('$\psi$', + xy=(ae_width + 0.2, 1 - self._agent.psi + offset), + xytext=(ae_width + 0.2, 1 - self._agent.psi / 2 + offset - 0.015), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + self._ax1.annotate('$\psi$', + xy=(ae_width + 0.2, 1 + offset), + xytext=(ae_width + 0.2, 1 - self._agent.psi / 2 + offset - 0.015), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + + self._ann_power_flow = Text(text="$p$", ha='center', fontsize="xx-large", x=self._ann_w_p.get_position()[0], + y=offset - 0.06) + self._arr_power_flow = FancyArrowPatch((self._ann_w_p.get_position()[0], offset - 0.078), + (self._ann_w_p.get_position()[0], 0.0), + arrowstyle='-|>', + mutation_scale=30, + color=self.__p_color) + + self._h.update(dict(xy=(w_p_left, offset), + width=w_p_width, + height=0.15, + color=self.__w_p_color)) + + self._ax1.annotate('$h$', + xy=(self._ann_w_p.get_position()[0] + 0.07, + 1 - self._agent.psi + offset), + xytext=(self._ann_w_p.get_position()[0] + 0.07, + (1 - self._agent.psi - self._h.get_height()) / 2 + self._h.get_height() + offset), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + self._ax1.annotate('$h$', + xy=(self._ann_w_p.get_position()[0] + 0.07, + self._h.get_height() + offset), + xytext=(self._ann_w_p.get_position()[0] + 0.07, + (1 - self._agent.psi - self._h.get_height()) / 2 + self._h.get_height() + offset), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + + self._ax1.annotate('$1$', + xy=(1.05, 0 + offset), + xytext=(1.05, 0.5 + offset), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + self._ax1.annotate('$1$', + xy=(1.05, 1 + offset), + xytext=(1.05, 0.5 + offset), + ha='center', + fontsize="xx-large", + arrowprops=dict(arrowstyle='-|>', + ls='-', + fc=self.__ann_color) + ) + + if self.__all_outlines: + self._arr_power_flow.set_edgecolor("black") + self._arr_r1_flow.set_edgecolor("black") + + self._ax1.axhline(offset, linestyle='--', color=self.__ann_color) + self._ax1.axhline(1 + offset - 0.001, linestyle='--', color=self.__ann_color) + self._ax1.add_artist(self._ann_power_flow) + self._ax1.add_artist(self._arr_power_flow) + self._ax1.add_artist(self._arr_r1_flow) + self._ax1.add_artist(self._ann_r1_flow) + + def __set_animation_layout(self): + """ + Adds layout components that are required for an animation + """ + + offset = self.__offset + o_width = self.__width_ae + phi_o = self._agent.phi + offset + + # U flow (R1) + self._arr_r1_flow = FancyArrowPatch((o_width, phi_o), + (o_width + 0.1, phi_o), + arrowstyle='simple', + mutation_scale=0, + fc=self.__ae_color) + self._ann_r1_flow = Text(text="flow: ", ha='right', fontsize="large", x=o_width, y=phi_o - 0.05) + + # Tap flow (Power) + self._arr_power_flow = FancyArrowPatch((self._ann_w_p.get_position()[0], offset - 0.05), + (self._ann_w_p.get_position()[0], 0.0), + arrowstyle='simple', + mutation_scale=0, + color=self.__p_color) + self._ann_power_flow = Text(text="flow: ", ha='center', fontsize="large", x=self._ann_w_p.get_position()[0], + y=offset - 0.05) + + # information annotation + self._ann_time = Text(x=1, y=0.9 + offset, ha="right") + + if self.__all_outlines: + self._arr_power_flow.set_edgecolor("black") + self._arr_r1_flow.set_edgecolor("black") + + self._ax1.add_artist(self._ann_power_flow) + self._ax1.add_artist(self._arr_power_flow) + self._ax1.add_artist(self._arr_r1_flow) + self._ax1.add_artist(self._ann_r1_flow) + + def __set_basic_layout(self): + """ + updates position estimations and layout + """ + + # u_left is 0 + u_width = self.__width_ae + + # some offset to the bottom + offset = self.__offset + phi_o = self._agent.phi + offset + + # Ae tank + self._ae = Rectangle((0.0, phi_o), 0.1, 1 + self._agent.phi, color=self.__ae_color, alpha=0.3) + self._ae1 = Rectangle((0.1, phi_o), 0.1, 1 + self._agent.phi, color=self.__ae_color, alpha=0.6) + self._ae2 = Rectangle((0.2, phi_o), 0.3, 1 + self._agent.phi, color=self.__ae_color) + self._r1 = Line2D([u_width, u_width + 0.1], + [phi_o, phi_o], + color=self.__ae_color) + self._ann_ae = Text(text="Ae", ha='center', fontsize="xx-large", + x=0.25, + y=phi_o + ((1 - self._agent.phi) / 2)) + + # W' tank + self._w_p = Rectangle((0.6, offset), 0.2, 1 - self._agent.psi, fill=False, ec="black") + self._h = Rectangle((0.6, offset), 0.2, 1 - self._agent.psi, color=self.__w_p_color) + self._ann_w_p = Text(text="$W^\prime$", ha='center', fontsize="xx-large", + x=0.7, + y=offset + (1 - self._agent.psi) / 2) + + # the basic layout + self._ax1.add_line(self._r1) + self._ax1.add_artist(self._ae) + self._ax1.add_artist(self._ae1) + self._ax1.add_artist(self._ae2) + self._ax1.add_artist(self._w_p) + self._ax1.add_artist(self._h) + self._ax1.add_artist(self._ann_ae) + self._ax1.add_artist(self._ann_w_p) + + def update_basic_layout(self, agent: TwoCompHydAgent): + """ + updates tank positions and sizes according to new agent + :param agent: agent to be visualised + """ + + self._agent = agent + + # o_left is 0 + width_ae = self.__width_ae + + # some offset to the bottom + offset = self.__offset + phi_o = agent.phi + offset + + # Ae tank + self._ae.set_bounds(0.0, phi_o, 0.05, 1 - self._agent.phi) + self._ae1.set_bounds(0.05, phi_o, 0.05, 1 - self._agent.phi) + self._ae2.set_bounds(0.1, phi_o, width_ae - 0.1, 1 - self._agent.phi) + self._r1.set_xdata([width_ae, width_ae + 0.1]) + self._r1.set_ydata([phi_o, phi_o]) + self._ann_ae.set_position(xy=(width_ae / 2, ((1 - self._agent.phi) / 2) + phi_o - 0.02)) + + # W' vessel + self._w_p.set_bounds(0.6, offset, 0.2, 1 - agent.psi) + self._h.set_bounds(0.6, offset, 0.2, 1 - agent.psi) + self._ann_w_p.set_position(xy=( + 0.6 + (0.2 / 2), + ((1 - agent.psi) / 2) + offset - 0.02)) + + # update levels + self._h.set_height(1 - self._agent.get_h()) + + def hide_basic_annotations(self): + """ + Simply hides the S, LF, and LS text + """ + self._ann_ae.set_text("") + self._ann_w_p.set_text("") + + def update_animation_data(self, frame_number): + """ + For animations and simulations. + The function to call at each frame. + :param frame_number: frame number has to be taken because of parent class method + :return: an iterable of artists + """ + + if not self._animated: + raise UserWarning("Animation flag has to be enabled in order to use this function") + + # perform one step + cur_time = self._agent.get_time() + power = self._agent.perform_one_step() + + # draw some information + self._ann_time.set_text("agent \n time: {}".format(int(cur_time))) + + # power arrow + self._ann_power_flow.set_text("power: {}".format(round(power))) + self._arr_power_flow.set_mutation_scale(math.log(power + 1) * 10) + + p_o = round(self._agent.get_p_o()) + + # oxygen arrow + max_str = "(CP)" if p_o == self._agent.m_u else "" + self._ann_r1_flow.set_text("flow: {} {}".format(p_o, max_str)) + self._arr_r1_flow.set_mutation_scale(math.log(p_o + 1) * 10) + + # update levels + self._h.set_height(1 - self._agent.get_h()) + + # list of artists to be drawn + return [self._ann_time, + self._ann_power_flow, + self._arr_power_flow, + self._h] From b5382d0a8b8222aa4c91828650abacb997ee0fce Mon Sep 17 00:00:00 2001 From: faweigend Date: Tue, 28 Jun 2022 18:45:45 +1000 Subject: [PATCH 62/71] fix h estimation in 2t model --- example_scripts/2tm_interactive_simulation.py | 2 +- .../agents/three_comp_hyd_agent.py | 6 +- src/threecomphyd/agents/two_comp_hyd_agent.py | 97 +++++++++++-------- .../visualiser/two_comp_visualisation.py | 54 +++++++---- 4 files changed, 94 insertions(+), 65 deletions(-) diff --git a/example_scripts/2tm_interactive_simulation.py b/example_scripts/2tm_interactive_simulation.py index 9394ae8..4067a17 100644 --- a/example_scripts/2tm_interactive_simulation.py +++ b/example_scripts/2tm_interactive_simulation.py @@ -9,7 +9,7 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # create two component hydraulic agent with example configuration - agent = TwoCompHydAgent(10, w_p=28000, m_u=240, phi=0.5, psi=0.1) + agent = TwoCompHydAgent(an=28000, cp=240, phi=0.5, psi=0.1, hz=10) # run the interactive animation ani = TwoCompHydAnimation(agent) diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 3e31169..2110772 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -189,21 +189,21 @@ def _estimate_possible_power_output(self): return self._pow - def is_exhausted(self): + def is_exhausted(self) -> bool: """ exhaustion is reached when level in LF cannot sustain power demand :return: simply returns the exhausted boolean """ return bool(self.__h >= 1.0) - def is_recovered(self): + def is_recovered(self) -> bool: """ recovery is estimated according to w_p ratio :return: simply returns the recovered boolean """ return self.get_w_p_ratio() == 1.0 - def is_equilibrium(self): + def is_equilibrium(self) -> bool: """ equilibrium is reached when ph meets pow and LS does not contribute or drain :return: boolean diff --git a/src/threecomphyd/agents/two_comp_hyd_agent.py b/src/threecomphyd/agents/two_comp_hyd_agent.py index dbcdbb9..bf4e8f5 100644 --- a/src/threecomphyd/agents/two_comp_hyd_agent.py +++ b/src/threecomphyd/agents/two_comp_hyd_agent.py @@ -3,33 +3,36 @@ class TwoCompHydAgent(HydAgentBasis): - def __init__(self, hz, w_p, m_u, phi, psi: float = 0.0): + def __init__(self, an: float, cp: float, phi: float, psi: float = 0.0, hz: int = 10): """ - :param hz: calculations per second - :param w_p: cross sectional area of W' - :param m_u: maximal flow from O to w' + :param an: capacity of An + :param cp: maximal flow from Ae to An :param phi: phi (distance Ae to bottom) - :param phi: psi (distance W' to top) + :param phi: psi (distance An to top) + :param hz: calculations per second """ super().__init__(hz=hz) if psi > 1 - phi: - raise UserWarning("Top of W\' has to be above or at bottom of Ae (psi > 1 - phi must be False)") + if abs(psi - (1 - phi)) < 0.001: + psi = 1 - phi + else: + raise UserWarning("Top of An has to be above or at bottom of Ae (psi > 1 - phi must be False)") # constants - self.__phi = phi - self.__psi = psi - self.__w_p = w_p # area of vessel W' - self.__m_u = m_u # max flow from O to P (max aerobic energy consumption i.e. VO2max) + self.__phi = phi # bottom of Ae tank + self.__psi = psi # top of An tank + self.__an = an # capacity of An tank + self.__cp = cp # max flow from Ae to An # variable parameters - self.__h = self.__psi # state of depletion of vessel W' - self.__p_o = 0 # flow through R1 (oxygen pipe) + self.__h = 0 # state of depletion of vessel W' + self.__p_ae = 0 # flow from Ae @property - def w_p(self): + def an(self): """:return cross sectional area of W'""" - return self.__w_p + return self.__an @property def phi(self): @@ -42,63 +45,75 @@ def psi(self): return self.__psi @property - def m_u(self): + def cp(self): """:return max flow through R1""" - return self.__m_u + return self.__cp + + def get_w_p_balance(self): + """:return remaining energy in W' tank""" + return (1 - self.__h) * self.__an def get_h(self): """:return state of depletion of vessel P""" return self.__h - def get_p_o(self): + def get_p_ae(self): """:return flow through R1""" - return self.__p_o + return self.__p_ae def _estimate_possible_power_output(self): """ Update internal capacity estimations by one step. :return: the amount of power that the athlete was able to put out """ - p = self._pow - # determine oxygen energy flow (R1) - # level P above pipe exit. Scale contribution according to h level - if self.__psi < self.__h <= (1.0 - self.__phi): - # contribution through R1 scales with maximal flow capacity - self.__p_o = self.__m_u * (self.__h / (1.0 - self.__phi)) - # at maximum rate because level h of P is below pipe exit of O - elif (1.0 - self.__phi) < self.__h <= 1.0: - # above aerobic threshold (?) max contribution R1 = m_o - self.__p_o = self.__m_u + # the change on fill-level of An by flow from tap + self.__h += self._pow / self.__an / self._hz + + # level An above pipe exit. Scale flow according to h level + if (self.__h + self.__psi) <= (1.0 - self.__phi): + self.__p_ae = self.__cp * (self.__h + self.__psi) / (1.0 - self.__phi) + + # at maximum rate because level h is below pipe exit of p_Ae + else: + self.__p_ae = self.__cp + + # due to psi there might be pressure on p_ae even though the tap is closed and An is full + if self.__p_ae > self._pow: + self.__p_ae = self._pow + + # consider hz (delta t) + self.__p_ae = self.__p_ae / self._hz - # the change on level in W' is determined by flow p_o - self.__h += ((p - self.__p_o) / self.__w_p) / self._hz + # the change on fill-level of An by flow from Ae + self.__h -= self.__p_ae / self.__an # also W' cannot be fuller than full - if self.__h < self.__psi: - self.__h = self.__psi + if self.__h < 0: + self.__h = 0 + # ...or emptier than empty elif self.__h > 1: self.__h = 1 return self._pow - def is_exhausted(self): + def is_exhausted(self) -> bool: """ - exhaustion is reached when level in AnF cannot sustain power demand + exhaustion is reached when level in An cannot sustain power demand :return: simply returns the exhausted flag """ return self.__h == 1.0 - def is_recovered(self): + def is_recovered(self) -> bool: """ - recovery is complete when W' is full again - :return: simply returns the recovered flag + recovery is complete when An is full again + :return: simply returns boolean flag """ return self.__h == self.__psi - def is_equilibrium(self): + def is_equilibrium(self) -> bool: """ - equilibrium is reached when po meets pow - :return: boolean + equilibrium is reached when p_ae meets p + :return: boolean flag """ - return abs(self.__p_o - self._pow) < 0.1 + return abs(self.__p_ae - self._pow) < 0.01 diff --git a/src/threecomphyd/visualiser/two_comp_visualisation.py b/src/threecomphyd/visualiser/two_comp_visualisation.py index 20ae32d..7b35c72 100644 --- a/src/threecomphyd/visualiser/two_comp_visualisation.py +++ b/src/threecomphyd/visualiser/two_comp_visualisation.py @@ -39,7 +39,7 @@ def __init__(self, agent: TwoCompHydAgent, # plot if no axis was assigned if axis is None: - fig = plt.figure(figsize=(6, 4.2)) + fig = plt.figure(figsize=(4, 4.2)) self._ax1 = fig.add_subplot(1, 1, 1) else: fig = None @@ -63,8 +63,8 @@ def __init__(self, agent: TwoCompHydAgent, self._agent = agent self.__offset = 0.2 - # U tank with three stripes - self.__width_ae = 0.5 + self.__width_ae = 0.5 # tank with three stripes + self.__width_w_p = 0.4 self._ae = None self._ae1 = None self._ae2 = None @@ -103,6 +103,7 @@ def __init__(self, agent: TwoCompHydAgent, # add layout for detailed annotations # detail annotation add greek letters for distances and positions if self.__detail_annotations: + self._ann_r1_max_flow = None self.__set_detailed_annotations_layout() self._ax1.set_xlim(0, 1.05) self._ax1.set_ylim(0, 1.2) @@ -137,8 +138,10 @@ def __set_detailed_annotations_layout(self): rcParams['text.usetex'] = True - self._ann_r1_flow = Text(text="$CP$", ha='right', fontsize="xx-large", x=ae_width + 0.09, + self._ann_r1_max_flow = Text(text="$CP$", ha='right', fontsize="xx-large", x=ae_width + 0.09, y=phi_o - 0.08) + self._ann_r1_flow = Text(text="$p_{Ae}$", ha='right', fontsize="xx-large", x=ae_width + 0.09, + y=phi_o + 0.06) self._arr_r1_flow = FancyArrowPatch((ae_width, phi_o), (ae_width + 0.1, phi_o), arrowstyle='-|>', @@ -164,8 +167,10 @@ def __set_detailed_annotations_layout(self): ) self._ax1.annotate('$\psi$', - xy=(ae_width + 0.2, 1 - self._agent.psi + offset), - xytext=(ae_width + 0.2, 1 - self._agent.psi / 2 + offset - 0.015), + xy=(ae_width + 0.1 + self.__width_w_p/2, + 1 - self._agent.psi + offset), + xytext=(ae_width + 0.1 + self.__width_w_p/2, + 1 - self._agent.psi / 2 + offset - 0.015), ha='center', fontsize="xx-large", arrowprops=dict(arrowstyle='-|>', @@ -173,8 +178,10 @@ def __set_detailed_annotations_layout(self): fc=self.__ann_color) ) self._ax1.annotate('$\psi$', - xy=(ae_width + 0.2, 1 + offset), - xytext=(ae_width + 0.2, 1 - self._agent.psi / 2 + offset - 0.015), + xy=(ae_width + 0.1 + self.__width_w_p/2, + 1 + offset), + xytext=(ae_width + 0.1 + self.__width_w_p/2, + 1 - self._agent.psi / 2 + offset - 0.015), ha='center', fontsize="xx-large", arrowprops=dict(arrowstyle='-|>', @@ -247,6 +254,7 @@ def __set_detailed_annotations_layout(self): self._ax1.add_artist(self._arr_power_flow) self._ax1.add_artist(self._arr_r1_flow) self._ax1.add_artist(self._ann_r1_flow) + self._ax1.add_artist(self._ann_r1_max_flow) def __set_animation_layout(self): """ @@ -257,7 +265,7 @@ def __set_animation_layout(self): o_width = self.__width_ae phi_o = self._agent.phi + offset - # U flow (R1) + # Ae flow (R1) self._arr_r1_flow = FancyArrowPatch((o_width, phi_o), (o_width + 0.1, phi_o), arrowstyle='simple', @@ -310,10 +318,12 @@ def __set_basic_layout(self): y=phi_o + ((1 - self._agent.phi) / 2)) # W' tank - self._w_p = Rectangle((0.6, offset), 0.2, 1 - self._agent.psi, fill=False, ec="black") - self._h = Rectangle((0.6, offset), 0.2, 1 - self._agent.psi, color=self.__w_p_color) + self._w_p = Rectangle((self.__width_ae + 0.1, offset), self.__width_w_p, 1 - self._agent.psi, fill=False, + ec="black") + self._h = Rectangle((self.__width_ae + 0.1, offset), self.__width_w_p, 1 - self._agent.psi, + color=self.__w_p_color) self._ann_w_p = Text(text="$W^\prime$", ha='center', fontsize="xx-large", - x=0.7, + x=self.__width_ae + 0.1 + self.__width_w_p / 2, y=offset + (1 - self._agent.psi) / 2) # the basic layout @@ -350,14 +360,18 @@ def update_basic_layout(self, agent: TwoCompHydAgent): self._ann_ae.set_position(xy=(width_ae / 2, ((1 - self._agent.phi) / 2) + phi_o - 0.02)) # W' vessel - self._w_p.set_bounds(0.6, offset, 0.2, 1 - agent.psi) - self._h.set_bounds(0.6, offset, 0.2, 1 - agent.psi) + self._w_p.set_bounds(self.__width_ae + 0.1, + offset, + self.__width_w_p, + 1 - agent.psi) + self._h.set_bounds(self.__width_ae + 0.1, + offset, self.__width_w_p, 1 - agent.psi) self._ann_w_p.set_position(xy=( - 0.6 + (0.2 / 2), + self.__width_ae + 0.1 + self.__width_w_p / 2, ((1 - agent.psi) / 2) + offset - 0.02)) # update levels - self._h.set_height(1 - self._agent.get_h()) + self._h.set_height(1 - self._agent.psi - self._agent.get_h()) def hide_basic_annotations(self): """ @@ -388,15 +402,15 @@ def update_animation_data(self, frame_number): self._ann_power_flow.set_text("power: {}".format(round(power))) self._arr_power_flow.set_mutation_scale(math.log(power + 1) * 10) - p_o = round(self._agent.get_p_o()) + p_o = round(self._agent.get_p_ae()) # oxygen arrow - max_str = "(CP)" if p_o == self._agent.m_u else "" - self._ann_r1_flow.set_text("flow: {} {}".format(p_o, max_str)) + max_str = "(CP)" if p_o == self._agent.cp else "" + self._ann_r1_flow.set_text("flow: {} {}".format(p_o * self._agent.hz, max_str)) self._arr_r1_flow.set_mutation_scale(math.log(p_o + 1) * 10) # update levels - self._h.set_height(1 - self._agent.get_h()) + self._h.set_height(1 - self._agent.psi - self._agent.get_h()) # list of artists to be drawn return [self._ann_time, From ef3d54f6f9f2567e4c9c58594b77b8fe07a1c6b8 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 29 Jun 2022 18:43:42 +1000 Subject: [PATCH 63/71] some 2tm fixes and changes to the cp fitting example --- .../visualiser/two_comp_visualisation.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/threecomphyd/visualiser/two_comp_visualisation.py b/src/threecomphyd/visualiser/two_comp_visualisation.py index 7b35c72..3e1064a 100644 --- a/src/threecomphyd/visualiser/two_comp_visualisation.py +++ b/src/threecomphyd/visualiser/two_comp_visualisation.py @@ -71,10 +71,10 @@ def __init__(self, agent: TwoCompHydAgent, self._r1 = None # line marking flow from Ae to W' self._ann_ae = None # Ae annotation - # W' tank - self._w_p = None + # An tank + self._an = None self._h = None # fill state - self._ann_w_p = None # annotation + self._ann_an = None # annotation # finish the basic layout self.__set_basic_layout() @@ -129,8 +129,8 @@ def __set_detailed_annotations_layout(self): """ ae_width = self.__width_ae - w_p_left = self._w_p.get_x() - w_p_width = self._w_p.get_width() + an_left = self._an.get_x() + an_width = self._an.get_width() # some offset to the bottom offset = self.__offset @@ -189,23 +189,23 @@ def __set_detailed_annotations_layout(self): fc=self.__ann_color) ) - self._ann_power_flow = Text(text="$p$", ha='center', fontsize="xx-large", x=self._ann_w_p.get_position()[0], + self._ann_power_flow = Text(text="$p$", ha='center', fontsize="xx-large", x=self._ann_an.get_position()[0], y=offset - 0.06) - self._arr_power_flow = FancyArrowPatch((self._ann_w_p.get_position()[0], offset - 0.078), - (self._ann_w_p.get_position()[0], 0.0), + self._arr_power_flow = FancyArrowPatch((self._ann_an.get_position()[0], offset - 0.078), + (self._ann_an.get_position()[0], 0.0), arrowstyle='-|>', mutation_scale=30, color=self.__p_color) - self._h.update(dict(xy=(w_p_left, offset), - width=w_p_width, + self._h.update(dict(xy=(an_left, offset), + width=an_width, height=0.15, color=self.__w_p_color)) self._ax1.annotate('$h$', - xy=(self._ann_w_p.get_position()[0] + 0.07, + xy=(self._ann_an.get_position()[0] + 0.07, 1 - self._agent.psi + offset), - xytext=(self._ann_w_p.get_position()[0] + 0.07, + xytext=(self._ann_an.get_position()[0] + 0.07, (1 - self._agent.psi - self._h.get_height()) / 2 + self._h.get_height() + offset), ha='center', fontsize="xx-large", @@ -214,9 +214,9 @@ def __set_detailed_annotations_layout(self): fc=self.__ann_color) ) self._ax1.annotate('$h$', - xy=(self._ann_w_p.get_position()[0] + 0.07, + xy=(self._ann_an.get_position()[0] + 0.07, self._h.get_height() + offset), - xytext=(self._ann_w_p.get_position()[0] + 0.07, + xytext=(self._ann_an.get_position()[0] + 0.07, (1 - self._agent.psi - self._h.get_height()) / 2 + self._h.get_height() + offset), ha='center', fontsize="xx-large", @@ -274,12 +274,12 @@ def __set_animation_layout(self): self._ann_r1_flow = Text(text="flow: ", ha='right', fontsize="large", x=o_width, y=phi_o - 0.05) # Tap flow (Power) - self._arr_power_flow = FancyArrowPatch((self._ann_w_p.get_position()[0], offset - 0.05), - (self._ann_w_p.get_position()[0], 0.0), + self._arr_power_flow = FancyArrowPatch((self._ann_an.get_position()[0], offset - 0.05), + (self._ann_an.get_position()[0], 0.0), arrowstyle='simple', mutation_scale=0, color=self.__p_color) - self._ann_power_flow = Text(text="flow: ", ha='center', fontsize="large", x=self._ann_w_p.get_position()[0], + self._ann_power_flow = Text(text="flow: ", ha='center', fontsize="large", x=self._ann_an.get_position()[0], y=offset - 0.05) # information annotation @@ -318,23 +318,23 @@ def __set_basic_layout(self): y=phi_o + ((1 - self._agent.phi) / 2)) # W' tank - self._w_p = Rectangle((self.__width_ae + 0.1, offset), self.__width_w_p, 1 - self._agent.psi, fill=False, - ec="black") + self._an = Rectangle((self.__width_ae + 0.1, offset), self.__width_w_p, 1 - self._agent.psi, fill=False, + ec="black") self._h = Rectangle((self.__width_ae + 0.1, offset), self.__width_w_p, 1 - self._agent.psi, color=self.__w_p_color) - self._ann_w_p = Text(text="$W^\prime$", ha='center', fontsize="xx-large", - x=self.__width_ae + 0.1 + self.__width_w_p / 2, - y=offset + (1 - self._agent.psi) / 2) + self._ann_an = Text(text="An", ha='center', fontsize="xx-large", + x=self.__width_ae + 0.1 + self.__width_w_p / 2, + y=offset + (1 - self._agent.psi) / 2) # the basic layout self._ax1.add_line(self._r1) self._ax1.add_artist(self._ae) self._ax1.add_artist(self._ae1) self._ax1.add_artist(self._ae2) - self._ax1.add_artist(self._w_p) + self._ax1.add_artist(self._an) self._ax1.add_artist(self._h) self._ax1.add_artist(self._ann_ae) - self._ax1.add_artist(self._ann_w_p) + self._ax1.add_artist(self._ann_an) def update_basic_layout(self, agent: TwoCompHydAgent): """ @@ -359,14 +359,14 @@ def update_basic_layout(self, agent: TwoCompHydAgent): self._r1.set_ydata([phi_o, phi_o]) self._ann_ae.set_position(xy=(width_ae / 2, ((1 - self._agent.phi) / 2) + phi_o - 0.02)) - # W' vessel - self._w_p.set_bounds(self.__width_ae + 0.1, - offset, - self.__width_w_p, - 1 - agent.psi) + # An tank + self._an.set_bounds(self.__width_ae + 0.1, + offset, + self.__width_w_p, + 1 - agent.psi) self._h.set_bounds(self.__width_ae + 0.1, offset, self.__width_w_p, 1 - agent.psi) - self._ann_w_p.set_position(xy=( + self._ann_an.set_position(xy=( self.__width_ae + 0.1 + self.__width_w_p / 2, ((1 - agent.psi) / 2) + offset - 0.02)) @@ -378,7 +378,7 @@ def hide_basic_annotations(self): Simply hides the S, LF, and LS text """ self._ann_ae.set_text("") - self._ann_w_p.set_text("") + self._ann_an.set_text("") def update_animation_data(self, frame_number): """ From 693e0fca63a5b8b332784633b70883fc259d5f59 Mon Sep 17 00:00:00 2001 From: faweigend Date: Sun, 10 Jul 2022 18:22:54 +1000 Subject: [PATCH 64/71] fix 2tm recovery error and shorten units in plots --- example_scripts/2tm_interactive_simulation.py | 2 +- src/threecomphyd/agents/two_comp_hyd_agent.py | 22 +++++++++---------- .../visualiser/two_comp_visualisation.py | 10 ++++----- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/example_scripts/2tm_interactive_simulation.py b/example_scripts/2tm_interactive_simulation.py index 4067a17..c625fad 100644 --- a/example_scripts/2tm_interactive_simulation.py +++ b/example_scripts/2tm_interactive_simulation.py @@ -9,7 +9,7 @@ format="%(asctime)s %(levelname)-5s %(name)s - %(message)s. [file=%(filename)s:%(lineno)d]") # create two component hydraulic agent with example configuration - agent = TwoCompHydAgent(an=28000, cp=240, phi=0.5, psi=0.1, hz=10) + agent = TwoCompHydAgent(an=28000, cp=240, phi=0.5, psi=0.4, hz=10) # run the interactive animation ani = TwoCompHydAnimation(agent) diff --git a/src/threecomphyd/agents/two_comp_hyd_agent.py b/src/threecomphyd/agents/two_comp_hyd_agent.py index bf4e8f5..19b98b6 100644 --- a/src/threecomphyd/agents/two_comp_hyd_agent.py +++ b/src/threecomphyd/agents/two_comp_hyd_agent.py @@ -51,7 +51,7 @@ def cp(self): def get_w_p_balance(self): """:return remaining energy in W' tank""" - return (1 - self.__h) * self.__an + return (1.0 - self.psi - self.__h) / (1.0 - self.psi) * self.__an def get_h(self): """:return state of depletion of vessel P""" @@ -68,7 +68,7 @@ def _estimate_possible_power_output(self): """ # the change on fill-level of An by flow from tap - self.__h += self._pow / self.__an / self._hz + self.__h += (1.0 - self.psi) * self._pow / self.__an / self._hz # level An above pipe exit. Scale flow according to h level if (self.__h + self.__psi) <= (1.0 - self.__phi): @@ -78,22 +78,22 @@ def _estimate_possible_power_output(self): else: self.__p_ae = self.__cp - # due to psi there might be pressure on p_ae even though the tap is closed and An is full - if self.__p_ae > self._pow: - self.__p_ae = self._pow - # consider hz (delta t) self.__p_ae = self.__p_ae / self._hz + # due to psi there might be pressure on p_ae even though the tap is closed and An is full + if self.__p_ae > self.get_w_p_balance(): + self.__p_ae = self.get_w_p_balance() + # the change on fill-level of An by flow from Ae - self.__h -= self.__p_ae / self.__an + self.__h -= (1.0 - self.psi) * self.__p_ae / self.__an # also W' cannot be fuller than full if self.__h < 0: self.__h = 0 # ...or emptier than empty - elif self.__h > 1: - self.__h = 1 + elif self.__h > 1.0 - self.psi: + self.__h = 1.0 - self.psi return self._pow @@ -102,14 +102,14 @@ def is_exhausted(self) -> bool: exhaustion is reached when level in An cannot sustain power demand :return: simply returns the exhausted flag """ - return self.__h == 1.0 + return self.__h == 1.0 - self.psi def is_recovered(self) -> bool: """ recovery is complete when An is full again :return: simply returns boolean flag """ - return self.__h == self.__psi + return self.__h == 0 def is_equilibrium(self) -> bool: """ diff --git a/src/threecomphyd/visualiser/two_comp_visualisation.py b/src/threecomphyd/visualiser/two_comp_visualisation.py index 3e1064a..28d1950 100644 --- a/src/threecomphyd/visualiser/two_comp_visualisation.py +++ b/src/threecomphyd/visualiser/two_comp_visualisation.py @@ -139,7 +139,7 @@ def __set_detailed_annotations_layout(self): rcParams['text.usetex'] = True self._ann_r1_max_flow = Text(text="$CP$", ha='right', fontsize="xx-large", x=ae_width + 0.09, - y=phi_o - 0.08) + y=phi_o - 0.08) self._ann_r1_flow = Text(text="$p_{Ae}$", ha='right', fontsize="xx-large", x=ae_width + 0.09, y=phi_o + 0.06) self._arr_r1_flow = FancyArrowPatch((ae_width, phi_o), @@ -167,9 +167,9 @@ def __set_detailed_annotations_layout(self): ) self._ax1.annotate('$\psi$', - xy=(ae_width + 0.1 + self.__width_w_p/2, + xy=(ae_width + 0.1 + self.__width_w_p / 2, 1 - self._agent.psi + offset), - xytext=(ae_width + 0.1 + self.__width_w_p/2, + xytext=(ae_width + 0.1 + self.__width_w_p / 2, 1 - self._agent.psi / 2 + offset - 0.015), ha='center', fontsize="xx-large", @@ -178,9 +178,9 @@ def __set_detailed_annotations_layout(self): fc=self.__ann_color) ) self._ax1.annotate('$\psi$', - xy=(ae_width + 0.1 + self.__width_w_p/2, + xy=(ae_width + 0.1 + self.__width_w_p / 2, 1 + offset), - xytext=(ae_width + 0.1 + self.__width_w_p/2, + xytext=(ae_width + 0.1 + self.__width_w_p / 2, 1 - self._agent.psi / 2 + offset - 0.015), ha='center', fontsize="xx-large", From 8153f8c19e3cc709db8a14078ace30044fc59a22 Mon Sep 17 00:00:00 2001 From: faweigend Date: Thu, 14 Jul 2022 17:13:44 +1000 Subject: [PATCH 65/71] minor plot adjustments --- example_scripts/model_behaviour_plots.py | 24 +++++++++---------- .../handler/pygmo_fitting_report_creator.py | 5 +++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/example_scripts/model_behaviour_plots.py b/example_scripts/model_behaviour_plots.py index 5217933..0bce6de 100644 --- a/example_scripts/model_behaviour_plots.py +++ b/example_scripts/model_behaviour_plots.py @@ -28,7 +28,7 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): two_p_color = "tab:blue" # fig sizes to make optimal use of space in paper - fig = plt.figure(figsize=(8, 3.4)) + fig = plt.figure(figsize=(5, 4)) ax = fig.add_subplot(1, 1, 1) resolution = 1 @@ -45,7 +45,7 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): ax.set_yticklabels([]) # small zoomed-in detail window - insert_ax = ax.inset_axes([0.3, 0.40, 0.3, 0.45]) + insert_ax = ax.inset_axes([0.18, 0.40, 0.3, 0.45]) detail_obs = resolution * 5 detail_ts = [120, 150, 180, 210] detail_ps = [] @@ -86,13 +86,13 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): # label axis and lines ax.set_xlabel("time to exhaustion (min)") - ax.set_ylabel("intensity (watt)", labelpad=10) + ax.set_ylabel("intensity (W)", labelpad=10) # insert number of models only if more than 1 was plotted if len(ps) > 1: - ax.plot([], linestyle='-', linewidth=1, color=hyd_color, label="hydraulic model ({})".format(len(ps))) + ax.plot([], linestyle='-', linewidth=1, color=hyd_color, label="$\mathrm{hydraulic}_\mathrm{weig}$" + " ({})".format(len(ps))) else: - ax.plot([], linestyle='-', linewidth=1, color=hyd_color, label="hydraulic model") + ax.plot([], linestyle='-', linewidth=1, color=hyd_color, label="$\mathrm{hydraulic}_\mathrm{weig}$") ax.legend() plt.tight_layout() @@ -125,7 +125,7 @@ def multiple_caen_recovery_overview(w_p: float, cp: float, ps: list): rec_ps = [cp_33, cp_66] rec_ts = [10, 20, 25, 30, 35, 40, 45, 50, 60, 70, 90, 110, 130, 150, 170, 240, 300, 360] - fig = plt.figure(figsize=(8, 3.4)) + fig = plt.figure(figsize=(6, 3.4)) # using combined CP33 and CP66 measures # only two plots with combined recovery intensities axes = [fig.add_subplot(1, 2, 1), fig.add_subplot(1, 2, 2)] @@ -167,25 +167,25 @@ def multiple_caen_recovery_overview(w_p: float, cp: float, ps: list): axes[0].errorbar(caen_combination["P4"][0], caen_combination["P4"][1], caen_combination["P4"][2], - label="Caen et al.", + label="Caen et al. 2019", linestyle='None', marker='o', capsize=3, color=c_color) - axes[0].set_title("P4") + axes[0].set_title("$P240$") axes[1].errorbar(caen_combination["P8"][0], caen_combination["P8"][1], caen_combination["P8"][2], - label="Caen et al.", + label="Caen et al. 2019", linestyle='None', marker='o', capsize=3, color=c_color) - axes[1].set_title("P8") + axes[1].set_title("$P480$") # insert number of models only if more than 1 was plotted if len(ps) > 1: - axes[0].plot([], linestyle='-', linewidth=1, color=hyd_color, label="hydraulic model ({})".format(len(ps))) + axes[0].plot([], linestyle='-', linewidth=1, color=hyd_color, label="$\mathrm{hydraulic}_\mathrm{weig}$" + " ({})".format(len(ps))) else: axes[0].plot([], linestyle='-', linewidth=1, color=hyd_color, label="hydraulic model") @@ -201,7 +201,7 @@ def multiple_caen_recovery_overview(w_p: float, cp: float, ps: list): fig.text(0.5, 0.04, 'recovery duration (min)', ha='center') fig.text(0.01, 0.5, 'recovery (%)', va='center', rotation='vertical') plt.tight_layout() - plt.subplots_adjust(left=0.09, bottom=0.20, top=0.91) + plt.subplots_adjust(left=0.12, bottom=0.20, top=0.91) plt.show() plt.close(fig) diff --git a/src/threecomphyd/handler/pygmo_fitting_report_creator.py b/src/threecomphyd/handler/pygmo_fitting_report_creator.py index fddb7b9..f7942e1 100644 --- a/src/threecomphyd/handler/pygmo_fitting_report_creator.py +++ b/src/threecomphyd/handler/pygmo_fitting_report_creator.py @@ -7,6 +7,7 @@ import threecomphyd.config as config import numpy as np +from pypermod.utility import PlotLayout from threecomphyd.evolutionary_fitter.pygmo_three_comp_fitter import PyGMOThreeCompFitter from threecomphyd.evolutionary_fitter.three_comp_tools import multi_to_single_objective @@ -237,7 +238,8 @@ def __plot_best_front(self, root, file, save_path): v = list(fronts.values())[-1] - fig = plt.figure(figsize=(8, 5)) + fig = plt.figure(figsize=(5, 4)) + PlotLayout.set_rc_params() axes = fig.add_subplot(1, 1, 1) # store the best solution details in here @@ -274,6 +276,7 @@ def __plot_best_front(self, root, file, save_path): axes.set_xlabel("expenditure NRMSE") axes.set_ylabel("recovery NRMSE") + plt.tight_layout() plt.savefig(os.path.join(save_path, "best_front_{}".format(len(fronts.values())))) plt.close(fig) From 836fd34a8466353253d420443ac2f174810b9081 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 18 Jul 2022 21:17:32 +1000 Subject: [PATCH 66/71] add Strava Bike exercise type and scripts for model simulations --- src/threecomphyd/agents/three_comp_hyd_agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/threecomphyd/agents/three_comp_hyd_agent.py b/src/threecomphyd/agents/three_comp_hyd_agent.py index 2110772..cbf8c11 100644 --- a/src/threecomphyd/agents/three_comp_hyd_agent.py +++ b/src/threecomphyd/agents/three_comp_hyd_agent.py @@ -223,7 +223,7 @@ def get_w_p_ratio(self): """ :return: wp estimation between 0 and 1 for comparison to CP models """ - return (1.0 - self.__h) * (1.0 - self.__g) + return 1.0 - self.__h def get_fill_lf(self): """ From e4303125ff687632e76f38097bdd3c123de99261 Mon Sep 17 00:00:00 2001 From: faweigend Date: Sat, 23 Jul 2022 18:27:58 +1000 Subject: [PATCH 67/71] extend simple rec measures to deal with fitness investigator and adjust parameter limits --- example_scripts/model_behaviour_plots.py | 1 - .../data_structure/simple_rec_measures.py | 40 +++++++++++++++++- .../data_structure/simple_tte_measures.py | 14 +++++++ .../evolutionary_fitter/three_comp_tools.py | 42 +++++++++---------- 4 files changed, 74 insertions(+), 23 deletions(-) diff --git a/example_scripts/model_behaviour_plots.py b/example_scripts/model_behaviour_plots.py index 0bce6de..1abdab7 100644 --- a/example_scripts/model_behaviour_plots.py +++ b/example_scripts/model_behaviour_plots.py @@ -35,7 +35,6 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): ts_ext = np.arange(120, 1801, 20 / resolution) ts = [120, 240, 360, 600, 900, 1800] - powers = [((w_p + x * cp) / x) for x in ts] powers_ext = [((w_p + x * cp) / x) for x in ts_ext] # mark P4 and P8 diff --git a/src/threecomphyd/data_structure/simple_rec_measures.py b/src/threecomphyd/data_structure/simple_rec_measures.py index fdc6316..682c146 100644 --- a/src/threecomphyd/data_structure/simple_rec_measures.py +++ b/src/threecomphyd/data_structure/simple_rec_measures.py @@ -1,6 +1,7 @@ class SimpleRecMeasures: """ - Used store W' recovery ratios in a consistent way for evolutionary fitting estimations + Used store W' recovery ratios in a consistent way for evolutionary fitting estimations. Parameters are named + according to the WB1->RB->WB2 protocol """ def __init__(self, name: str): @@ -48,3 +49,40 @@ def name(self): :return: the defined name """ return self.__name + + def get_all_wb_rb_combinations(self): + """ + returns all combinations of power that lead to exhaustion (WB) and recovery intensity (RB) that + this recovery measure storage contains + :return: + """ + combs = [] + for values in list(self.__measures): + comb = (values[0], values[1]) + if comb not in combs: + combs.append(comb) + return combs + + def get_all_obs_for_wb_rb_combination(self, p_work, p_rec): + """ + Get all observations for a p_work and p_rec combination. + returns times and ratios in two lists. + :param p_work: power that lead to exhaustion (WB) + :param p_rec: recovery intensity (RB) + :return: times, ratios + """ + times, ratios = [], [] + for values in list(self.__measures): + if values[0] == p_work and values[1] == p_rec: + times.append(values[2]) + ratios.append(values[3]) + return times, ratios + + def get_max_t_rec(self): + """ + :return: maximal recovery time in stored trials + """ + max_t = 0 + for values in list(self.__measures): + max_t = max(max_t, values[2]) + return max_t diff --git a/src/threecomphyd/data_structure/simple_tte_measures.py b/src/threecomphyd/data_structure/simple_tte_measures.py index b0f10d6..4c74c86 100644 --- a/src/threecomphyd/data_structure/simple_tte_measures.py +++ b/src/threecomphyd/data_structure/simple_tte_measures.py @@ -38,6 +38,20 @@ def pairs(self): """ return self.__pairs + @property + def times(self): + """ + :return: first entries of stored pairs + """ + return [x[0] for x in self.__pairs] + + @property + def powers(self): + """ + :return: second entries of stored pairs + """ + return [x[1] for x in self.__pairs] + def add_pair(self, t: float, p: float): """ adds a (time,constant power) pair to internal data diff --git a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py index 1a22a0a..73a404b 100644 --- a/src/threecomphyd/evolutionary_fitter/three_comp_tools.py +++ b/src/threecomphyd/evolutionary_fitter/three_comp_tools.py @@ -9,14 +9,14 @@ # bounds for all parameters of the three comp hydraulic model three_comp_parameter_limits = { - "lf": [5000, 500000], - "ls": [5000, 500000], - "m_u": [1, 2000], - "m_ls": [1, 2000], - "m_lf": [1, 2000], - "theta": [0.01, 0.99], # 0.0 and 1.0 are not possible because equations would divide by 0 - "gamma": [0.01, 0.99], - "phi": [0.01, 0.99] + "lf": [1, 500000], + "ls": [1, 500000], + "m_u": [1, 5000], + "m_ls": [1, 5000], + "m_lf": [1, 5000], + "theta": [0, 1], # 0.0 and 1.0 are not possible because equations would divide by 0 + "gamma": [0, 1], + "phi": [0, 1] } @@ -27,21 +27,28 @@ class MultiObjectiveThreeCompUDP: def __init__(self, ttes: SimpleTTEMeasures, - recovery_measures: SimpleRecMeasures): + recovery_measures: SimpleRecMeasures, + hz: int = 1): """ init needs a couple parameters to be able to call the objective function :param ttes: standardised time to exhaustion measures to use :param recovery_measures: standardised recovery ratio measures to use + :param hz: precision of accuracy estimations """ - - self.__hz = 1 # hz - self.__limits = three_comp_parameter_limits # limits + self.__hz = hz # hz self.__ttes = ttes # tte_ps self.__recs = recovery_measures # recovery_trials # transform limits dict into object that returns bounds in expected format - self.__bounds = ([x[0] for x in self.__limits.values()], - [x[1] for x in self.__limits.values()]) + self.__bounds = ([x[0] for x in three_comp_parameter_limits.values()], + [x[1] for x in three_comp_parameter_limits.values()]) + + def get_bounds(self): + """ + bounds for config parameters + :return: bounds in tuple format + """ + return self.__bounds def create_educated_initial_guess(self, cp: float = 250.0, w_p: float = 50000.0): """ @@ -94,13 +101,6 @@ def fitness(self, x): # error measures as fitness and constraint return [tte_nrmse, rec_nrmse] - def get_bounds(self): - """ - bounds for config parameters - :return: bounds in tuple format - """ - return self.__bounds - def get_additional_info(self): """ some additional info regarding the parameters in use From 63cd1e788f7ed3dea31b891d1c1d749324bc713c Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 25 Jul 2022 16:27:26 +1000 Subject: [PATCH 68/71] model behaviour plot adjustments --- example_scripts/model_behaviour_plots.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/example_scripts/model_behaviour_plots.py b/example_scripts/model_behaviour_plots.py index 1abdab7..490951c 100644 --- a/example_scripts/model_behaviour_plots.py +++ b/example_scripts/model_behaviour_plots.py @@ -28,7 +28,7 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): two_p_color = "tab:blue" # fig sizes to make optimal use of space in paper - fig = plt.figure(figsize=(5, 4)) + fig = plt.figure(figsize=(5, 3.5)) ax = fig.add_subplot(1, 1, 1) resolution = 1 @@ -85,17 +85,18 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): # label axis and lines ax.set_xlabel("time to exhaustion (min)") - ax.set_ylabel("intensity (W)", labelpad=10) + ax.set_ylabel("power output (W)", labelpad=10) # insert number of models only if more than 1 was plotted if len(ps) > 1: - ax.plot([], linestyle='-', linewidth=1, color=hyd_color, label="$\mathrm{hydraulic}_\mathrm{weig}$" + " ({})".format(len(ps))) + ax.plot([], linestyle='-', linewidth=1, color=hyd_color, + label="$\mathrm{hydraulic}_\mathrm{weig}$" + " ({})".format(len(ps))) else: ax.plot([], linestyle='-', linewidth=1, color=hyd_color, label="$\mathrm{hydraulic}_\mathrm{weig}$") ax.legend() plt.tight_layout() - plt.subplots_adjust(bottom=0.20, top=0.91) + plt.subplots_adjust(left=0.071, bottom=0.162, top=0.99, right=0.986) plt.show() plt.close(fig) @@ -184,7 +185,8 @@ def multiple_caen_recovery_overview(w_p: float, cp: float, ps: list): # insert number of models only if more than 1 was plotted if len(ps) > 1: - axes[0].plot([], linestyle='-', linewidth=1, color=hyd_color, label="$\mathrm{hydraulic}_\mathrm{weig}$" + " ({})".format(len(ps))) + axes[0].plot([], linestyle='-', linewidth=1, color=hyd_color, + label="$\mathrm{hydraulic}_\mathrm{weig}$" + " ({})".format(len(ps))) else: axes[0].plot([], linestyle='-', linewidth=1, color=hyd_color, label="hydraulic model") @@ -200,7 +202,7 @@ def multiple_caen_recovery_overview(w_p: float, cp: float, ps: list): fig.text(0.5, 0.04, 'recovery duration (min)', ha='center') fig.text(0.01, 0.5, 'recovery (%)', va='center', rotation='vertical') plt.tight_layout() - plt.subplots_adjust(left=0.12, bottom=0.20, top=0.91) + plt.subplots_adjust(left=0.12, bottom=0.20, top=0.91, right=0.99) plt.show() plt.close(fig) From 20bd450fe9953eb554ac6527ffb28542a9784895 Mon Sep 17 00:00:00 2001 From: faweigend Date: Wed, 27 Jul 2022 22:16:12 +1000 Subject: [PATCH 69/71] add averaged elapsed time to fitting report creator --- .../handler/pygmo_fitting_report_creator.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/threecomphyd/handler/pygmo_fitting_report_creator.py b/src/threecomphyd/handler/pygmo_fitting_report_creator.py index f7942e1..872a3c8 100644 --- a/src/threecomphyd/handler/pygmo_fitting_report_creator.py +++ b/src/threecomphyd/handler/pygmo_fitting_report_creator.py @@ -2,6 +2,7 @@ import os import shutil from collections import defaultdict +from datetime import datetime, timedelta import matplotlib.pyplot as plt @@ -343,6 +344,8 @@ def __create_overview_stats_and_plots(self, path: os.path, all_stats: dict, plot all_best_confs = defaultdict(list) all_best_dists = defaultdict(list) all_best_mean_dists = defaultdict(list) + all_times = defaultdict(list) + all_early_stops = defaultdict(list) # best performing solutions are determined bests = {} @@ -368,12 +371,20 @@ def __create_overview_stats_and_plots(self, path: os.path, all_stats: dict, plot # category overview regarding minimal distance and minimal mean distance all_best_dists[k].append(x["dist_min"]) all_best_mean_dists[k].append(x["dist_mean"]) + # "05:02:57" + t = datetime.strptime(x["time_elapsed"], "%H:%M:%S") + # ...and use datetime's hour, min and sec properties to build a timedelta + delta = timedelta(hours=t.hour, minutes=t.minute, seconds=t.second) + all_times[k].append(delta.total_seconds()) + + all_early_stops[k].append(int(x["early_stopping_triggered"] == True)) # all stats merged into one overview merged += v for k in all_stats.keys(): np_best_dists = np.array(all_best_dists[k]) + np_times_elapsed = np.array(all_times[k]) b_config = all_best_confs[k][int(np.argmin(np_best_dists))] # the configuration of the best b_config = [round(x, 2) for x in b_config] @@ -383,6 +394,8 @@ def __create_overview_stats_and_plots(self, path: os.path, all_stats: dict, plot s_config = s_config[:-2] bests.update({"{}({})".format(k, len(np_best_dists)): { + "early_stops" : np.mean(all_early_stops[k]), + "mean_time_elapsed": str(timedelta(seconds=int(np.round(np.mean(np_times_elapsed))))), "mean_best_dist": np.mean(np_best_dists), "std_best_dist": np.std(np_best_dists), "min_best_dist": np.min(np_best_dists), From 3b52c8afbb5ace6d9845c3d980efbd0967bad4a2 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 1 Aug 2022 19:17:30 +1000 Subject: [PATCH 70/71] add VO2 athlete meta --- example_scripts/model_behaviour_plots.py | 130 ++++++----------------- 1 file changed, 30 insertions(+), 100 deletions(-) diff --git a/example_scripts/model_behaviour_plots.py b/example_scripts/model_behaviour_plots.py index 490951c..862903f 100644 --- a/example_scripts/model_behaviour_plots.py +++ b/example_scripts/model_behaviour_plots.py @@ -49,6 +49,9 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): detail_ts = [120, 150, 180, 210] detail_ps = [] + max_err = 0 + max_err_t = 0 + # plot three comp agents for p in ps: three_comp_agent = ThreeCompHydAgent(hz=1, lf=p[0], ls=p[1], m_u=p[2], m_ls=p[3], m_lf=p[4], @@ -63,6 +66,13 @@ def multiple_exhaustion_comparison_overview(w_p: float, cp: float, ps: list): insert_ax.plot(hyd_fitted_times_ext[:detail_obs], hyd_powers_ext[:detail_obs], linestyle='-', linewidth=1, color=hyd_color) + limit = 100 + if max(np.abs(np.array(ts_ext) - np.array(hyd_fitted_times_ext))[:limit]) > max_err: + max_err = np.max(np.abs(np.array(ts_ext) - np.array(hyd_fitted_times_ext))[:limit]) + max_err_t = ts_ext[np.argmax(np.abs(np.array(ts_ext) - np.array(hyd_fitted_times_ext))[:limit])] + + print(max_err, max_err_t) + # plot CP curve insert_ax.plot(ts_ext[:detail_obs], powers_ext[:detail_obs], linestyle='-', linewidth=2, label="critical power\nmodel", color=two_p_color) @@ -221,106 +231,26 @@ def multiple_caen_recovery_overview(w_p: float, cp: float, ps: list): # all configurations for three component hydraulic agents ps = [ - [ - 19267.32481889428, - 53448.210634894436, - 247.71129903795293, - 105.16070293047284, - 15.666725105148853, - 0.7594031586210723, - 0.01174178885462004, - 0.21384965007797813 - ], - [ - 18042.056916563655, - 46718.177938027344, - 247.39628102450715, - 106.77042879166977, - 16.96027055119397, - 0.715113715965181, - 0.01777338005017555, - 0.24959503053279475 - ], - [ - 16663.27479753794, - 58847.492279822916, - 247.33216892510987, - 97.81632336744637, - 17.60675026641434, - 0.6982927653365388, - 0.09473837917127066, - 0.28511732156368097 - ], - [ - 18122.869259409636, - 67893.3143320925, - 248.05526835319083, - 90.11814418883185, - 16.70767452536087, - 0.7239627763005275, - 0.10788624159437807, - 0.24269812950697436 - ], - [ - 16442.047054013587, - 43977.34142455164, - 246.6580206034908, - 112.31940101973737, - 18.851235626075855, - 0.6825405103462707, - 0.011882463932316418, - 0.2859061602516494 - ], - [ - 18241.03024888177, - 52858.78758906142, - 247.23306533702817, - 103.15393367087151, - 16.753404619019577, - 0.7323264858183177, - 0.03138505655373579, - 0.2448593661774296 - ], - [ - 16851.79305167275, - 48348.71227226837, - 246.55295343504088, - 106.85431134985403, - 19.123861764063058, - 0.693498746836151, - 0.03083991609890696, - 0.28415132656652087 - ], - [ - 16350.59391699999, - 40391.18583934175, - 246.40648185859834, - 111.34355485406216, - 19.583788050143703, - 0.6498660573226038, - 0.01016029963401485, - 0.30008300685218803 - ], - [ - 16748.777297458924, - 41432.324234179905, - 246.68605949562686, - 108.72756892117448, - 19.34245943802004, - 0.6596009015402553, - 0.013654881063378194, - 0.2814663041365496 - ], - [ - 17687.258359719104, - 51859.254197641196, - 247.39818147041177, - 103.37418807409084, - 17.566017275093582, - 0.7251325338084225, - 0.03563558893277309, - 0.24824817650787334 - ] + [17456.501540851958, 46235.99782887891, 246.8754194575765, 105.0458949192332, 18.61813360834505, + 0.693542652379417, 0.01997874313126515, 0.2600493216687265], + [17571.44900414276, 43402.54960764281, 246.97054379746947, 108.14610069719515, 17.1821984762747, + 0.6912752767454273, 0.010027987890364522, 0.2645509984088022], + [18052.873627292814, 50179.25213290453, 247.47018172193998, 105.62464445309857, 16.23985284036382, + 0.7290616564998857, 0.028183446547478494, 0.25082072506146275], + [16683.592287141386, 44887.58560272133, 246.63457734640542, 112.68272987053999, 18.073084252052254, + 0.6862224549739296, 0.018586310009923508, 0.2941636955314809], + [16852.46273505409, 42909.778724502234, 246.81384076206803, 108.9815702415051, 18.8647120699937, + 0.6769843873369275, 0.015245173850835667, 0.2787578120456768], + [16606.045920840497, 39679.597398728074, 246.8007548653167, 112.23416682274151, 18.476718183869735, + 0.6555286114289854, 0.010386190778171801, 0.28920487069782524], + [16898.924891950985, 41130.49577887294, 246.86360534861495, 109.21040516611954, 18.828103302561843, + 0.6647849659185002, 0.013486135401909773, 0.2784037171471362], + [17531.522855693423, 45987.13199861467, 246.83527483837145, 110.74973247039583, 18.39464657469822, + 0.7056095369263342, 0.01198807017906641, 0.26591486702753386], + [16312.389121504952, 38982.0514065314, 246.68018865065818, 111.63159367625984, 19.378294909372325, + 0.6445812471142565, 0.010438963567674072, 0.29581431611381087], + [16891.005497924867, 45520.8495750172, 246.842373015698, 115.53307266702876, 17.367498696562347, + 0.7039380779511493, 0.010040967408569901, 0.28745204966698823] ] logging.info("Start Exhaustion Comparison") From 25c0252f2163791640a9031e0bd69b9cc18a9270 Mon Sep 17 00:00:00 2001 From: faweigend Date: Mon, 1 Aug 2022 19:45:38 +1000 Subject: [PATCH 71/71] update version number --- README.md | 2 +- setup.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4610db1..1425244 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,6 @@ CPU cores for the grid search approach. * `evolutionary_fitting.py` Runs 10 fittings on an athlete using the in the script defined parameters for CP, W' and recovery rations by Caen et al. The script uses the published set of best fitting parameters (30 generations, 40 - cycles, 64 population size, 7 islands) to fit the model with the outlined evolutionary computation approach + cycles, 64 population size, 7 or 14 islands) to fit the model with the outlined evolutionary computation approach (see Appendix B and C). Results are stored into a `data-storage` folder in the root directory of the project. diff --git a/setup.cfg b/setup.cfg index 4afd7b0..d99859b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,9 @@ [metadata] name = threecomphyd -version = 1.1.1 +version = 1.2.0 author = Fabian Weigend author_email = fabian.weigend@westernsydney.edu.au -description = Implementation of the generalized Three Component Hydraulic Model +description = Implementation of a generalized Hydraulic Model and a fitting algorithm long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/faweigend/three_comp_hyd