diff --git a/Battery/EquivalentCircuitModel/calibration/FittingEIS.m b/Battery/EquivalentCircuitModel/calibration/FittingEIS.m new file mode 100644 index 000000000..c1812db7a --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/FittingEIS.m @@ -0,0 +1,587 @@ +classdef FittingEIS + + properties + params0 + Z_re_exp + Z_im_exp + omega + scales + + % best_params_found = [] + % fitting_error = [] + + end + + + + methods + + + function feis = FittingEIS(params0, scales, Z_re_exp, Z_im_exp, omega) + + % feis.params0 = params0; + % feis.scales = scales; + % feis.Z_re_exp = Z_re_exp; + % feis.Z_im_exp = Z_im_exp; + % feis.omega = omega; + + + if istable(Z_re_exp), Z_re_exp = table2array(Z_re_exp); end + if istable(Z_im_exp), Z_im_exp = table2array(Z_im_exp); end + if istable(omega), omega = table2array(omega); end + if istable(params0), params0 = table2array(params0); end + if istable(scales), scales = table2array(scales); end + + % Everything has to be "double" and colon (:) + feis.params0 = double(params0(:)); + feis.scales = double(scales(:)); + feis.Z_re_exp= double(Z_re_exp(:)); + feis.Z_im_exp= double(Z_im_exp(:)); + feis.omega = double(omega(:)); + + %pb here because the way I take exp data changes a lot the + %result... + + end + + function p_norm = unscaled2scaled(feis, p) + pmin = feis.scales(1:5); + pmax = feis.scales(6:10); + p_norm = (p-pmin)./(pmax-pmin); + end + + function p = scaled2unscaled(feis, p_norm) + pmin = feis.scales(1:5); + pmax = feis.scales(6:10); + p = (pmax-pmin).*p_norm +pmin; + end + + %% Thevenin Model + + function [min_value, history, best_params, fitting_error] = optimizationBFGS(feis) + + + f_opt = @(p_norm) feis.optifunc(scaled2unscaled(feis, p_norm)); + + params0_norm = unscaled2scaled(feis, feis.params0); + % best_params_norm = lsqnonlin(deltagap, params0_norm, lb_norm, ub_norm, [0,0,-1,0,10], 0); + params0_norm = params0_norm(:); + + pmin = feis.scales(1:5); + pmax = feis.scales(6:10); + p03min = pmin(3); + p03max = pmax(3); + p05min = pmin(5); + p05max = pmax(5); + + A_custom = [0, 0, -1, 0, 2*(p05max - p05min)/(p03max- p03min)]; + b_custom = (p03min-2*p05min)/(p03max - p03min); + + max_iter = 300; + tol_obj = 1e-15; + tol_grad = 1e-5; + + [min_value, best_params_norm, history] = unitBoxBFGS(... + params0_norm, ... + f_opt, ... + 'maximize', false, ... + 'linIneq', struct('A', A_custom, 'b', b_custom), ... %A*u<=b + 'enforceFeasible', true, ... + 'maxIt', max_iter, ... + 'objChangeTol', tol_obj, ... + 'gradTol', tol_grad, ... + 'lineSearchMaxIt', 100 ... + ); + best_params = scaled2unscaled(feis, best_params_norm(:) ); + + % explications of why it stopped + fitting_error = feis.optifunc(best_params); + + it_count = length(history.val) - 1; + final_pg = history.pg(end); + + % Cost function variation + if it_count >= 1 + delta_v = abs(history.val(end) - history.val(end-1)); + else + delta_v = inf; + end + + if it_count >= max_iter + fprintf('Stopped because %d iterations reached.\n', max_iter); + elseif final_pg < tol_grad + fprintf('Stopped because reached gradTol = %e.\n', tol_grad); + elseif delta_v < tol_obj + fprintf('Stopped because reached objChangeTol = %e.\n', tol_obj); + else + fprintf('Unexpected error or relative tolerance reached.\n'); + end + + + end + + + + function [v, g_norm] = optifunc(feis, p) + + % Preventing physical values to get too close from 0 + p_safe = max(p, 1e-8); + + [Z_re_param, Z_im_param] = load_nyquist(p_safe, feis.omega); + + + if istable(Z_re_param), Z_re_param = table2array(Z_re_param); end + if istable(Z_im_param), Z_im_param = table2array(Z_im_param); end + + + Z_re_param = double(Z_re_param(:)); + Z_im_param = double(Z_im_param(:)); + + + if ~isnumeric(Z_re_param) || ~isreal(Z_re_param) || any(isnan(Z_re_param(:))) + v = 1e10; + g_norm = zeros(5,1); + return; + end + + Modulus_Z = sqrt(feis.Z_re_exp.^2 + feis.Z_im_exp.^2); + Modulus_Z = Modulus_Z(:); + + err_re = (feis.Z_re_exp(:) - Z_re_param(:)) ./ Modulus_Z(:); + err_im = (feis.Z_im_exp(:) - Z_im_param(:)) ./ Modulus_Z(:); + + v = sum(err_re.^2 + err_im.^2); + + + if nargout > 1 + + w = feis.omega(:); + R0 = p_safe(1); + R1 = p_safe(2); + C1 = p_safe(3); + R2 = p_safe(4); + C2 = p_safe(5); + + g_re_dR0 = -ones(size(w)); + g_re_dR1 = -(1-(R1*C1.*w).^2) ./ (1+(R1*C1.*w).^2).^2; + g_re_dC1 = (2*C1*R1^3.*w.^2) ./ (1+(R1*C1.*w).^2).^2; + g_re_dR2 = -(1-(R2*C2.*w).^2) ./ (1+(R2*C2.*w).^2).^2; + g_re_dC2 = (2*C2*R2^3.*w.^2) ./ (1+(R2*C2.*w).^2).^2; + + J_re = [g_re_dR0, g_re_dR1, g_re_dC1, g_re_dR2, g_re_dC2]; + + g_im_dR0 = zeros(size(w)); + g_im_dR1 = (2*R1*C1.*w) ./ (1+(R1*C1.*w).^2).^2; + g_im_dC1 = (R1^2.*w - C1^2*R1^4.*w.^3) ./ (1+(R1*C1.*w).^2).^2; + g_im_dR2 = (2*R2*C2.*w) ./ (1+(R2*C2.*w).^2).^2; + g_im_dC2 = (R2^2.*w - C2^2*R2^4.*w.^3) ./ (1+(R2*C2.*w).^2).^2; + + J_im = [g_im_dR0, g_im_dR1, g_im_dC1, g_im_dR2, g_im_dC2]; + derr_re_dp = J_re ./ Modulus_Z; + derr_im_dp = J_im ./ Modulus_Z; + + g_true = 2 * (derr_re_dp' * err_re) + 2 * (derr_im_dp' * err_im); + % Chain rule : + pmin = feis.scales(1:5); + pmax = feis.scales(6:10); + dp_dpnorm = (pmax - pmin); % Dérivée de p par rapport à p_norm + g_norm = g_true .* dp_dpnorm; + + + end + end + + % finite difference method + % if nargout > 1 + % g_norm = zeros(5,1); + % dp_norm = 1e-6; + % + % for i = 1:5 + % p_perturb = p; + % p_perturb(i) = p_perturb(i) + dp_norm * feis.scales(i); + % + % [Z_re_pert, Z_im_pert] = load_nyquist(p_perturb, feis.omega); + % + % err_re_pert = (feis.Z_re_exp(:) - double(Z_re_pert(:))) ./ Modulus_Z; + % err_im_pert = (feis.Z_im_exp(:) - double(Z_im_pert(:))) ./ Modulus_Z; + % + % v_perturb = sum(err_re_pert.^2 + err_im_pert.^2); + % + % g_norm = (v_perturb - v) / dp_norm; + % end + % end + + +%% Warburg Model + + function [min_value, history, best_params, fitting_error] = optimizationBFGS_warburg(feis) + + f_opt = @(p_norm) feis.optifunc_warburg(p_norm.* feis.scales) ; + + params0_norm = feis.params0 ./ feis.scales; + params0_norm = params0_norm(:); + + + + [min_value, best_params_norm, history] = unitBoxBFGS(... + params0_norm, ... + f_opt, ... + 'maximize', false, ... + 'enforceFeasible', true, ... + 'maxIt', 300, ... + 'objChangeTol', 1e-6, ... + 'gradTol', 1e-5, ... + 'lineSearchMaxIt', 200 ... + ); + best_params = best_params_norm(:) .* feis.scales(:); + + fitting_error = feis.optifunc_warburg(best_params); + + end + + function [v, g_norm] = optifunc_warburg(feis, p) + + % Preventing physical values to get too close from 0 + p_safe = max(p, 1e-7); + + + w = feis.omega(:); + R0 = p_safe(1); + R1 = p_safe(2); + Q1 = p_safe(3); + a1 = p_safe(4); + R2 = p_safe(5); + Q2 = p_safe(6); + a2 = p_safe(7); + Q = p_safe(8); + L = p_safe(9); + + s = 1i .* w; + s = s(:); + + sa1 = s.^a1; + sa2 = s.^a2; + + Z_param_cplx = R0 + s.*L + R1 ./ (1 + R1.*Q1.*sa1) + R2 ./ (1 + R2.*Q2.*sa2) + 1 ./ (Q.*(s.^0.5)); + + Z_re_param = real(Z_param_cplx); + Z_im_param = imag(Z_param_cplx); + + if istable(Z_re_param), Z_re_param = table2array(Z_re_param); end + if istable(Z_im_param), Z_im_param = table2array(Z_im_param); end + + + Z_re_param = double(Z_re_param(:)); + Z_im_param = double(Z_im_param(:)); + + Modulus_Z = sqrt(Z_re_param.^2 + Z_im_param.^2); + Modulus_Z = Modulus_Z(:); + + err_re = (feis.Z_re_exp(:) - Z_re_param(:)) ./ Modulus_Z(:); + err_im = (feis.Z_im_exp(:) - Z_im_param(:)) ./ Modulus_Z(:); + + v = sum(err_re.^2 + err_im.^2); + + + if nargout > 1 + + den1 = (1 + R1 .* Q1 .* sa1).^2; + den2 = (1 + R2 .* Q2 .* sa2).^2; + + + dZ_dR0 = ones(size(s)); % R0 + dZ_dR1 = 1 ./ den1; % R1 + dZ_dR2 = 1 ./ den2; % R2 + + dZ_dQ1 = - (R1^2 .* sa1) ./ den1; % Q1 + dZ_dQ2 = - (R2^2 .* sa2) ./ den2; % Q2 + + dZ_da1 = - (Q1 * R1^2 .* sa1 .* log(s)) ./ den1; % a1 + dZ_da2 = - (Q2 * R2^2 .* sa2 .* log(s)) ./ den2; % a2 + + dZ_dQ = - 1 ./ (Q^2 .* (s.^0.5)); % Q + dZ_dL = s; % L + + J = [dZ_dR0, dZ_dR1, dZ_dQ1, dZ_da1, dZ_dR2, dZ_dQ2, dZ_da2, dZ_dQ, dZ_dL]; + + J_im = imag(J); + J_re = real(J); + + derr_re_dp = -J_re ./ Modulus_Z; + derr_im_dp = -J_im ./ Modulus_Z; + + g_true = 2 * (derr_re_dp' * err_re) + 2 * (derr_im_dp' * err_im); + g_norm = g_true .* feis.scales(:); + + + if any(isinf(g_norm)) || any(isnan(g_norm)) || isinf(v) || isnan(v) v = 1e10; + g_norm = zeros(9,1); + return; + end + end + end + + + + %% lsq method + + function residuals = optifunc_lsq(feis, p) + % 1. On empêche physiquement les paramètres de valoir zéro + p = max(p, 1e-8); + + % 2. Chargement du modèle + [Z_re_param, Z_im_param] = load_nyquist(p, feis.omega); + if istable(Z_re_param), Z_re_param = table2array(Z_re_param); end + if istable(Z_im_param), Z_im_param = table2array(Z_im_param); end + + Z_re_param = double(Z_re_param(:)); + Z_im_param = double(Z_im_param(:)); + + % Si le modèle renvoie du NaN (physiquement impossible), on pénalise + if ~isnumeric(Z_re_param) || ~isreal(Z_re_param) || any(isnan(Z_re_param(:))) + residuals = 1e10 * ones(2 * length(feis.omega), 1); + return; + end + + % 3. Calcul du module pour l'erreur relative + Modulus_Z = sqrt(feis.Z_re_exp.^2 + feis.Z_im_exp.^2); + Modulus_Z = Modulus_Z(:); + + % 4. Vecteurs de résidus (Non mis au carré, lsqnonlin s'en charge) + err_re = (feis.Z_re_exp(:) - Z_re_param(:)) ./ Modulus_Z; + err_im = (feis.Z_im_exp(:) - Z_im_param(:)) ./ Modulus_Z; + + % 5. On empile les parties réelles et imaginaires en un seul vecteur colonne + residuals = [err_re; err_im]; + end + + function [min_value, best_params, resnorm] = optimizationLsqnonlin(feis) + % Fonction objectif : renvoie le vecteur de résidus + f_res = @(p_norm) feis.optifunc_lsq(p_norm(:) .* feis.scales(:)); + + % Paramètres initiaux normés + params0_norm = feis.params0(:) ./ feis.scales(:); + + % Bornes (Limites inférieures strictes > 0 pour éviter les crashs) + lb_norm = ones(5,1) * 1e-5; + ub_norm = ones(5,1) * 1e5./ feis.scales(:); + + % Options de lsqnonlin + options = optimoptions('lsqnonlin', ... + 'Display', 'iter-detailed', ... % Affichage très détaillé + 'Algorithm', 'trust-region-reflective', ... % Parfait pour l'EIS + 'MaxFunctionEvaluations', 3000, ... + 'MaxIterations', 500, ... + 'StepTolerance', 1e-8, ... + 'FunctionTolerance', 1e-6); + + % Lancement de l'optimiseur natif MATLAB + [best_params_norm, resnorm, ~, exitflag, output] = lsqnonlin(... + f_res, params0_norm, lb_norm, ub_norm, options); + + % Récupération des vraies valeurs (dénormalisation) + best_params = best_params_norm(:) .* feis.scales(:); + min_value = resnorm; % Erreur finale (somme des carrés des résidus) + + % Affichage du statut de fin + disp('=== RAISON DE FIN DE LSQNONLIN ==='); + disp(output.message); + end + +%% lsq method warburg + function v = optifunclsq_warburg(feis, p) + p_safe = max(p, 1e-15); + + R0 = p_safe(1); R1 = p_safe(2); Q1 = p_safe(3); a1 = p_safe(4); + R2 = p_safe(5); Q2 = p_safe(6); a2 = p_safe(7); Q = p_safe(8); L = p_safe(9); + + w = feis.omega(:); + s = 1i .* w; + + sa1 = s.^a1; + sa2 = s.^a2; + Z_param_cplx = R0 + s.*L + R1 ./ (1 + R1.*Q1.*sa1) + R2 ./ (1 + R2.*Q2.*sa2) + 1 ./ (Q.*(s.^0.5)); + + Z_re_param = real(Z_param_cplx); + Z_im_param = imag(Z_param_cplx); + + if any(isnan(Z_re_param)) || any(isnan(Z_im_param)) || any(isinf(Z_re_param)) || any(isinf(Z_im_param)) + v = 1e6; % Pénalité forte mais gérable + return; + end + + Modulus_Z = sqrt(Z_re_param.^2 + Z_im_param.^2); + Modulus_Z = Modulus_Z(:); + + err_re = (feis.Z_re_exp(:) - Z_re_param) ./ Modulus_Z; + err_im = (feis.Z_im_exp(:) - Z_im_param) ./ Modulus_Z; + + v = sum(err_re.^2 + err_im.^2); + + end + + + function [min_value, history, best_params, fitting_error] = optimizationlsq_warburg(feis) + f_opt = @(p_norm) feis.optifunclsq_warburg(p_norm .* feis.scales); + + params0_norm = feis.params0 ./ feis.scales; + params0_norm = params0_norm(:); + + if length(params0_norm) ~= 9 + error('feis.params0 should have 9 parameters but it has %d).', length(params0_norm)); + end + + lb_norm = [1e-10; 1e-10; 1e-10; 1e-3; 1e-10; 1e-10; 0.01; 1e-5; 1e-15] ./ feis.scales; + ub_norm = [1e5; 1e5; 1e6; 1.0; 1e5; 1e6; 1.0; 1e15; 1e-2 ] ./ feis.scales; + + options = optimoptions('fmincon', ... + 'Display', 'iter', ... + 'Algorithm', 'sqp', ... % Très bon pour ce type de fitting + 'SpecifyObjectiveGradient', false, ... + 'MaxFunctionEvaluations', 10000, ... + 'MaxIterations', 1000); + + [best_params_norm, min_value] = fmincon(f_opt, params0_norm, [], [], [], [], lb_norm, ub_norm, [], options); + + history = []; % Si vous n'en avez pas besoin + best_params = best_params_norm(:) .* feis.scales(:); + fitting_error = feis.optifunclsq_warburg(best_params); + end + + %% showing the results + + function plotresults_thevenin(feis, best_params, fitting_error) + + [Z_re_fit, Z_im_fit] = load_nyquist(best_params, feis.omega); + + figure; + subplot(3,1,1); + semilogx(feis.omega, feis.Z_re_exp, 'r', 'MarkerFaceColor', 'r'); + hold on; + semilogx(feis.omega, Z_re_fit, 'b'); + legend('experience', 'fitted model'); + title('Fitting Real Impedance'); + xlabel('Omega'); + ylabel('Z_{re} '); + + subplot(3,1,2); + semilogx(feis.omega, -feis.Z_im_exp, 'r', 'MarkerFaceColor', 'r'); + hold on; + semilogx(feis.omega, -Z_im_fit, 'b'); + legend('experience', 'fitted model'); + title('Fitting Imaginary Impedance'); + xlabel('Omega'); + ylabel('-Z_{im} '); + + subplot(3,1,3); + plot(feis.Z_re_exp, -feis.Z_im_exp, 'r', 'MarkerFaceColor', 'r'); + hold on; + plot(feis.Z_re_exp, -Z_im_fit, 'b'); + legend('experience', 'fitted model'); + title('Nyquist Diagram'); + xlabel('Z_{re}'); + ylabel('-Z_{im} '); + axis equal; + + grid on; + + text_error = sprintf('Fitting error : %.2e', fitting_error); + + subplot(3,1,3); + + text(0.05, 0.90, text_error, 'Units', 'normalized', ... + 'BackgroundColor', 'white', ... + 'EdgeColor', 'black', ... + 'FontSize', 11, ... + 'FontWeight', 'bold'); + end + + + function plotresults_warburg(feis, best_params, fitting_error) + + % [Z_re_fit, Z_im_fit] = load_nyquist(best_params, feis.omega); + w = feis.omega(:); + R0 = best_params(1); + R1 = best_params(2); + Q1 = best_params(3); + a1 = best_params(4); + R2 = best_params(5); + Q2 = best_params(6); + a2 = best_params(7); + Q = best_params(8); + L = best_params(9); + s = 1i .* w; + s = s(:); + + sa1 = s.^a1; + sa2 = s.^a2; + + Z_param_cplx = R0 + s.*L + R1 ./ (1 + R1.*Q1.*sa1) + R2 ./ (1 + R2.*Q2.*sa2) + 1 ./ (Q.*(s.^0.5)); + + Z_re_fit = real(Z_param_cplx); + Z_im_fit = imag(Z_param_cplx); + + + figure; + subplot(3,1,1); + semilogx(feis.omega, feis.Z_re_exp, 'r', 'MarkerFaceColor', 'r'); + hold on; + semilogx(feis.omega, Z_re_fit, 'b'); + legend('experience', 'fitted model'); + title('Fitting results'); + xlabel('Omega'); + ylabel('Z_{re} '); + + subplot(3,1,2); + semilogx(feis.omega, -feis.Z_im_exp, 'r', 'MarkerFaceColor', 'r'); + hold on; + semilogx(feis.omega, -Z_im_fit, 'b'); + legend('experience', 'fitted model'); + title('Fitting results'); + xlabel('Omega'); + ylabel('-Z_{im} '); + + subplot(3,1,3); + plot(feis.Z_re_exp, -feis.Z_im_exp, 'r', 'MarkerFaceColor', 'r'); + hold on; + plot(Z_re_fit, -Z_im_fit, 'b'); + legend('experience', 'fitted model'); + title('Nyquist'); + xlabel('Z_{re}'); + ylabel('-Z_{im} '); + axis equal; + + grid on; + text_error = sprintf('Fitting error : %.2e', fitting_error); + text(0.05, 0.85, text_error, 'Units', 'normalized', ... + 'BackgroundColor', 'white', ... + 'EdgeColor', 'black', ... + 'FontSize', 11, ... + 'FontWeight', 'bold'); + end + + function printResults(feis, best_params, fitting_error) + + fprintf('\n=== FITTING SCORE ===\n'); + fprintf('Error : %e\n', fitting_error); + + fprintf('\n=== PARAMETERS FOUND ===\n'); + fprintf('R0 = %.2e Ohms\n', best_params(1)); + fprintf('R1 = %.2e Ohms\n', best_params(2)); + fprintf('C1 = %.2e Farads\n', best_params(3)); + fprintf('R2 = %.2e Ohms\n', best_params(4)); + fprintf('C2 = %.2e Farads\n', best_params(5)); + + % fprintf('\n=== PARAMETERS FOUND ===\n'); + % fprintf('R0 = %.4e Ohms\n', best_params(1)); + % fprintf('R1 = %.4e Ohms\n', best_params(2)); + % fprintf('Q1 = %.4e s^a/Ohm\n', best_params(3)); + % fprintf('a1 = %.4f \n', best_params(4)); % %f suffit pour 'a' car il est entre 0 et 1 + % fprintf('R2 = %.4e Ohms\n', best_params(5)); + % fprintf('Q2 = %.4e s^a/Ohm\n', best_params(6)); + % fprintf('a2 = %.4f \n', best_params(7)); + % fprintf('Q = %.4e \n', best_params(8)); + % fprintf('L = %.4e Henrys\n', best_params(9)); + end + end +end diff --git a/Battery/EquivalentCircuitModel/calibration/plotting/plot_nyquist_ank.m b/Battery/EquivalentCircuitModel/calibration/plotting/plot_nyquist_ank.m new file mode 100644 index 000000000..2044245c4 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/plotting/plot_nyquist_ank.m @@ -0,0 +1,24 @@ + + + +load_experimental_data(); + +% --- 5. Tracé du diagramme de Nyquist --- +figure('Name', 'EIS', 'Color', 'w'); + +% On trace -Im(Z) en fonction de Re(Z) +plot(Z_real, Z_imag, 'o', ... + 'LineWidth', 1.5, ... + 'MarkerSize', 2, ... + 'MarkerFaceColor', [0 0.4470 0.7410], ... + 'MarkerEdgeColor', 'k'); +grid on; +axis equal; % Force la même échelle en X et Y + +% Ajout des labels +xlabel('Z_{re} ', 'FontSize', 12, 'FontWeight', 'bold'); +ylabel('-Z_{im}', 'FontSize', 12, 'FontWeight', 'bold'); +title('Nyquist diagram from data', 'FontSize', 14); + +% Amélioration des axes +set(gca, 'FontSize', 11, 'LineWidth', 1); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/plotting/plot_nyquist_params.m b/Battery/EquivalentCircuitModel/calibration/plotting/plot_nyquist_params.m new file mode 100644 index 000000000..2afa33f60 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/plotting/plot_nyquist_params.m @@ -0,0 +1,26 @@ + +% params = [6.2261e-3, 0.3e-3, 10000.1264, 0.00097, 2e5]; % best params +% handmade + +params = [0.003, 0.00134, 5, 0.00330, 20000]; +[Z_re_exp, Z_im_exp, omega] = load_experimental_data(); +omega = logspace(-2, 4, 100); + +[Z_real, Z_imag] = load_nyquist(params, omega) + +figure; +plot(Z_re_exp, Z_im_exp, 'ro', 'MarkerFaceColor', 'r'); +hold on; +plot(Z_real, Z_imag, '-o', 'LineWidth', 1.5, 'MarkerFaceColor', 'b'); +axis equal; % Essentiel pour Nyquist +grid on; +xlabel('Z_{réel} (\Omega)', 'FontWeight', 'bold'); +ylabel('-Z_{imaginaire} (\Omega)', 'FontWeight', 'bold'); +title('Nyquist diagram simulated'); + + + + + + + diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/activationEnergyOfReaction_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/activationEnergyOfReaction_impact.m new file mode 100644 index 000000000..6ff436588 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/activationEnergyOfReaction_impact.m @@ -0,0 +1,48 @@ + +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); + +b = linspace(1e4, 5e4, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.ActiveMaterial.Interface.activationEnergyOfReaction = b(i); %change here + inputparams.PositiveElectrode.Coating.ActiveMaterial.Interface.activationEnergyOfReaction = b(i)/2; %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('activation Energy Of Reaction = %.2e', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/bruggeman_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/bruggeman_impact.m new file mode 100644 index 000000000..1412d6644 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/bruggeman_impact.m @@ -0,0 +1,46 @@ + +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); +b = linspace(0.5,2,10); + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.bruggemanCoefficient = b(i); + inputparams.PositiveElectrode.Coating.bruggemanCoefficient = b(i); + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('Bruggeman coefficient = %.2f', b(i)); + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/doubleLayerCapacitance_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/doubleLayerCapacitance_impact.m new file mode 100644 index 000000000..df5714137 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/doubleLayerCapacitance_impact.m @@ -0,0 +1,61 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + +ne = 'NegativeElectrode'; +co = 'Coating'; +am = 'ActiveMaterial'; +itf = 'Interface'; + +includeDoubleLayer = true; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); + +b = logspace(-2, 2, 20); %change here + +frequences = logspace(-2, 4, 30); + + +figure; +hold on; +for i = 1:length(b) + + fprintf('compute impedance for doubleLayerCapacitance = %.2e ... ', b(i)); %change here + + tic + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = b(i); %change here + [model, inputparams, ~] = setupModelFromJson(jsonstruct); + + % inputparams.(ne).(co).(am).(itf).doubleLayerCapacitance = b(i); + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + + fprintf('done in %g s\n', toc); + + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('doubleLayerCapacitance = %.2e', b(i)); %change here + + plot(Z_re, -Z_im, 'DisplayName', curve); + +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/electronicConductivity_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/electronicConductivity_impact.m new file mode 100644 index 000000000..df70d8b3f --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/electronicConductivity_impact.m @@ -0,0 +1,46 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); +b = linspace(10, 1000, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.ActiveMaterial.electronicConductivity = b(i); %change here + inputparams.PositiveElectrode.Coating.ActiveMaterial.electronicConductivity = b(i); %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('Electronic Conductivity = %.2f', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/param_dependency.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/param_dependency.m new file mode 100644 index 000000000..8a44ae62d --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/param_dependency.m @@ -0,0 +1,130 @@ +function param_dependency() + mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + + % 1. Chargement des données (Conservez votre méthode de chargement ici) + jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); + jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + jsonstruct = mergeJsonStructs({jsonstruct_material, jsonstruct_geometry}); + ne = 'NegativeElectrode'; + co = 'Coating'; + am = 'ActiveMaterial'; + itf = 'Interface'; + + includeDoubleLayer = true; + + if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + + end + + + + c_ne = 29.866*mol/litre; + c_pe = 17.038*mol/litre; + + % 2. Création de l'interface graphique (Fenêtre plus haute pour accueillir les curseurs) + fig = uifigure('Name', 'Impedance Explorer', 'Position', [100, 50, 600, 650]); + + % Graphique + ax = uiaxes(fig, 'Position', [50, 350, 500, 250]); + title(ax, 'Nyquist Diagram'); + xlabel(ax, 'Re(Z)'); + ylabel(ax, '-Im(Z)'); + grid(ax, 'on'); + + % Label de statut + lbl = uilabel(fig, 'Position', [50, 310, 500, 22], 'Text', 'Ready, use slider to start.'); + lbl.FontWeight = 'bold'; + + % --- CURSEUR 1 : Diffusion Solide (Anode) --- + % Modifie le transport du Lithium dans les particules de graphite. + uilabel(fig, 'Position', [50, 260, 500, 40], 'WordWrap', 'on', ... + 'Text', '1. Anode refDiffusionCoefficient * 10^x (initial value: 1.3135e-15)'); + + sld1 = uislider(fig, 'Position', [50, 240, 500, 3], ... + 'Limits', [-5, 5], ... % Définit le minimum et le maximum + 'MajorTicks', -5:0.5:5, ... % Ajoute une graduation tous les 1 + 'Value', 0); + + % --- CURSEUR 2 : Taux de réaction (Anode) --- + % Modifie la cinétique de transfert de charge. + uilabel(fig, 'Position', [50, 170, 500, 40], 'WordWrap', 'on', ... + 'Text', '2. Anode reactionRateConstant * 10^x (initial value: 5.031e-11) '); + sld2 = uislider(fig, 'Position', [50, 150, 500, 3],... + 'Limits', [-5, 5], ... % Définit le minimum et le maximum + 'MajorTicks', -5:0.5:5, ... % Ajoute une graduation tous les 1 + 'Value', 0); + + % --- CURSEUR 3 : Taux de réaction (Cathode) --- + % Modifie la cinétique de transfert de charge. + uilabel(fig, 'Position', [50, 80, 500, 40], 'WordWrap', 'on', ... + 'Text', '3. doubleLayerCapacitance * x (initial value: 0.2)'); + sld3 = uislider(fig, 'Position', [50, 60, 500, 3], ... + 'Limits', [-5, 5], ... % Définit le minimum et le maximum + 'MajorTicks', -5:0.5:5, ... % Ajoute une graduation tous les 1 + 'Value', 0); + + % 4. Connexion aux événements + % On crée une fonction anonyme qui passe TOUS les curseurs + callback_fcn = @(src, event) updatePlot(ax, lbl, jsonstruct, c_ne, c_pe, sld1, sld2, sld3); + + sld1.ValueChangedFcn = callback_fcn; + sld2.ValueChangedFcn = callback_fcn; + sld3.ValueChangedFcn = callback_fcn; + + % Optionnel : Tracer l'état initial + % updatePlot(ax, lbl, jsonstruct, c_ne, c_pe, sld1, sld2, sld3); +end + +% --- Fonction de mise à jour --- +function updatePlot(ax, lbl, jsonstruct, c_ne, c_pe, sld1, sld2, sld3) + + % Figer l'affichage + lbl.Text = 'Processing... '; + lbl.FontColor = '#D95319'; + drawnow; + + % 1. Récupération des valeurs de base depuis votre JSON + base_diff_anode = 1.3135e-15; + base_rate_anode = 5.031e-11; + base_doubleLayerCapacitance = 0.2; + + % 2. Application des multiplicateurs (10^slider_val) + val_diff_anode = base_diff_anode * (10^(sld1.Value)); + val_rate_anode = base_rate_anode * (10^(sld2.Value)); + val_doubleLayerCapacitance = base_doubleLayerCapacitance * 10^(sld3.Value); + + % 3. Injection dans la structure JSON AVANT de construire le modèle + jsonstruct.NegativeElectrode.Coating.ActiveMaterial.SolidDiffusion.referenceDiffusionCoefficient = val_diff_anode; + jsonstruct.NegativeElectrode.Coating.ActiveMaterial.Interface.reactionRateConstant = val_rate_anode; + jsonstruct.NegativeElectrode.Coating.ActiveMaterial.Interface.doubleLayerCapacitance = val_doubleLayerCapacitance; + + try + % 4. Exécution de BattMo + [model, inputparams, ~] = setupModelFromJson(jsonstruct); + initstate = initStateChen2020(model, c_ne, c_pe); + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + frequences = logspace(-2, 3, 30); + Z = impsolv.computeImpedance(frequences); + + % 5. Tracé + plot(ax, real(Z), -imag(Z), '-o', 'LineWidth', 2, 'Color', '#0072BD'); + + % Retour à la normale + lbl.Text = 'Done'; + lbl.FontColor = '#77AC30'; + + catch ME + lbl.Text = 'Erreur lors du calcul (vérifiez la console).'; + lbl.FontColor = '#A2142F'; + disp('--- ERREUR DANS LE CALCUL ---'); + disp(ME.message); + for k = 1:length(ME.stack) + disp(['Ligne ', num2str(ME.stack(k).line), ' dans ', ME.stack(k).name]); + end + end +end \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/porosity_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/porosity_impact.m new file mode 100644 index 000000000..47d1a7cce --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/porosity_impact.m @@ -0,0 +1,45 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); +b = linspace(0.1, 0.9, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.Separator.porosity = b(i); %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('porosity = %.2f', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/reactionRateConstant_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/reactionRateConstant_impact.m new file mode 100644 index 000000000..12baa2706 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/reactionRateConstant_impact.m @@ -0,0 +1,47 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); + +b = linspace(1e-11, 1e-12, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.ActiveMaterial.Interface.reactionRateConstant = b(i); %change here + inputparams.PositiveElectrode.Coating.ActiveMaterial.Interface.reactionRateConstant = b(i)*5; %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('reactionRateConstant = %.2e', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/referenceDiffusionCoefficient_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/referenceDiffusionCoefficient_impact.m new file mode 100644 index 000000000..88aebd8d1 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/referenceDiffusionCoefficient_impact.m @@ -0,0 +1,46 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); +b = logspace(-15, -10, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.ActiveMaterial.SolidDiffusion.referenceDiffusionCoefficient = b(i); %change here + inputparams.PositiveElectrode.Coating.ActiveMaterial.SolidDiffusion.referenceDiffusionCoefficient = b(i)/10; %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('Diffusion Coefficient = %.2e', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/saturationConcentration_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/saturationConcentration_impact.m new file mode 100644 index 000000000..ac40f3e89 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/saturationConcentration_impact.m @@ -0,0 +1,47 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); + +b = linspace(3e4, 7e4, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.ActiveMaterial.Interface.saturationConcentration = b(i); %change here + inputparams.PositiveElectrode.Coating.ActiveMaterial.Interface.saturationConcentration = b(i)+2e4; %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('saturation Concentration = %.2e', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/volumefraction_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/volumefraction_impact.m new file mode 100644 index 000000000..3eff22ccb --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/volumefraction_impact.m @@ -0,0 +1,46 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); +b = linspace(0.5,0.95,10); + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.volumeFraction = b(i); + inputparams.PositiveElectrode.Coating.volumeFraction = b(i); + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('Volume Fraction = %.2f', b(i)); + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/volumetricSurfaceArea_impact.m b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/volumetricSurfaceArea_impact.m new file mode 100644 index 000000000..3e949d075 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/pxdParamsImpact/volumetricSurfaceArea_impact.m @@ -0,0 +1,47 @@ +mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + +jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); +jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + +jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + +includeDoubleLayer = false; + +if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + +end + + +[model, inputparams, ~] = setupModelFromJson(jsonstruct); + +c_ne = 29.866*mol/litre; % initial concentration at negative electrode +c_pe = 17.038*mol/litre; % initial concentration at positive electrode + +initstate = initStateChen2020(model, c_ne, c_pe); + +b = linspace(1e5, 7e5, 20); %change here + +frequences = logspace(-2, 4, 30); +figure; +hold on; +for i = 1:length(b) + + inputparams.NegativeElectrode.Coating.ActiveMaterial.Interface.volumetricSurfaceArea = b(i); %change here + inputparams.PositiveElectrode.Coating.ActiveMaterial.Interface.volumetricSurfaceArea = b(i); %change here + + impsolv = ImpedanceSolver(inputparams, 'initstate', initstate, 'computeSteadyState', false); + + Z = impsolv.computeImpedance(frequences); + Z_re = real(Z); + Z_im = imag(Z); + curve = sprintf('volumetric Surface Area = %.2e', b(i)); %change here + plot(Z_re, -Z_im, 'DisplayName', curve); +end + +legend('show'); \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/runClassFitting.m b/Battery/EquivalentCircuitModel/calibration/runClassFitting.m new file mode 100644 index 000000000..59dbedd95 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/runClassFitting.m @@ -0,0 +1,80 @@ + +%% Main code: + +% filename = 'C:\Users\Alexandre Fichter\Documents\stage_3A\contenu stage\data_August\ank_data\Supplementary material\02_Electrical_characterization\EIS\131-828_EIS_01_MB_CD8.txt'; +% filename = '/home/xavier/Matlab/Projects/battmo/Data/131-828_EIS_01_MB_CD8.txt'; +% +% [Z_re_exp, Z_im_exp, omega] = load_experimental_data(filename); +% [Z_re_exp, Z_im_exp, omega] = load_santoni_data(); +[Z_re_exp, Z_im_exp, omega] = load_chen_data(); + +% omega = logspace(-4, 2, 50); +% params = [0.05052, 1.12673, 59119.9, 0.03155, 11054.0]; +% [Z_re_exp, Z_im_exp] = load_nyquist(params, omega); + +params0 = [0.05052, 1.12673, 59119.9, 0.03155, 11054.0]; % initial condition: C1>2*C2 + +a = 1000; + +pmin = params0 / a; +pmax = params0 * a; +scales = [pmin, pmax]; + +params0_w = [1e-1,1e-1,1e-1,1e-1,1e-2,1,1,1e2,1e-07]; +scales_w = params0_w*10; %to be changed between lsq and bfgs + +feis = FittingEIS(params0, scales, Z_re_exp, Z_im_exp, omega); + +[~, ~, best_params, fitting_error] = feis.optimizationBFGS(); + + +% [min_value, best_params, fitting_error] = feis.optimizationLsqnonlin(); +% % Lancement avec LSQNONLIN + + +%% Printing results +feis.plotresults_thevenin(best_params, fitting_error); + +feis.printResults(best_params, fitting_error); % to be changed btw Warburg and Thevenin + + + + +%% Robustness test +% for i = 1:5 + % params0 = pmin + (pmax - pmin) .* rand(1, 5); + % disp(['Tour ', num2str(i), ' - params0 :']); + % disp(params0); + % feis = FittingEIS(params0, scales, Z_re_exp, Z_im_exp, omega); + % + % [~, ~, best_params, fitting_error] = feis.optimizationBFGS(); + % feis.plotresults_thevenin(best_params, fitting_error); + % feis.printResults(best_params, fitting_error); + % + % drawnow; + +% end +%% + +% result with lsqnonlin: +% === FITTING SCORE === +% Error : 7.324871e-02 +% +% === PARAMETERS FOUND === +% R0 = 0.05052 Ohms +% R1 = 1.12673 Ohms +% C1 = 59119.9 Farads +% R2 = 0.03155 Ohms +% C2 = 11054.0 Farads + + +% results warburg chen +% 4.7197e-02 ... +% 1.0000e-2 ... +% 2.3392e-03 ... +% 0.0103 ... +% 6.8573e-04 ... +% 1.0000e-1 ... +% 0.0307 ... +% 7.0505e+02 ... +% 2.9134e-06 \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/runFittingEIS.m b/Battery/EquivalentCircuitModel/calibration/runFittingEIS.m new file mode 100644 index 000000000..33c8d940a --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/runFittingEIS.m @@ -0,0 +1,172 @@ +%% Fitting Script + +% initial values [R0, R1, C1, R2, C2] +% params0 = [8.2e-3, 1.5e-3, 1, 1.5e-2, 200]; % handmade best ones + +params0 = [3e-3, 0.00260, 100, 0.00132, 1]; + +[Z_re_exp, Z_im_exp, omega] = load_experimental_data(); + +% lengthZ = length(Z_im_exp); + +% synthetic data +% opti_params = [0.00341, 0.00260, 5663.9, 0.00132, 0.9]; % params with +% lsqnonlin +% +% params with unitboxBFGS: +% R0 = 0.00318 Ohms +% R1 = 0.00247 Ohms +% C1 = 10654.4 Farads +% R2 = 0.00155 Ohms +% C2 = 0.4 Farads +% + +% [Z_re_exp, Z_im_exp] = load_nyquist(opti_params, omega); + +%% + +scales = [10e-2, 10e-3, 15000, 10e-3, 5]; + +params0_norm = params0 ./ scales; + +lb_true = [1e-5, 1e-5, 1e-5, 1e-5, 1e-5]; +lb_norm = lb_true ./ scales; +ub_norm = []; + +f_opt = @(p_norm) optifunc(p_norm, scales, Z_re_exp, Z_im_exp, omega) ; + +% best_params_norm = lsqnonlin(deltagap, params0_norm, lb_norm, ub_norm, [0,0,-1,0,10], 0); +params0_norm = params0_norm(:); + +[min_value, best_params_norm, history] = unitBoxBFGS(... + params0_norm, ... + f_opt, ... + 'maximize', false, ... + 'linIneq', struct('A', [0, 0, -1, 0, 2*scales(5)/scales(3)], 'b', 0), ... + 'enforceFeasible', true, ... + 'maxIt', 300, ... + 'objChangeTol', 1e-6, ... + 'gradTol', 1e-5 ... +); + + +best_params = best_params_norm(:) .* scales(:); + +[v_opt, g_opt] = optifunc(best_params_norm, scales, Z_re_exp, Z_im_exp, omega) + +fprintf('\n=== FITTING SCORE ===\n'); +fprintf('Error : %e\n', v_opt); + +fprintf('\n=== PARAMETERS FOUND ===\n'); +fprintf('R0 = %.5f Ohms\n', best_params(1)); +fprintf('R1 = %.5f Ohms\n', best_params(2)); +fprintf('C1 = %.1f Farads\n', best_params(3)); +fprintf('R2 = %.5f Ohms\n', best_params(4)); +fprintf('C2 = %.1f Farads\n', best_params(5)); + +%% Graphique + + +parameters.R0 = best_params(1); +parameters.R1 = best_params(2); +parameters.C1 = best_params(3); +parameters.R2 = best_params(4); +parameters.C2 = best_params(5); + +[Z_re_fit, Z_im_fit] = load_nyquist(best_params, omega); + + +%% +figure; +subplot(3,1,1); +semilogx(omega, Z_re_exp, 'ro', 'MarkerFaceColor', 'r'); +hold on; +semilogx(omega, Z_re_fit, 'bo', 'LineWidth', 2); +% legend('Expérimental', 'Modèle (Fitted)'); +title('Fitting results'); +xlabel('Omega'); +ylabel('Z_{re} '); + +subplot(3,1,2); +semilogx(omega, Z_im_exp, 'ro', 'MarkerFaceColor', 'r'); +hold on; +semilogx(omega, Z_im_fit, 'bo', 'LineWidth', 2); +% legend('Expérimental', 'Modèle (Fitted)'); +title('Fitting results'); +xlabel('Omega'); +ylabel('-Z_{im} '); + +subplot(3,1,3); +plot(Z_re_exp, Z_im_exp, 'ro', 'MarkerFaceColor', 'r'); +hold on; +plot(Z_re_exp, Z_im_fit, 'bo', 'LineWidth', 2); +% legend('Expérimental', 'Modèle (Fitted)'); +title('Nyquist'); +xlabel('Z_{re}'); +ylabel('-Z_{im} '); +axis equal; + +grid on; + +text_error = sprintf('Fitting error : %.2e', v_opt); + +subplot(3,1,3); + +text(0.05, 0.90, text_error, 'Units', 'normalized', ... + 'BackgroundColor', 'white', ... + 'EdgeColor', 'black', ... + 'FontSize', 11, ... + 'FontWeight', 'bold'); + +function [v, g_norm] = optifunc(p_norm, scales, Z_re_exp, Z_im_exp, omega) + + p = p_norm(:) .* scales(:); + + [Z_re_param, Z_Im_param] = load_nyquist(p, omega); + + Module_Z = sqrt(Z_re_exp.^2 + Z_im_exp.^2); + Module_Z = Module_Z(:); + + err_re = (Z_re_exp(:) - Z_re_param(:)) ./ Module_Z(:); + err_im = (Z_im_exp(:) - Z_Im_param(:)) ./ Module_Z(:); + + v = sum(err_re.^2 + err_im.^2); + + +if nargout > 1 + w = omega(:); + R0 = p(1); + R1 = p(2); + C1 = p(3); + R2 = p(4); + C2 = p(5); + + + g_re_dR0 = -ones(size(w)); + g_re_dR1 = -(1-(R1*C1.*w).^2) ./ (1+(R1*C1.*w).^2).^2; + g_re_dC1 = (2*C1*R1^3.*w.^2) ./ (1+(R1*C1.*w).^2).^2; + g_re_dR2 = -(1-(R2*C2.*w).^2) ./ (1+(R2*C2.*w).^2).^2; + g_re_dC2 = (2*C2*R2^3.*w.^2) ./ (1+(R2*C2.*w).^2).^2; + + J_re = [g_re_dR0, g_re_dR1, g_re_dC1, g_re_dR2, g_re_dC2]; + + + g_im_dR0 = zeros(size(w)); + g_im_dR1 = -(2*R1*C1.*w) ./ (1+(R1*C1.*w).^2).^2; + g_im_dC1 = -(R1^2.*w - C1^2*R1^4.*w.^3) ./ (1+(R1*C1.*w).^2).^2; + g_im_dR2 = -(2*R2*C2.*w) ./ (1+(R2*C2.*w).^2).^2; + g_im_dC2 = -(R2^2.*w - C2^2*R2^4.*w.^3) ./ (1+(R2*C2.*w).^2).^2; + + J_im = [g_im_dR0, g_im_dR1, g_im_dC1, g_im_dR2, g_im_dC2]; + derr_re_dp = J_re ./ Module_Z; + derr_im_dp = J_im ./ Module_Z; + + + g_true = 2 * (derr_re_dp' * err_re) + 2 * (derr_im_dp' * err_im); + g_norm = g_true .* scales(:); +end + +end + + + diff --git a/Battery/EquivalentCircuitModel/calibration/utils/costFunctionEIS.m b/Battery/EquivalentCircuitModel/calibration/utils/costFunctionEIS.m new file mode 100644 index 000000000..28a284906 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/utils/costFunctionEIS.m @@ -0,0 +1,15 @@ +function [total_error, Z_re_exp, Z_im_exp] = costFunctionEIS(best_params) + + + Z_re_exp = plot_nyquist_ank.Z_real; + Z_im_exp = plot_nyquist_ank.Z_imag; + + + omega = logspace(-2, 4, 100) + + Z_re_params = plot_nyquist_params.Z_real(best_params); + Z_im_params = plot_nyquist_params.Z_imag(best_params); + + totalerror = sum((Z_re_exp(omega)-Z_re_params(omega)).^2) + sum((Z_im_exp(omega)-Z_im_params(omega))^2) + +end diff --git a/Battery/EquivalentCircuitModel/calibration/utils/load_chen_data.m b/Battery/EquivalentCircuitModel/calibration/utils/load_chen_data.m new file mode 100644 index 000000000..a05b021e5 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/utils/load_chen_data.m @@ -0,0 +1,56 @@ +function [Z_real, Z_imag, omegas] = load_chen_data() + + + % We define some shorthand names for simplicity. + ne = 'NegativeElectrode'; + pe = 'PositiveElectrode'; + elyte = 'Electrolyte'; + thermal = 'ThermalModel'; + co = 'Coating'; + am = 'ActiveMaterial'; + itf = 'Interface'; + sd = 'SolidDiffusion'; + ctrl = 'Control'; + cc = 'CurrentCollector'; + + mrstModule add ad-core mrst-gui mpfa agmg linearsolvers + + jsonstruct_material = parseBattmoJson(fullfile('ParameterData','ParameterSets','Chen2020','chen2020_lithium_ion_battery.json')); + jsonstruct_geometry = parseBattmoJson(fullfile('Examples', 'JsonDataFiles', 'geometryChen.json')); + + jsonstruct = mergeJsonStructs({jsonstruct_material, ... + jsonstruct_geometry}); + + + + includeDoubleLayer = true; + + if includeDoubleLayer + + jsonstruct.(ne).(co).(am).(itf).useDoubleLayerCapacity = true; + jsonstruct.(ne).(co).(am).(itf).doubleLayerCapacitance = 0.2; + + end + + [model, inputparams, ~] = setupModelFromJson(jsonstruct); + + c_ne = 29.866*mol/litre; % initial concentration at negative electrode + c_pe = 17.038*mol/litre; % initial concentration at positive electrode + + initstate = initStateChen2020(model, c_ne, c_pe); + + options = []; + options.stateInitialization.initializationSetup = 'given state'; + options.stateInitialization.computeSteadyState = false; + + extrastructs = []; + extrastructs.initstate = initstate; + + impsolv = ImpedanceSolver(inputparams, options, extrastructs); + + omegas = logspace(-4, 2, 50); + Z = impsolv.computeImpedance(omegas); + + Z_real = real(Z); + Z_imag = imag(Z); +end diff --git a/Battery/EquivalentCircuitModel/calibration/utils/load_experimental_data.m b/Battery/EquivalentCircuitModel/calibration/utils/load_experimental_data.m new file mode 100644 index 000000000..b2e0c2643 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/utils/load_experimental_data.m @@ -0,0 +1,55 @@ +function [Z_real, Z_imag, omega] = load_experimental_data(filename) + + % --- 1. Configuration et Importation des données --- + % filename = 'C:\Users\Alexandre Fichter\Documents\stage_3A\contenu stage\data_August\ank_data\Supplementary material\02_Electrical_characterization\EIS\131-828_EIS_01_MB_CD8.txt'; + + % Le délimiteur est la tabulation ('\t') + opts = detectImportOptions(filename, 'Delimiter', '\t'); + + opts.VariableNamesLine = 1; + opts.DataLine = 2; + opts.VariableNamingRule = 'preserve'; + data = readtable(filename, opts); + colNames = data.Properties.VariableNames; + + + idx_real = contains(colNames, 'Re(Z)', 'IgnoreCase', true); + idx_imag = contains(colNames, 'Im(Z)', 'IgnoreCase', true); + idx_freq = contains(colNames, 'freq', 'IgnoreCase', true); + + % Extraction des données brutes (peu importe où elles se trouvent) + Z_real_raw = data{:, idx_real}; + Z_imag_raw = data{:, idx_imag}; + freq_raw = data{:, idx_freq}; + + if iscell(Z_real_raw) || isstring(Z_real_raw) + Z_real = str2double(strrep(Z_real_raw, ',', '.')); + Z_imag = str2double(strrep(Z_imag_raw, ',', '.')); + freq = str2double(strrep(freq_raw, ',', '.')); + else + Z_real = Z_real_raw; + Z_imag = Z_imag_raw; + freq = freq_raw; + end + + idx_valides = (Z_real ~= 0) & ~isnan(Z_real); + Z_real = Z_real(idx_valides); + Z_imag = -Z_imag(idx_valides); % in the file is given -Im + freq = freq(idx_valides); + + Z_real = Z_real(8:60); + Z_imag = Z_imag(8:60); + omega = 2*pi*freq(8:60); + + Z_real = Z_real(:); + Z_imag = Z_imag(:); + omega = omega(:); +%% Figure test + % figure; + % semilogx(omega, Z_real, 'ro', 'MarkerFaceColor', 'r'); + % % legend('Expérimental', 'Modèle (Fitted)'); + % title('Fitting results'); + % xlabel('Omega'); + % ylabel('Z_{re} '); + +end diff --git a/Battery/EquivalentCircuitModel/calibration/utils/load_nyquist.m b/Battery/EquivalentCircuitModel/calibration/utils/load_nyquist.m new file mode 100644 index 000000000..1cad56a49 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/utils/load_nyquist.m @@ -0,0 +1,15 @@ +function [Z_real, Z_imag] = load_nyquist(params, omega) + + R_0 = params(1); + R_1 = params(2); + C_1 = params(3); + R_2 = params(4); + C_2 = params(5); + + + + Z_real = R_0 + R_1 ./ (1+(R_1* C_1.*omega).^2) + R_2 ./ (1+(R_2* C_2.*omega).^2); + Z_imag = -(R_1*R_1*C_1.*omega ./ (1+(R_1* C_1.*omega).^2)) ... + - (R_2*R_2*C_2.*omega ./ (1+(R_2* C_2.*omega).^2)); + +end \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/calibration/utils/load_santoni_data.m b/Battery/EquivalentCircuitModel/calibration/utils/load_santoni_data.m new file mode 100644 index 000000000..5a84bcf63 --- /dev/null +++ b/Battery/EquivalentCircuitModel/calibration/utils/load_santoni_data.m @@ -0,0 +1,12 @@ +function [Z_real, Z_imag, omegas] = load_santoni_data() + + path_data = 'C:\Users\Alexandre Fichter\Documents\stage_3A\contenu stage\papers\LiPo Battery LP-503562-IS-3 EIS, Capacity, ECM Data\LiPO_1\EIS_Charge_discharge\EIS_45\1_EIS.csv'; + data_exp = readtable(path_data, 'Delimiter', '\t'); + + + freq = table2array(data_exp(:, 1)); + Z_real = table2array(data_exp(:, 2)); + Z_imag = table2array(data_exp(:, 3)); + + omegas = 2 * pi .* freq; +end \ No newline at end of file diff --git a/Battery/EquivalentCircuitModel/examples/exampleECM.m b/Battery/EquivalentCircuitModel/examples/exampleECM.m new file mode 100644 index 000000000..e555eb2c0 --- /dev/null +++ b/Battery/EquivalentCircuitModel/examples/exampleECM.m @@ -0,0 +1,38 @@ +%% Equivalent Circuit Model Simulation + +% Load data set + +params = createParametersECM(); +inputparams = EquivalentCircuitModelInputParams(params); + +% Setup model +model = EquivalentCircuitModel(inputparams); + +% Run Simulation +[t, U, I, SOC] = model.solve(); + + +% Plotting +figure(Name='Highlighting the difference in resistance depending on the state of charge'); +tiledlayout(3, 1); + +nexttile +plot(t, U, 'LineWidth', 2) +title('Battery Voltage') +xlabel('Time /s') +ylabel('Voltage /V') +grid on + +nexttile +plot(t, I, 'LineWidth', 2, 'Color', 'r') +title('Applied Current') +xlabel('Time /s') +ylabel('Current /A') +grid on + +nexttile +plot(t, SOC, 'LineWidth', 2, 'Color', 'g') +title('SOC') +xlabel('Time /s') +ylabel('SOC') +grid on diff --git a/Battery/EquivalentCircuitModel/examples/notebooks/calibrationECM.mlx b/Battery/EquivalentCircuitModel/examples/notebooks/calibrationECM.mlx new file mode 100644 index 000000000..f158497b4 Binary files /dev/null and b/Battery/EquivalentCircuitModel/examples/notebooks/calibrationECM.mlx differ diff --git a/Battery/EquivalentCircuitModel/examples/notebooks/exampleECM.ipynb b/Battery/EquivalentCircuitModel/examples/notebooks/exampleECM.ipynb new file mode 100644 index 000000000..6dafa2118 --- /dev/null +++ b/Battery/EquivalentCircuitModel/examples/notebooks/exampleECM.ipynb @@ -0,0 +1,104 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "# Equivalent Circuit Model\n", + "\n", + "## Load data set" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "source": [ + "jsonstruct = parseBattmoJson('exampleECM.json');\n", + "inputparams = EquivalentCircuitModelInputParams(jsonstruct);" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup model" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "source": [ + "model = EquivalentCircuitModel(inputparams);" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## run Simulation" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "source": [ + "[t, U, I] = model.solve();" + ], + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Plotting" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "source": [ + "tiledlayout(1, 2);\n", + "nexttile\n", + "plot(t, U)\n", + "nexttile\n", + "plot(t, I)" + ], + "outputs": [ + { + "data": { + "text/html": [ + "