def constrained_cubic_integrate_point(y_coarse, ntx, nti, end=130): """ Like :py:function`constrained_cubic_disaggregation_interval`, this integrates over an interpolated curve, but this assumes the input data is defined pointwise at the start of each interval, so it first does a fit, then integrates, then fits, then takes derivatives. Args: y_coarse (np.ndarray): dependent axis, same size as :math:`x`. This can have more than one dimension, and the algorithm treats all but the last dimension as separate runs. x_coarse (DemographicInterval): independent axis x_fine (DemographicInterval): new independent axis end (float): Where to set the value to zero. Returns: np.ndarray: dependent values on finer axis """ x = np.hstack([ntx.start, [end]]) out_shape = list(y_coarse.shape[:-1]) + [len(nti)] y_out = np.zeros(out_shape, dtype=y_coarse.dtype) y = np.zeros((y_coarse.shape[-1] + 1, )) y[-1] = 0 for draw in itertools.product(*[range(x) for x in y_coarse.shape[:-1]]): y[:-1] = y_coarse[draw] assert x.shape == y.shape fit = PchipInterpolator(x, y, extrapolate=True) # Last entry is for T_{x+5} integrate_once = 1 integrated = fit.antiderivative(integrate_once)(nti.bound) y_out[draw] = np.diff(integrated, 1, axis=-1) return y_out
def constrained_cubic_disaggregation(x_coarse, y_coarse, x_fine): """ Given observed values on a coarse domain, estimate their values on a finer domain. This uses constrained cubic splines. It constructs the cumulative sum of the y, fits the spline, interpolates to the finer x, and then takes differences across intervals. Args: x_coarse (np.ndarray): independent axis y_coarse (np.ndarray): dependent axis, same size as :math:`x` x_fine (np.ndarray): new independent axis Returns: np.ndarray: dependent values on finer axis """ fit = PchipInterpolator(x_coarse, y_coarse, extrapolate=True) # Last entry is for T_{x+5} integrate_once = 1 integrated = fit.antiderivative(integrate_once)(x_fine) return np.diff(integrated, 1, axis=0)
def lx_to_nLx_weeks(lx, ruler, ruler_extended): """ Integrates a fitted lx curve to compute nLx, which would be used to compute survivorship downstream. Args: lx (np.array): lx with extrapolated values beyond fhs terminal age group start. ruler (Ruler): ruler for the fhs age groups. This ruler determines #the age group starts that need to be returned2 ruler_extended (Ruler): ruler that matches lx. This ruler, along with lx, provide the points for interpolation. Returns: (np.array): nLx, in shape of (sex, age), where age corresponds to fhs age group ids. """ assert has_dims(lx, "sex age") LOGGER.debug("lx dims {}".format(lx.shape)) # We need 2 extra data points beyond the terminal age group start to # compute its survivorship: terminal_age_start + 1 week, and 110 years. x_all_weeks = np.concatenate( [ruler.x_ww, [ruler.x_ww[-1] + 1, ruler_extended.x_ww[-1]]]) # all in weeks integrated_lx = list() for sex in [0, 1]: # first interpolate lx over the "extended" fhs age groups (in weeks) fit = PchipInterpolator(ruler_extended.x_gw, lx[sex], extrapolate=True) # now integrate lx and evaluate over all weeks integrate_once = 1 integrated = fit.antiderivative(integrate_once)(x_all_weeks) LOGGER.debug("integrated lx end {}".format(integrated[-5:])) traceplot("integrated_lx", x_all_weeks, integrated) integrated_lx.append(integrated) lx_int_sexed = np.vstack(integrated_lx) # Will be one less than number of weeks return np.diff(lx_int_sexed, 1, axis=1)
class convergeMe: """Adsorption cycle simulation and fast optimization This routine effectively performs optimization of the dwell times in the adsorption and desorption steps to maximize cooling capacity. Each cycle in an adsorber includes: Desorption, decompression, adsorption, compression. Compared to Gupta's original solution method, this solution: * Adds dwell times for compression and decompression. These are significant fractions of total cycle time and should not be neglected. Computing them does not require integration. * Integrates the ODE to find time given temperature, instead of finding temperature given a time interval. Temperature is a more universal input than cycle time. This applies only for this equilibrium model. The optimization is a rough, first pass, intended to visualize the solution space and the relations: * between independent sets of internal control parameters, namely {adsorber bed temperature limits} vs. {dwell times}, and * between internal control parameters {dwell times} and outputs {Q, COP}. Numerically, the optimization uses a brute force comparison over the search space which is a grid of points (q_low, q_high). The dwell times for adsorption and desorption steps are interpolated from the discrete curves of adsorbed ratio vs time, precomputed for those processes. Then, the compression and decompression dwell times are computed at each point, as well as the resulting time-averaged heat flow rates and COP. """ def __init__(self, ch, q_min=None, q_max=None, refine=False): t_cond = ch.ctrl.t_cond t_evap = ch.ctrl.t_evap t_cool = ch.ctrl.t_cool t_exhaust = ch.ctrl.t_exhaust # The extreme ranges depend on the heat and cold sources. margin_up = 1e-3 margin_down = 1e-5 if q_min == None: self.q_min = ch.f.Q(t_cond, t_exhaust) + margin_down else: self.q_min = q_min if q_max == None: self.q_max = ch.f.Q(t_evap, t_cool) - margin_up else: self.q_max = q_max self.q_range = linspace(self.q_min, self.q_max) # We should start desorption at q_max. T_d0 = ch.f.T(t_cond, self.q_max) T_d1 = ch.f.T(t_cond, self.q_min) T_a0 = ch.f.T(t_evap, self.q_min) T_a1 = ch.f.T(t_evap, self.q_max) # Collect data for (all) desorption processes T_d = linspace(T_d0, T_d1) [self.t_d, self.q_d] = ch.desorptionFlip(T_d) # Collect data for (all) adsorption processes T_a = linspace(T_a0, T_a1) [self.t_a, self.q_a] = ch.adsorptionFlip(T_a) # Collect data for compression and decompression self.deltat_cmp = ch.compress(self.q_range) self.deltat_dec = ch.decompress(self.q_range) # Spline it. # Time functions self.ppd = PchipInterpolator(self.q_d[::-1], self.t_d[::-1]) self.ppa = PchipInterpolator(self.q_a, self.t_a) # Compression / decompression is time delta only self.pp_comp = PchipInterpolator(self.q_range, self.deltat_cmp) self.pp_decomp = PchipInterpolator(self.q_range, self.deltat_dec) # Temperature functions self.pp_Td = PchipInterpolator(self.q_d[::-1], T_d[::-1]) self.pp_Ta = PchipInterpolator(self.q_a, T_a) # q as function of T, required to integrate for heat input self.pp_q_of_T = PchipInterpolator(T_d, self.q_d) self.pp_qintegral = self.pp_q_of_T.antiderivative() def initPlots(self): # Stuff to display for user td_splined = self.ppd(self.q_range) ta_splined = self.ppa(self.q_range) figure(1) plot(self.t_d, self.q_d, 'ro', label='Desorb') plot(self.t_a, self.q_a, 'bs', label='Adsorb') plot(self.deltat_cmp, self.q_range, 'pink', label='Compress') plot(self.deltat_dec, self.q_range, 'g', label='Decompress') plot(td_splined, self.q_range, 'r') plot(ta_splined, self.q_range, 'b') xlabel('Integration time, $t$ (s)') ylabel('Adsorbed mass ratio, $q$ (kg/kg)') title('$T_{{\\rm exhaust}}$ = {:g} K'.format(t_exhaust)) plt.legend() def parametric(self, ch, refine=False): """Now do a 'parametric study' over (q_low, q_high) space. For each (q_low,q_high), determine the time needed in each step, and the other outputs of the system. Then, find the index that gives the maximum cooling capacity, and return properties at that index. """ if True: Ni = 200 Nj = 250 self.q_lows = linspace(self.q_min + 0.0001, self.q_max - 0.001, Ni) self.q_highs = linspace(self.q_min + 0.0001, self.q_max - 0.001, Nj) #self.q_lows = linspace(0.02, 0.05, Ni) #self.q_highs = linspace(0.07, 0.11, Nj) else: self.q_lows = self.q_d[::2] self.q_highs = self.q_a[::2] Ni = len(self.q_lows) Nj = len(self.q_highs) self.t_desorb = zeros((Ni, Nj)) self.t_decomp = zeros((Ni, Nj)) self.t_adsorb = zeros((Ni, Nj)) self.t_compress = zeros((Ni, Nj)) self.t_cycle = zeros((Ni, Nj)) self.q_ref = zeros((Ni, Nj)) self.q_in = zeros((Ni, Nj)) self.cop = zeros((Ni, Nj)) mask1 = zeros((Ni, Nj)) masknan = zeros((Ni, Nj)) masknan.fill(nan) # X is low, Y is high [X, Y] = meshgrid(self.q_lows, self.q_highs, indexing='ij') for i, q_low in enumerate(self.q_lows): self.t_desorb[i, :] += self.ppd(q_low) self.t_decomp[i, :] = self.pp_decomp(q_low) self.t_adsorb[i, :] += -self.ppa(q_low) for j, q_high in enumerate(self.q_highs): self.t_desorb[:, j] += -self.ppd(q_high) self.t_adsorb[:, j] += self.ppa(q_high) self.t_compress[:, j] = self.pp_comp(q_high) self.t_cycle = self.t_desorb + self.t_compress + self.t_adsorb + self.t_decomp #self.q_ref[i,j], self.q_in[i,j], self.cop[i,j], _ = ch.afterSolve(q_low,q_high,self.t_cycle[i,j]) # End of desorption: P_cond, q_low T_d1 = self.pp_Td(X) # Skip decompression ... # End of adsorption: P_evap, q_high T_a1 = self.pp_Ta(Y) # End of compression: P_cond, q_high T_c1 = self.pp_Td(Y) m_ref = ch.spec.N_beds * (Y - X) * ch.spec.m2 / self.t_cycle DeltaH = WaterTQ2H(ch.ctrl.t_evap, 1) - WaterTQ2H(ch.ctrl.t_cond, 0) self.q_ref = m_ref * DeltaH # Changed: the Y and T_ terms require an integral. dead_mass_capacity = (ch.spec.m2 * ch.spec.c2 + ch.spec.m1 * ch.spec.c1) Q_dot_in_compress = (ch.spec.N_beds / self.t_cycle) \ * dead_mass_capacity * (T_c1 - T_a1) Q_dot_in_desorb = (m_ref * ch.spec.hads) \ + (ch.spec.N_beds / self.t_cycle) \ * (dead_mass_capacity * (T_d1 - T_c1) \ + ch.spec.m2 * ch.spec.cw * (self.pp_qintegral(T_d1) - self.pp_qintegral(T_c1))) Q_dot_in = Q_dot_in_compress + Q_dot_in_desorb self.q_in = Q_dot_in self.cop = self.q_ref / self.q_in # Do not use this equation, it is wrong: #t_cycle = t_desorb + t_adsorb; # Desorb step fraction of the total time t_fractiond = self.t_desorb / self.t_cycle self.mask = logical_and(self.t_desorb >= 0, self.t_adsorb >= 0) mask1[self.mask] = 1 masknan[self.mask] = 1 # This gives a quick guess at the maximum Iflat = (self.q_ref * mask1).argmax() I1, I2 = unravel_index(Iflat, self.q_ref.shape) t_d_opt = self.t_desorb[I1, I2] t_e_opt = self.t_decomp[I1, I2] t_a_opt = self.t_adsorb[I1, I2] t_c_opt = self.t_compress[I1, I2] Q_max = self.q_ref[I1, I2] COP_max = self.cop[I1, I2] t_total = self.t_cycle[I1, I2] q_low = self.q_lows[I1 - 1] q_high = self.q_highs[I2 + 1] if refine: from scipy.interpolate import RectBivariateSpline from scipy.optimize import minimize spline = RectBivariateSpline(self.q_lows, self.q_highs, self.q_ref) def fun(x): return -spline(x[0], x[1]) opt = minimize(fun, array([[q_low, q_high]])) q_low, q_high = opt.x Q_max = -opt.fun t_d_opt = self.ppd(q_low) - self.ppd(q_high) t_e_opt = self.pp_decomp(q_low) t_a_opt = self.ppa(q_high) - self.ppa(q_low) t_c_opt = self.pp_comp(q_high) t_total = t_d_opt + t_e_opt + t_a_opt + t_c_opt T_d1_opt = self.pp_Td(q_high) T_a1_opt = self.pp_Ta(q_low) m_ref_opt = 2 * (q_high - q_low) * ch.spec.m2 / t_total q_in_opt = (m_ref_opt * ch.spec.hads) \ + 2 * (ch.spec.m2 * (ch.spec.c2 + q_high * ch.spec.cw) + ch.spec.m1 * ch.spec.c1) * (T_d1_opt - T_a1_opt) / (t_total) COP_max = Q_max / q_in_opt if False: figure(2) if not refine: plt.cla() #CS = plt.contourf(X,Y,self.t_cycle) #clabel(CS, inline=1, fontsize=10) plt.pcolormesh(X, Y, self.t_cycle * masknan, vmin=0, vmax=self.t_cycle.max()) xlabel('Low mass ratio, $q_{low}$ (kg/kg)') ylabel('High mass ratio, $q_{high}$ (kg/kg)') title('Cycle time, $t_{cycle}$ (s)') fig = figure(3) ax = fig.gca() if not refine: ax.cla() #CS = plt.contourf(X,Y,t_fractiond*masknan) #clabel(CS, inline=1, fontsize=10) plt.pcolormesh(X, Y, masknan * t_fractiond, vmin=0, vmax=1) xlabel('Low mass ratio, $q_{low}$ (kg/kg)') ylabel('High mass ratio, $q_{high}$ (kg/kg)') title('Desorption step fraction, $t_{des}/t_{cycle}$') fig = figure(4) if not refine: plt.clf() """ax = fig.add_subplot(111, projection='3d') ax.grid(True) ax.plot_wireframe(self.t_desorb*masknan,self.t_adsorb*masknan,X,color='b') ax.plot_wireframe(self.t_desorb*masknan,self.t_adsorb*masknan,Y,color='r') #plot3([t_d_opt t_d_opt], [t_a_opt t_a_opt], [X(I1,I2) Y(I1,I2)], 'ko-') #set(gca,'xlim',[0, max(t_desorb(:))]) #set(gca,'ylim',[0, max(t_adsorb(:))]) xlabel('Desorb step time, $\Delta t$ (s)') ylabel('Adsorb step time, $\Delta t$ (s)') ax.set_zlabel('Mass ratio, $q$ (kg/kg)')""" ax = fig.add_subplot(111) ax.pcolormesh(X, Y, mask1 * self.q_ref) xlabel('Low mass ratio, $q_{low}$ (kg/kg)') ylabel('High mass ratio, $q_{high}$ (kg/kg)') title('Cooling capacity, $Q_{cool}$ (kW)') fig = figure(5) if not refine: plt.clf() if False: ax = fig.add_subplot(111, projection='3d') ax.grid(True) ax.plot_wireframe(self.t_desorb * masknan, self.t_adsorb * masknan, self.q_ref * masknan, color='b') ax.plot([t_d_opt, t_d_opt], [t_a_opt, t_a_opt], [0, Q_max], 'ko-') ax.set_zlabel('Cooling capacity, $Q_{cool}$ (kW)') else: #ax.plot_surface(self.t_desorb*masknan, self.t_adsorb*masknan, self.q_ref*masknan) ax = fig.add_subplot(111) #ax.contour(self.t_desorb*masknan, self.t_adsorb*masknan, self.q_ref*masknan) #ax.tripcolor((self.t_desorb*masknan).flat, (self.t_adsorb*masknan).flat, (self.q_ref*masknan).flat) # Uncomment this if you want tcf = ax.tricontourf(self.t_desorb[self.mask], self.t_adsorb[self.mask], self.q_ref[self.mask]) if not refine: fig.colorbar(tcf) ax.set_xlim([0, self.t_desorb.max()]) ax.set_ylim([0, self.t_adsorb.max()]) ax.set_title('Cooling capacity, $Q_{cool}$ (kW)') ax.set_xlabel('Desorb step time, $\Delta t$ (s)') ax.set_ylabel('Adsorb step time, $\Delta t$ (s)') t_opts = (t_d_opt, t_e_opt, t_a_opt, t_c_opt) return t_opts, Q_max, COP_max, t_total, q_low, q_high