diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/assets/deltares.png b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/deltares.png new file mode 100644 index 000000000000..098947bf3654 Binary files /dev/null and b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/deltares.png differ diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/assets/direct_shear.png b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/direct_shear.png new file mode 100644 index 000000000000..cb1c07b8252a Binary files /dev/null and b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/direct_shear.png differ diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/assets/icon.ico b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/icon.ico new file mode 100644 index 000000000000..41ceee87426f Binary files /dev/null and b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/icon.ico differ diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/assets/kratos.png b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/kratos.png new file mode 100644 index 000000000000..1f97452c94bd Binary files /dev/null and b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/kratos.png differ diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/assets/triaxial.png b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/triaxial.png new file mode 100644 index 000000000000..9e1e19a08c6c Binary files /dev/null and b/applications/GeoMechanicsApplication/python_scripts/element_test/assets/triaxial.png differ diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/triaxial.py b/applications/GeoMechanicsApplication/python_scripts/element_test/generic_test_runner.py similarity index 78% rename from applications/GeoMechanicsApplication/python_scripts/element_test/triaxial.py rename to applications/GeoMechanicsApplication/python_scripts/element_test/generic_test_runner.py index 3e32a5b85b40..02bf1f82d473 100644 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/triaxial.py +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/generic_test_runner.py @@ -14,33 +14,7 @@ raise ImportError(f"Tests folder not found at: {tests_folder_path}") -class TriaxialTest: - def __init__(self, json_file_path): - self.json_file_path = json_file_path - self.data = self._read_json() - - def _read_json(self): - with open(self.json_file_path, 'r') as file: - return json.load(file) - - def _write_json(self): - with open(self.json_file_path, 'w') as file: - json.dump(self.data, file, indent=4) - - def modify_umat_parameters(self, new_value): - self.data['properties'][0]['Material']['Variables']['UMAT_PARAMETERS'][0] = new_value - self._write_json() - - def read_umat_parameters(self): - try: - umat_parameters = self.data['properties'][0]['Material']['Variables']['UMAT_PARAMETERS'] - cohesion = umat_parameters[2] - friction_angle = umat_parameters[3] - return cohesion, friction_angle - except KeyError: - raise KeyError("Cohesion and Friction angle not found in the UMAT_PARAMETERS.") - -class TriaxialTestRunner: +class GenericTestRunner: def __init__(self, output_file_paths, work_dir): self.output_file_paths = output_file_paths self.work_dir = work_dir @@ -51,11 +25,12 @@ def run(self): stress, mean_stress, von_mises, _, strain = self._collect_results() tensors = self._extract_stress_tensors(stress) - yy_strain, vol_strain = self._compute_strains(strain) + shear_stress_xy = self._extract_shear_stress_xy(stress) + yy_strain, vol_strain, shear_strain_xy = self._compute_strains(strain) von_mises_values = self._compute_scalar_stresses(von_mises) mean_stress_values = self._compute_scalar_stresses(mean_stress) - return tensors, yy_strain, vol_strain, von_mises_values, mean_stress_values + return tensors, yy_strain, vol_strain, von_mises_values, mean_stress_values, shear_stress_xy, shear_strain_xy def _load_stage_parameters(self): parameter_files = [os.path.join(self.work_dir, 'ProjectParameters.json')] @@ -119,8 +94,19 @@ def _extract_stress_tensors(self, stress_results): reshaped[time_step] = [tensor] return reshaped + def _extract_shear_stress_xy(self, stress_results): + shear_stress_xy = [] + for result in stress_results: + values = result["values"] + if not values: + continue + stress_components = values[0]["value"][0] + shear_xy = stress_components[3] + shear_stress_xy.append(shear_xy) + return shear_stress_xy + def _compute_strains(self, strain_results): - yy, vol = [], [] + yy, vol, shear_xy = [], [], [] for result in strain_results: values = result["values"] if not values: @@ -128,7 +114,8 @@ def _compute_strains(self, strain_results): eps_xx, eps_yy, eps_zz, eps_xy = values[0]["value"][0][:4] vol.append(eps_xx + eps_yy + eps_zz) yy.append(eps_yy) - return yy, vol + shear_xy.append(eps_xy) + return yy, vol, shear_xy def _compute_scalar_stresses(self, results): return [r["values"][0]["value"][1] for r in results if r["values"]] diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/mdpa_editor.py b/applications/GeoMechanicsApplication/python_scripts/element_test/mdpa_editor.py index 3285d47090b6..e618afdf2a71 100644 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/mdpa_editor.py +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/mdpa_editor.py @@ -27,7 +27,7 @@ def update_maximum_strain(self, maximum_strain): replacer = self._replacer_factory(prescribed_displacement) new_text, count = re.subn(pattern, replacer, self.raw_text) if count == 0: - log_message("Could not update maximum strain.", "Warning") + log_message("Could not update maximum strain.", "warn") else: self.raw_text = new_text MdpaEditor.save(self) @@ -38,7 +38,7 @@ def update_initial_effective_cell_pressure(self, initial_effective_cell_pressure replacer = self._replacer_factory(initial_effective_cell_pressure) new_text, count = re.subn(pattern, replacer, self.raw_text) if count == 0: - log_message("Could not update initial effective cell pressure.", "Warning") + log_message("Could not update initial effective cell pressure.", "warn") else: self.raw_text = new_text MdpaEditor.save(self) @@ -50,7 +50,7 @@ def update_first_timestep(self, num_steps, end_time): replacer = self._replacer_factory(first_timestep) new_text, count = re.subn(pattern, replacer, self.raw_text) if count == 0: - log_message("Could not apply the first time step.", "Warning") + log_message("Could not apply the first time step.", "warn") else: self.raw_text = new_text MdpaEditor.save(self) @@ -62,7 +62,19 @@ def update_end_time(self, end_time): new_text, count = re.subn(pattern, replacement, self.raw_text) if count == 0: - log_message("Could not update the end time.", "Warning") + log_message("Could not update the end time.", "warn") + else: + self.raw_text = new_text + MdpaEditor.save(self) + + def update_middle_maximum_strain(self, maximum_strain): + pattern = r'\$middle_maximum_strain\b' + prescribed_middle_displacement = (-maximum_strain / 2) / 100 + + replacer = self._replacer_factory(prescribed_middle_displacement) + new_text, count = re.subn(pattern, replacer, self.raw_text) + if count == 0: + log_message("Could not update middle maximum strain.", "warn") else: self.raw_text = new_text MdpaEditor.save(self) diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/plots.py b/applications/GeoMechanicsApplication/python_scripts/element_test/plots.py new file mode 100644 index 000000000000..9bdf480d952e --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/plots.py @@ -0,0 +1,168 @@ +import numpy as np +from ui_logger import log_message +from ui_labels import ( + SIGMA1_LABEL, SIGMA3_LABEL, SIGMA1_SIGMA3_DIFF_LABEL, VERTICAL_STRAIN_LABEL, + VOLUMETRIC_STRAIN_LABEL, SHEAR_STRAIN_LABEL, SHEAR_STRESS_LABEL, EFFECTIVE_STRESS_LABEL, + MOBILIZED_SHEAR_STRESS_LABEL, P_STRESS_LABEL, Q_STRESS_LABEL, TITLE_SIGMA1_VS_SIGMA3, + TITLE_DIFF_PRINCIPAL_SIGMA_VS_STRAIN, TITLE_VOL_VS_VERT_STRAIN, TITLE_MOHR, TITLE_P_VS_Q, TITLE_SHEAR_VS_STRAIN, + LEGEND_MC, LEGEND_MC_FAILURE +) + + +def plot_principal_stresses_triaxial(ax, sigma_1, sigma_3): + ax.plot(sigma_3, sigma_1, '-', color='blue', label=TITLE_SIGMA1_VS_SIGMA3) + ax.set_title(TITLE_SIGMA1_VS_SIGMA3) + ax.set_xlabel(SIGMA3_LABEL) + ax.set_ylabel(SIGMA1_LABEL) + ax.grid(True) + ax.locator_params(nbins=8) + + min_val = 0 + max_val_x = max(sigma_3) + max_val_y = min(sigma_1) + padding_x = 0.1 * (max_val_x - min_val) + padding_y = 0.1 * (max_val_y - min_val) + ax.set_xlim(min_val, max_val_x + padding_x) + ax.set_ylim(min_val, max_val_y + padding_y) + ax.minorticks_on() + +def plot_delta_sigma_triaxial(ax, vertical_strain, sigma_diff): + ax.plot(vertical_strain, sigma_diff, '-', color='blue', label=SIGMA1_SIGMA3_DIFF_LABEL) + ax.set_title(TITLE_DIFF_PRINCIPAL_SIGMA_VS_STRAIN) + ax.set_xlabel(VERTICAL_STRAIN_LABEL) + ax.set_ylabel(SIGMA1_SIGMA3_DIFF_LABEL) + ax.grid(True) + ax.invert_xaxis() + ax.locator_params(nbins=8) + ax.minorticks_on() + +def plot_volumetric_vertical_strain_triaxial(ax, vertical_strain, volumetric_strain): + ax.plot(vertical_strain, volumetric_strain, '-', color='blue', label=TITLE_VOL_VS_VERT_STRAIN) + ax.set_title(TITLE_VOL_VS_VERT_STRAIN) + ax.set_xlabel(VERTICAL_STRAIN_LABEL) + ax.set_ylabel(VOLUMETRIC_STRAIN_LABEL) + ax.grid(True) + ax.invert_xaxis() + ax.invert_yaxis() + ax.locator_params(nbins=8) + ax.minorticks_on() + +def plot_mohr_coulomb_triaxial(ax, sigma_1, sigma_3, cohesion=None, friction_angle=None): + if np.isclose(sigma_1, sigma_3): + log_message("σ₁ is equal to σ₃. Mohr circle collapses to a point.", "warn") + center = (sigma_1 + sigma_3) / 2 + radius = (sigma_1 - sigma_3) / 2 + theta = np.linspace(0, np.pi, 200) + sigma = center + radius * np.cos(theta) + tau = -radius * np.sin(theta) + + ax.plot(sigma, tau, label=LEGEND_MC, color='blue') + + if cohesion is not None and friction_angle is not None: + phi_rad = np.radians(friction_angle) + x_line = np.linspace(0, sigma_1, 200) + y_line = x_line * np.tan(phi_rad) - cohesion + ax.plot(x_line, -y_line, 'r--', label=LEGEND_MC_FAILURE) + ax.legend(loc='upper left') + + ax.set_title(LEGEND_MC) + ax.set_xlabel(EFFECTIVE_STRESS_LABEL) + ax.set_ylabel(MOBILIZED_SHEAR_STRESS_LABEL) + ax.grid(True) + ax.invert_xaxis() + ax.set_xlim(left=0, right= 1.2*np.max(sigma_1)) + ax.set_ylim(bottom=0, top = -0.6*np.max(sigma_1)) + ax.minorticks_on() + +def plot_p_q_triaxial(ax, p_list, q_list): + ax.plot(p_list, q_list, '-', color='blue', label=TITLE_P_VS_Q) + ax.set_title(TITLE_P_VS_Q) + ax.set_xlabel(P_STRESS_LABEL) + ax.set_ylabel(Q_STRESS_LABEL) + ax.grid(True) + ax.invert_xaxis() + ax.locator_params(nbins=8) + ax.minorticks_on() + +def plot_principal_stresses_direct_shear(ax, sigma_1, sigma_3): + ax.plot(sigma_3, sigma_1, '-', color='blue', label=TITLE_SIGMA1_VS_SIGMA3) + ax.set_title(TITLE_SIGMA1_VS_SIGMA3) + ax.set_xlabel(SIGMA3_LABEL) + ax.set_ylabel(SIGMA1_LABEL) + ax.grid(True) + ax.locator_params(nbins=8) + + min_x, max_x = min(sigma_3), max(sigma_3) + min_y, max_y = min(sigma_1), max(sigma_1) + x_padding = 0.1 * (max_x - min_x) if max_x > min_x else 1.0 + y_padding = 0.1 * (max_y - min_y) if max_y > min_y else 1.0 + ax.set_xlim(min_x - x_padding, max_x + x_padding) + ax.set_ylim(min_y - y_padding, max_y + y_padding) + + ax.invert_xaxis() + ax.invert_yaxis() + ax.minorticks_on() + +def plot_strain_stress_direct_shear(ax, shear_strain_xy, shear_stress_xy): + gamma_xy = 2 * np.array(shear_strain_xy) + ax.plot(np.abs(gamma_xy), np.abs(shear_stress_xy), '-', color='blue', label=TITLE_SHEAR_VS_STRAIN) + ax.set_title(TITLE_SHEAR_VS_STRAIN) + ax.set_xlabel(SHEAR_STRAIN_LABEL) + ax.set_ylabel(SHEAR_STRESS_LABEL) + ax.grid(True) + ax.locator_params(nbins=8) + ax.minorticks_on() + +def plot_mohr_coulomb_direct_shear(ax, sigma_1, sigma_3, cohesion=None, friction_angle=None): + if np.isclose(sigma_1, sigma_3): + log_message("σ₁ is equal to σ₃. Mohr circle collapses to a point.", "warn") + center = (sigma_1 + sigma_3) / 2 + radius = (sigma_1 - sigma_3) / 2 + theta = np.linspace(0, np.pi, 400) + sigma = center + radius * np.cos(theta) + tau = -radius * np.sin(theta) + + ax.plot(sigma, tau, label=LEGEND_MC, color='blue') + + if cohesion is not None and friction_angle is not None: + phi_rad = np.radians(friction_angle) + max_sigma = center + radius + x_line = np.linspace(0, max_sigma * 1.5, 400) + y_line = x_line * np.tan(phi_rad) - cohesion + ax.plot(x_line, -y_line, 'r--', label=LEGEND_MC_FAILURE) + ax.legend(loc='upper left') + + ax.set_title(LEGEND_MC) + ax.set_xlabel(EFFECTIVE_STRESS_LABEL) + ax.set_ylabel(MOBILIZED_SHEAR_STRESS_LABEL) + ax.grid(True) + ax.invert_xaxis() + + epsilon = 0.1 + relative_diff = np.abs(sigma_1 - sigma_3) / max(np.abs(sigma_1), 1e-6) + + if relative_diff < epsilon: + ax.set_xlim(center - (1.2 * radius), center + (1.2 * radius)) + ax.set_ylim(bottom=0, top = -0.9*(np.max(sigma_1) - np.max(sigma_3))) + + else: + if sigma_1 > 0 or sigma_3 > 0: + ax.set_xlim(left=1.2*np.max(sigma_3), right=1.2*np.max(sigma_1)) + ax.set_ylim(bottom=0, top = -0.9*(np.max(sigma_1) - np.max(sigma_3))) + else: + ax.set_xlim(left=0, right=1.2*np.max(sigma_1)) + ax.set_ylim(bottom=0, top = -0.9*np.max(sigma_1)) + + + ax.minorticks_on() + +def plot_p_q_direct_shear(ax, p_list, q_list): + ax.plot(p_list, q_list, '-', color='blue', label=TITLE_P_VS_Q) + ax.set_title(TITLE_P_VS_Q) + ax.set_xlabel(P_STRESS_LABEL) + ax.set_ylabel(Q_STRESS_LABEL) + ax.grid(True) + ax.invert_xaxis() + ax.set_xlim(left=0, right=1.2*np.max(p_list)) + ax.locator_params(nbins=8) + ax.minorticks_on() diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/run_simulation.py b/applications/GeoMechanicsApplication/python_scripts/element_test/run_simulation.py new file mode 100644 index 000000000000..7089b2c9f532 --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/run_simulation.py @@ -0,0 +1,118 @@ +import os +import shutil +import tempfile +import numpy as np + +from material_editor import MaterialEditor +from project_parameter_editor import ProjectParameterEditor +from mdpa_editor import MdpaEditor +from generic_test_runner import GenericTestRunner +from plots import ( + plot_delta_sigma_triaxial, plot_volumetric_vertical_strain_triaxial, plot_principal_stresses_triaxial, + plot_p_q_triaxial, plot_mohr_coulomb_triaxial, + plot_strain_stress_direct_shear, plot_principal_stresses_direct_shear, + plot_p_q_direct_shear, plot_mohr_coulomb_direct_shear +) + +def setup_simulation_files(test_type, tmp_folder): + base_dir = os.path.dirname(os.path.abspath(__file__)) + src_dir = os.path.join(base_dir, f"test_{test_type}") + for filename in ["MaterialParameters.json", "ProjectParameters.json", "mesh.mdpa"]: + shutil.copy(os.path.join(src_dir, filename), tmp_folder) + return ( + os.path.join(tmp_folder, "MaterialParameters.json"), + os.path.join(tmp_folder, "ProjectParameters.json"), + os.path.join(tmp_folder, "mesh.mdpa") + ) + +def set_material_constitutive_law(json_file_path, dll_path, material_parameters, index): + editor = MaterialEditor(json_file_path) + if dll_path: + editor.update_material_properties({ + "IS_FORTRAN_UDSM": True, + "UMAT_PARAMETERS": material_parameters, + "UDSM_NAME": dll_path, + "UDSM_NUMBER": index + }) + editor.set_constitutive_law("SmallStrainUDSM2DPlaneStrainLaw") + else: + editor.update_material_properties({ + "YOUNG_MODULUS": material_parameters[0], + "POISSON_RATIO": material_parameters[1] + }) + editor.set_constitutive_law("GeoLinearElasticPlaneStrain2DLaw") + +def set_project_parameters(project_path, num_steps, end_time, initial_stress): + editor = ProjectParameterEditor(project_path) + editor.update_property('time_step', end_time / num_steps) + editor.update_property('end_time', end_time) + stress_vector = [-initial_stress] * 3 + [0.0] + editor.update_nested_value("apply_initial_uniform_stress_field", "value", stress_vector) + +def set_mdpa(mdpa_path, max_strain, init_pressure, num_steps, end_time, test_type): + editor = MdpaEditor(mdpa_path) + editor.update_maximum_strain(max_strain) + editor.update_end_time(end_time) + editor.update_first_timestep(num_steps, end_time) + if test_type == "triaxial": + editor.update_initial_effective_cell_pressure(init_pressure) + if test_type == "direct_shear": + editor.update_middle_maximum_strain(max_strain) + +def calculate_principal_stresses(tensors): + sigma_1, sigma_3 = [], [] + for tensors_at_time in tensors.values(): + for sigma in tensors_at_time: + eigenvalues, _ = np.linalg.eigh(sigma) + sigma_1.append(np.min(eigenvalues)) + sigma_3.append(np.max(eigenvalues)) + return sigma_1, sigma_3 + +def get_cohesion_phi(umat_parameters, indices): + if indices: + c_idx, phi_idx = indices + return float(umat_parameters[c_idx - 1]), float(umat_parameters[phi_idx - 1]) + return None, None + +def plot_results(test_type, axes, yy, vol, sigma1, sigma3, shear_xy, shear_strain_xy, mean_stress, von_mises, cohesion, phi): + if test_type == "triaxial": + plot_delta_sigma_triaxial(axes[0], yy, abs(np.array(sigma1) - np.array(sigma3))) + plot_volumetric_vertical_strain_triaxial(axes[1], yy, vol) + plot_principal_stresses_triaxial(axes[2], sigma1, sigma3) + plot_p_q_triaxial(axes[3], mean_stress, von_mises) + plot_mohr_coulomb_triaxial(axes[4], sigma1[-1], sigma3[-1], cohesion, phi) + elif test_type == "direct_shear": + plot_strain_stress_direct_shear(axes[0], shear_strain_xy, shear_xy) + plot_principal_stresses_direct_shear(axes[1], sigma1, sigma3) + plot_p_q_direct_shear(axes[2], mean_stress, von_mises) + plot_mohr_coulomb_direct_shear(axes[3], sigma1[-1], sigma3[-1], cohesion, phi) + else: + raise ValueError(f"Unsupported test_type: {test_type}") + +def run_simulation(test_type, dll_path, index, material_parameters, num_steps, end_time, maximum_strain, + initial_effective_cell_pressure, cohesion_phi_indices=None, axes=None): + tmp_folder = tempfile.mkdtemp(prefix=f"{test_type}_") + + try: + json_path, project_path, mdpa_path = setup_simulation_files(test_type, tmp_folder) + + set_material_constitutive_law(json_path, dll_path, material_parameters, index) + set_project_parameters(project_path, num_steps, end_time, initial_effective_cell_pressure) + set_mdpa(mdpa_path, maximum_strain, initial_effective_cell_pressure, num_steps, end_time, test_type) + + runner = GenericTestRunner([os.path.join(tmp_folder, 'gid_output', "output.post.res")], tmp_folder) + tensors, yy_strain, vol_strain, von_mises, mean_stress, shear_xy, shear_strain_xy = runner.run() + + sigma_1, sigma_3 = calculate_principal_stresses(tensors) + cohesion, friction_angle = get_cohesion_phi(material_parameters, cohesion_phi_indices) + + if axes: + for ax in axes: + ax.clear() + + plot_results(test_type, axes, yy_strain, vol_strain, sigma_1, sigma_3, + shear_xy, shear_strain_xy, mean_stress, von_mises, cohesion, friction_angle) + + finally: + if os.path.exists(tmp_folder): + shutil.rmtree(tmp_folder) diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/run_triaxial_simulation.py b/applications/GeoMechanicsApplication/python_scripts/element_test/run_triaxial_simulation.py deleted file mode 100644 index dfc164002eed..000000000000 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/run_triaxial_simulation.py +++ /dev/null @@ -1,97 +0,0 @@ -import os -import shutil -import tempfile -import numpy as np - -from material_editor import MaterialEditor -from project_parameter_editor import ProjectParameterEditor -from mdpa_editor import MdpaEditor -from triaxial import TriaxialTestRunner -from triaxial_plots import plot_delta_sigma, plot_volumetric_strain, plot_sigma, plot_p_q, plot_mohr_coulomb_circle - - -def run_triaxial_simulation(dll_path, index, umat_parameters, num_steps, end_time, maximum_strain, initial_effective_cell_pressure, - cohesion_phi_indices=None, axes=None): - tmp_folder = tempfile.mkdtemp(prefix="triaxial_") - - base_dir = os.path.dirname(os.path.abspath(__file__)) - src_dir = os.path.join(base_dir, "test_triaxial") - files_to_copy = [ - "MaterialParameters.json", - "ProjectParameters.json", - "triaxial.mdpa" - ] - - for filename in files_to_copy: - shutil.copy(os.path.join(src_dir, filename), tmp_folder) - - json_file_path = os.path.join(tmp_folder, "MaterialParameters.json") - project_param_path = os.path.join(tmp_folder, "ProjectParameters.json") - mdpa_path = os.path.join(tmp_folder, "triaxial.mdpa") - - material_editor = MaterialEditor(json_file_path) - - if dll_path: - material_editor.update_material_properties({ - "IS_FORTRAN_UDSM": True, - "UMAT_PARAMETERS": umat_parameters, - "UDSM_NAME": dll_path, - "UDSM_NUMBER": index - }) - material_editor.set_constitutive_law("SmallStrainUDSM2DPlaneStrainLaw") - else: - material_editor.update_material_properties({ - "YOUNG_MODULUS": umat_parameters[0], - "POISSON_RATIO": umat_parameters[1] - }) - material_editor.set_constitutive_law("GeoLinearElasticPlaneStrain2DLaw") - - project_editor = ProjectParameterEditor(project_param_path) - project_editor.update_property('time_step', end_time / num_steps) - project_editor.update_property('end_time', end_time) - initial_uniform_stress_value = [-initial_effective_cell_pressure] * 3 + [0.0] - project_editor.update_nested_value("apply_initial_uniform_stress_field", "value", initial_uniform_stress_value) - - mdpa_editor = MdpaEditor(mdpa_path) - mdpa_editor.update_maximum_strain(maximum_strain) - mdpa_editor.update_initial_effective_cell_pressure(initial_effective_cell_pressure) - mdpa_editor.update_end_time(end_time) - mdpa_editor.update_first_timestep(num_steps, end_time) - - output_files = [os.path.join(tmp_folder, 'gid_output', "triaxial.post.res")] - runner = TriaxialTestRunner(output_files, tmp_folder) - reshaped_values_by_time, vertical_strain, volumetric_strain, von_mises_stress, mean_effective_stresses = runner.run() - - if cohesion_phi_indices: - c_idx, phi_idx = cohesion_phi_indices - cohesion = float(umat_parameters[c_idx - 1]) - friction_angle = float(umat_parameters[phi_idx - 1]) - else: - cohesion = None - friction_angle = None - - sigma1_list = [] - sigma3_list = [] - eigenvector_list = [] - eigenvalue_list = [] - for time_step, tensors in reshaped_values_by_time.items(): - for sigma in tensors: - eigenvalues, eigenvectors = np.linalg.eigh(sigma) - eigenvector_list.append(eigenvectors) - eigenvalue_list.append(eigenvalues) - sigma_max = np.max(eigenvalues) - sigma_min = np.min(eigenvalues) - sigma1_list.append(sigma_min) - sigma3_list.append(sigma_max) - - sigma_diff = abs(np.array(sigma1_list) - np.array(sigma3_list)) - - if axes: - for ax in axes: - ax.clear() - - plot_delta_sigma(axes[0], vertical_strain, sigma_diff) - plot_volumetric_strain(axes[1], vertical_strain, volumetric_strain) - plot_sigma(axes[2], sigma1_list, sigma3_list) - plot_p_q(axes[3], mean_effective_stresses, von_mises_stress) - plot_mohr_coulomb_circle(axes[4], sigma1_list[-1], sigma3_list[-1], cohesion, friction_angle) diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/MaterialParameters.json b/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/MaterialParameters.json new file mode 100644 index 000000000000..02803fa55c60 --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/MaterialParameters.json @@ -0,0 +1,30 @@ +{ + "properties": [ + { + "model_part_name": "PorousDomain.Soil", + "properties_id": 1, + "Material": { + "constitutive_law": { + "name": "SmallStrainUDSM2DPlaneStrainLaw" + }, + "Variables": { + "IGNORE_UNDRAINED": true, + "DENSITY_SOLID": 2.65, + "DENSITY_WATER": 1.0, + "POROSITY": 0.3, + "BULK_MODULUS_SOLID": 1000000000.0, + "BULK_MODULUS_FLUID": 2e-30, + "PERMEABILITY_XX": 4.5e-30, + "PERMEABILITY_YY": 4.5e-30, + "PERMEABILITY_XY": 0.0, + "DYNAMIC_VISCOSITY": 8.9e-07, + "BIOT_COEFFICIENT": 1.0, + "RETENTION_LAW": "SaturatedBelowPhreaticLevelLaw", + "SATURATED_SATURATION": 1.0, + "RESIDUAL_SATURATION": 1e-10, + "MINIMUM_RELATIVE_PERMEABILITY": 0.0001 + } + } + } + ] +} \ No newline at end of file diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/ProjectParameters.json b/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/ProjectParameters.json new file mode 100644 index 000000000000..987834de8ac2 --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/ProjectParameters.json @@ -0,0 +1,152 @@ +{ + "problem_data" : { + "problem_name" : "direct_shear", + "parallel_type" : "OpenMP", + "echo_level" : 1, + "start_time" : 0.0, + "end_time" : 1.0 + }, + "solver_settings" : { + "time_stepping" : { + "time_step" : 0.05, + "max_delta_time_factor" : 1 + }, + "solver_type" : "U_Pw", + "solution_type" : "Quasi-Static", + "strategy_type" : "line_search", + "scheme_type" : "Backward_Euler", + "model_part_name" : "PorousDomain", + "domain_size" : 2, + "echo_level" : 1, + "model_import_settings" : { + "input_type" : "mdpa", + "input_filename" : "mesh" + }, + "material_import_settings" : { + "materials_filename" : "MaterialParameters.json" + }, + "buffer_size" : 2, + "clear_storage" : false, + "compute_reactions" : true, + "move_mesh_flag" : false, + "reform_dofs_at_each_step" : false, + "nodal_smoothing" : false, + "block_builder" : true, + "reset_displacements" : true, + "convergence_criterion" : "residual_criterion", + "residual_relative_tolerance" : 1.0e-3, + "residual_absolute_tolerance" : 1.0e-9, + "min_iterations" : 6, + "max_iterations" : 15, + "number_cycles" : 1, + "reduction_factor" : 0.5, + "increase_factor" : 2.0, + "desired_iterations" : 4, + "max_radius_factor" : 10.0, + "min_radius_factor" : 0.1, + "calculate_reactions" : true, + "max_line_search_iterations" : 5, + "first_alpha_value" : 0.5, + "second_alpha_value" : 1.0, + "min_alpha" : 0.1, + "max_alpha" : 2.0, + "line_search_tolerance" : 0.5, + "rotation_dofs" : false, + "problem_domain_sub_model_part_list" : ["Soil"], + "processes_sub_model_part_list" : ["Fixed_base","Soil","Top_displacement","Middle_displacement"], + "body_domain_sub_model_part_list" : ["Soil"], + "linear_solver_settings" : { + "solver_type" : "LinearSolversApplication.sparse_lu", + "scaling" : true + } + }, + "processes" : { + "constraints_process_list" : [{ + "python_module" : "apply_vector_constraint_table_process", + "kratos_module" : "KratosMultiphysics.GeoMechanicsApplication", + "process_name" : "ApplyVectorConstraintTableProcess", + "Parameters" : { + "model_part_name" : "PorousDomain.Fixed_base", + "variable_name" : "DISPLACEMENT", + "active" : [true,true,false], + "is_fixed" : [true,true,false], + "value" : [0.0,0.0,0.0], + "table" : [0,0,0] + } + },{ + "python_module" : "apply_vector_constraint_table_process", + "kratos_module" : "KratosMultiphysics.GeoMechanicsApplication", + "process_name" : "ApplyVectorConstraintTableProcess", + "Parameters" : { + "model_part_name" : "PorousDomain.Top_displacement", + "variable_name" : "DISPLACEMENT", + "active" : [true,true,false], + "is_fixed" : [true,true,false], + "value" : [0.0,0.0,0.0], + "table" : [1,0,0] + } + },{ + "python_module" : "apply_vector_constraint_table_process", + "kratos_module" : "KratosMultiphysics.GeoMechanicsApplication", + "process_name" : "ApplyVectorConstraintTableProcess", + "Parameters" : { + "model_part_name" : "PorousDomain.Middle_displacement", + "variable_name" : "DISPLACEMENT", + "active" : [true,true,false], + "is_fixed" : [true,true,false], + "value" : [0.0,0.0,0.0], + "table" : [2,0,0] + } + },{ + "python_module": "apply_scalar_constraint_table_process", + "kratos_module": "KratosMultiphysics.GeoMechanicsApplication", + "process_name": "ApplyScalarConstraintTableProcess", + "Parameters": { + "model_part_name": "PorousDomain.Soil", + "variable_name": "WATER_PRESSURE", + "is_fixed": true, + "fluid_pressure_type": "Uniform", + "value": 0.0, + "table": 0 + } + }], + "loads_process_list" : [{ + "python_module": "apply_initial_uniform_stress_field", + "kratos_module": "KratosMultiphysics.GeoMechanicsApplication", + "process_name": "ApplyInitialUniformStressField", + "Parameters": { + "model_part_name": "PorousDomain.Soil", + "value": [-100,-100.0,-100,0.0] + } + }] + }, + "output_processes" : { + "gid_output" : [{ + "python_module" : "gid_output_process", + "kratos_module" : "KratosMultiphysics", + "process_name" : "GiDOutputProcess", + "Parameters" : { + "model_part_name" : "PorousDomain.porous_computational_model_part", + "postprocess_parameters" : { + "result_file_configuration" : { + "gidpost_flags" : { + "GiDPostMode" : "GiD_PostAscii", + "WriteDeformedMeshFlag" : "WriteDeformed", + "WriteConditionsFlag" : "WriteConditions", + "MultiFileFlag" : "SingleFile" + }, + "file_label" : "step", + "output_control_type" : "step", + "output_interval" : 1, + "body_output" : true, + "node_output" : false, + "skin_output" : false, + "nodal_results" : ["DISPLACEMENT","WATER_PRESSURE"], + "gauss_point_results" : ["CAUCHY_STRESS_TENSOR","VON_MISES_STRESS","MEAN_EFFECTIVE_STRESS","ENGINEERING_STRAIN_TENSOR"] + } + }, + "output_name" : "gid_output/output" + } + }] + } +} diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/mesh.mdpa b/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/mesh.mdpa new file mode 100644 index 000000000000..95ea9fbe6722 --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/test_direct_shear/mesh.mdpa @@ -0,0 +1,83 @@ +Begin Table 1 TIME DISPLACEMENT_X + 0.0 0.0 + $first_timestep 0.0 + $end_time $maximum_strain +End Table + +Begin Table 2 TIME DISPLACEMENT_X + 0.0 0.0 + $first_timestep 0.0 + $end_time $middle_maximum_strain +End Table + +Begin Properties 0 +End Properties +Begin Nodes + 1 0.0000000000 1.0000000000 0.0000000000 + 2 0.5000000000 1.0000000000 0.0000000000 + 3 0.0000000000 0.5000000000 0.0000000000 + 4 0.5000000000 0.5000000000 0.0000000000 + 5 0.0000000000 0.0000000000 0.0000000000 + 6 1.0000000000 1.0000000000 0.0000000000 + 7 1.0000000000 0.5000000000 0.0000000000 + 8 0.5000000000 0.0000000000 0.0000000000 + 9 1.0000000000 0.0000000000 0.0000000000 +End Nodes + +Begin Elements SmallStrainUPwDiffOrderElement2D6N// GUI group identifier: Soil + 1 0 1 5 9 3 8 4 + 2 0 9 6 1 7 2 4 +End Elements + +Begin Conditions LineNormalLoadDiffOrderCondition2D3N// GUI group identifier: Top_load + 1 0 6 1 2 +End Conditions + +Begin SubModelPart Fixed_base // Group Fixed_base // Subtree DISPLACEMENT + Begin SubModelPartNodes + 5 + 8 + 9 + End SubModelPartNodes +End SubModelPart + +Begin SubModelPart Soil // Group Soil // Subtree Parts_Soil + Begin SubModelPartNodes + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + End SubModelPartNodes + Begin SubModelPartElements + 1 + 2 + End SubModelPartElements +End SubModelPart + +Begin SubModelPart Top_displacement // Group Top_displacement // Subtree Top_displacement + Begin SubModelPartTables + 1 + End SubModelPartTables + Begin SubModelPartNodes + 1 + 2 + 6 + End SubModelPartNodes +End SubModelPart + +Begin SubModelPart Middle_displacement // Group Middle_displacement // Subtree Middle_displacement + Begin SubModelPartTables + 2 + End SubModelPartTables + Begin SubModelPartNodes + 3 + 4 + 7 + End SubModelPartNodes +End SubModelPart + diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/ProjectParameters.json b/applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/ProjectParameters.json index 8376801e0390..33c4fab011af 100644 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/ProjectParameters.json +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/ProjectParameters.json @@ -20,7 +20,7 @@ "echo_level" : 1, "model_import_settings" : { "input_type" : "mdpa", - "input_filename" : "triaxial" + "input_filename" : "mesh" }, "material_import_settings" : { "materials_filename" : "MaterialParameters.json" @@ -37,8 +37,8 @@ "residual_relative_tolerance" : 1.0e-3, "residual_absolute_tolerance" : 1.0e-9, "min_iterations" : 6, - "max_iterations" : 150, - "number_cycles" : 5, + "max_iterations" : 15, + "number_cycles" : 1, "reduction_factor" : 0.5, "increase_factor" : 2.0, "desired_iterations" : 4, @@ -153,13 +153,11 @@ "body_output" : true, "node_output" : false, "skin_output" : false, - "plane_output" : [], "nodal_results" : ["DISPLACEMENT","WATER_PRESSURE"], - "nodal_nonhistorical_results" : [], "gauss_point_results" : ["CAUCHY_STRESS_TENSOR","VON_MISES_STRESS","MEAN_EFFECTIVE_STRESS","ENGINEERING_STRAIN_TENSOR"] } }, - "output_name" : "gid_output/triaxial" + "output_name" : "gid_output/output" } }] } diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/triaxial.mdpa b/applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/mesh.mdpa similarity index 100% rename from applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/triaxial.mdpa rename to applications/GeoMechanicsApplication/python_scripts/element_test/test_triaxial/mesh.mdpa diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/triaxial_plots.py b/applications/GeoMechanicsApplication/python_scripts/element_test/triaxial_plots.py deleted file mode 100644 index cb2ae4948ab7..000000000000 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/triaxial_plots.py +++ /dev/null @@ -1,73 +0,0 @@ -import numpy as np -from ui_logger import log_message - - -def plot_sigma(ax, sigma_1, sigma_3): - ax.plot(sigma_3, sigma_1, '-', color='blue', label='σ₁ vs σ₃') - ax.set_title('σ₁ vs σ₃') - ax.set_xlabel('σ₃ (Principal Stress 3) [kN/m²]') - ax.set_ylabel('σ₁ (Principal Stress 1) [kN/m²]') - ax.grid(True) - ax.locator_params(nbins=8) - - min_val = 0 - max_val_x = max(sigma_3) - max_val_y = min(sigma_1) - padding_x = 0.1 * (max_val_x - min_val) - padding_y = 0.1 * (max_val_y - min_val) - ax.set_xlim(min_val, max_val_x + padding_x) - ax.set_ylim(min_val, max_val_y + padding_y) - -def plot_delta_sigma(ax, vertical_strain, sigma_diff): - ax.plot(vertical_strain, sigma_diff, '-', color='blue', label='|σ₁ - σ₃|') - ax.set_title('|σ₁ - σ₃| vs εᵧᵧ') - ax.set_xlabel('εᵧᵧ (Vertical Strain) [-]') - ax.set_ylabel('|σ₁ - σ₃| [kN/m²]') - ax.grid(True) - ax.invert_xaxis() - ax.locator_params(nbins=8) - -def plot_volumetric_strain(ax, vertical_strain, volumetric_strain): - ax.plot(vertical_strain, volumetric_strain, '-', color='blue', label='Volumetric Strain') - ax.set_title('εᵥ vs εᵧᵧ') - ax.set_xlabel('εᵧᵧ (Vertical Strain) [-]') - ax.set_ylabel('εᵥ (Volumetric Strain) [-]') - ax.grid(True) - ax.invert_xaxis() - ax.invert_yaxis() - ax.locator_params(nbins=8) - -def plot_mohr_coulomb_circle(ax, sigma_1, sigma_3, cohesion=None, friction_angle=None): - if np.isclose(sigma_1, sigma_3): - log_message("σ₁ is equal to σ₃. Mohr circle collapses to a point.", "warn") - center = (sigma_1 + sigma_3) / 2 - radius = (sigma_1 - sigma_3) / 2 - theta = np.linspace(0, np.pi, 200) - sigma = center + radius * np.cos(theta) - tau = -radius * np.sin(theta) - - ax.plot(sigma, tau, label='Mohr-Coulomb', color='blue') - - if cohesion is not None and friction_angle is not None: - phi_rad = np.radians(friction_angle) - x_line = np.linspace(0, sigma_1, 200) - y_line = x_line * np.tan(phi_rad) - cohesion - ax.plot(x_line, -y_line, 'r--', label="Failure Criterion: τ = σ' tan(φ°) + c'") - ax.legend(loc='upper left') - - ax.set_title("Mohr-Coulomb") - ax.set_xlabel("σ' (Effective Stress) [kN/m²]") - ax.set_ylabel("τ (Mobilized Shear Stress) [kN/m²]") - ax.grid(True) - ax.invert_xaxis() - ax.set_xlim(left=0, right= 1.2*np.max(sigma_1)) - ax.set_ylim(bottom=0, top = -0.6*np.max(sigma_1)) - -def plot_p_q(ax, p_list, q_list): - ax.plot(p_list, q_list, '-', color='blue', label="p' vs q") - ax.set_title("p' vs q") - ax.set_xlabel("p' (Mean Effective Stress) [kN/m²]") - ax.set_ylabel("q (Deviatoric Stress) [kN/m²]") - ax.grid(True) - ax.invert_xaxis() - ax.locator_params(nbins=8) diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/ui_builder.py b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_builder.py index 3ed92f268704..ba558e73d659 100644 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/ui_builder.py +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_builder.py @@ -1,26 +1,22 @@ +import os +import math import tkinter as tk from tkinter import ttk, scrolledtext import matplotlib.pyplot as plt from matplotlib.gridspec import GridSpec from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import threading +import traceback -from run_triaxial_simulation import run_triaxial_simulation +from ui_runner import run_gui_builder from ui_logger import init_log_widget, log_message, clear_log from ui_udsm_parser import input_parameters_format_to_unicode -import traceback - - -MAX_STRAIN_LABEL = "Maximum Strain |εᵧᵧ|" -INIT_PRESSURE_LABEL = "Initial effective cell pressure |σ'ₓₓ|" -STRESS_INC_LABEL = "Stress increment |σ'ᵧᵧ|" -NUM_STEPS_LABEL = "Number of steps" -DURATION_LABEL = "Duration" -FL2_UNIT_LABEL = "kN/m²" -SECONDS_UNIT_LABEL = "s" -PERCENTAGE_UNIT_LABEL = "%" -WITHOUT_UNIT_LABEL = "" +from ui_labels import ( + TRIAXIAL, DIRECT_SHEAR, + MAX_STRAIN_LABEL, INIT_PRESSURE_LABEL, STRESS_INC_LABEL, NUM_STEPS_LABEL, DURATION_LABEL, + FL2_UNIT_LABEL, SECONDS_UNIT_LABEL, PERCENTAGE_UNIT_LABEL, WITHOUT_UNIT_LABEL +) class GeotechTestUI: def __init__(self, root, parent_frame, test_name, dll_path, model_dict, external_widgets=None): @@ -33,16 +29,19 @@ def __init__(self, root, parent_frame, test_name, dll_path, model_dict, external self.model_var = tk.StringVar(root) self.model_var.set(model_dict["model_name"][0]) - self.current_test = tk.StringVar(value="Triaxial") + self.current_test = tk.StringVar(value=TRIAXIAL) self._init_frames() - self._init_dropdown_section() - self._init_plot_canvas() - self._create_input_fields() + + self.plot_frame = ttk.Frame(self.parent, padding="5", width=800, height=600) + self.plot_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5) self.is_running = False self.external_widgets = external_widgets if external_widgets else [] + self._init_dropdown_section() + self._create_input_fields() + def _start_simulation_thread(self): if self.is_running: return @@ -67,17 +66,18 @@ def _init_frames(self): self.log_frame = ttk.Frame(self.left_frame, padding="5") self.log_frame.pack(fill="x", padx=10, pady=(0, 10)) - def _init_plot_canvas(self): - self.plot_frame = ttk.Frame(self.parent, padding="5", width=800, height=600) - self.plot_frame.pack(side="right", fill="both", expand=True, padx=5, pady=5) + def _init_plot_canvas(self, num_plots): + self._destroy_existing_plot_canvas() - self.fig = plt.figure(figsize=(12, 15)) - self.gs = GridSpec(3, 2, figure=self.fig, wspace=0.4, hspace=0.6) - self.axes = [self.fig.add_subplot(self.gs[i]) for i in range(5)] + self.fig = plt.figure(figsize=(12, 8), dpi=100) + rows = math.ceil(math.sqrt(num_plots)) + cols = math.ceil(num_plots / rows) + + self.gs = GridSpec(rows, cols, figure=self.fig, wspace=0.4, hspace=0.6) + self.axes = [self.fig.add_subplot(self.gs[i]) for i in range(num_plots)] self.canvas = FigureCanvasTkAgg(self.fig, master=self.plot_frame) + self.canvas.draw() self.canvas.get_tk_widget().pack(fill="both", expand=True) - if len(self.fig.axes) == 6: - self.fig.delaxes(self.fig.axes[5]) def _init_dropdown_section(self): ttk.Label(self.dropdown_frame, text="Select a Model:", font=("Arial", 12, "bold")).pack(anchor="w", padx=5, pady=5) @@ -108,14 +108,7 @@ def _create_input_fields(self): params = self.model_dict["param_names"][index] units = self.model_dict.get("param_units", [[]])[index] - raw_defaults = { - "1. E": "10000", "2. n_ur": "0.3", "3. c'": "0.0", "4. f_peak": "30.0", - "5. y_peak": "0.0", "6. s_t, cut-off": "0.0", "7. yield function (MC=1 DP=2 MNC=3 MN=4)": "1", - "8. n_un (UMAT)": "0.3", "YOUNG_MODULUS": "10000", "POISSON_RATIO": "0.3" - } - default_values = { - input_parameters_format_to_unicode(k): v for k, v in raw_defaults.items() - } + default_values = {} self.entry_widgets = self._create_entries(self.param_frame, "Soil Input Parameters", params, units, default_values) self.mohr_checkbox = tk.BooleanVar() @@ -129,13 +122,20 @@ def _create_input_fields(self): self.test_selector_frame.pack(fill="x", pady=(10, 5)) self.test_buttons = {} - for test_name in ["Triaxial", "Oedometer", "Direct Shear"]: + self.test_images = { + TRIAXIAL: tk.PhotoImage(file=os.path.join(os.path.dirname(__file__), "assets", "triaxial.png")), + DIRECT_SHEAR: tk.PhotoImage(file=os.path.join(os.path.dirname(__file__), "assets", "direct_shear.png")) + } + + for test_name in [TRIAXIAL, DIRECT_SHEAR]: btn = tk.Button( self.test_selector_frame, text=test_name, - font=("Arial", 8, "bold"), - width=10, - height=10, + image=self.test_images[test_name], + compound="top", + font=("Arial", 10, "bold"), + width=100, + height=100, relief="raised", command=lambda name=test_name: self._switch_test(name) ) @@ -145,7 +145,7 @@ def _create_input_fields(self): self.test_input_frame = ttk.Frame(self.param_frame, padding="10") self.test_input_frame.pack(fill="both", expand=True) - self._switch_test("Triaxial") + self._switch_test(TRIAXIAL) self.run_button = ttk.Button(self.button_frame, text="Run Calculation", command=self._start_simulation_thread) self.run_button.pack(pady=5) @@ -201,84 +201,99 @@ def _switch_test(self, test_name): for name, button in self.test_buttons.items(): if name == test_name: - button.config(relief="sunken", bg="#d9d9d9", state="disabled") + button.config(relief="sunken", bg="SystemButtonFace", state="normal") else: button.config(relief="raised", bg="SystemButtonFace", state="normal") for w in self.test_input_frame.winfo_children(): w.destroy() - if test_name == "Triaxial": + if test_name == TRIAXIAL: + self._init_plot_canvas(num_plots=5) + ttk.Label(self.test_input_frame, text="Triaxial Input Data", font=("Arial", 12, "bold")).pack(anchor="w", padx=5, pady=(5, 0)) + self._add_test_type_dropdown(self.test_input_frame) self.triaxial_widgets = self._create_entries( self.test_input_frame, - "Triaxial Input Data", + "", [INIT_PRESSURE_LABEL, MAX_STRAIN_LABEL, NUM_STEPS_LABEL, DURATION_LABEL], [FL2_UNIT_LABEL, PERCENTAGE_UNIT_LABEL, WITHOUT_UNIT_LABEL, SECONDS_UNIT_LABEL], - {INIT_PRESSURE_LABEL: "100", MAX_STRAIN_LABEL: "10", + {INIT_PRESSURE_LABEL: "100", MAX_STRAIN_LABEL: "20", NUM_STEPS_LABEL: "100", DURATION_LABEL: "1.0"} ) - elif test_name == "Oedometer": - self.oedometer_widgets = self._create_entries( - self.test_input_frame, - "Oedometer Input Data", - [DURATION_LABEL, STRESS_INC_LABEL, NUM_STEPS_LABEL], - [FL2_UNIT_LABEL, FL2_UNIT_LABEL, WITHOUT_UNIT_LABEL], - {DURATION_LABEL: "1.0", STRESS_INC_LABEL: "100", NUM_STEPS_LABEL: "100"} - ) - elif test_name == "Direct Shear": + + elif test_name == DIRECT_SHEAR: + self._init_plot_canvas(num_plots=4) + ttk.Label(self.test_input_frame, text="Direct Simple Shear Input Data", font=("Arial", 12, "bold")).pack(anchor="w", padx=5, pady=(5, 0)) + self._add_test_type_dropdown(self.test_input_frame) self.shear_widgets = self._create_entries( self.test_input_frame, - "Direct Shear Input Data", + "", [INIT_PRESSURE_LABEL, MAX_STRAIN_LABEL, NUM_STEPS_LABEL, DURATION_LABEL], - [FL2_UNIT_LABEL, PERCENTAGE_UNIT_LABEL, "", SECONDS_UNIT_LABEL], - {INIT_PRESSURE_LABEL: "100", MAX_STRAIN_LABEL: "10", + [FL2_UNIT_LABEL, PERCENTAGE_UNIT_LABEL, WITHOUT_UNIT_LABEL, SECONDS_UNIT_LABEL], + {INIT_PRESSURE_LABEL: "100", MAX_STRAIN_LABEL: "20", NUM_STEPS_LABEL: "100", DURATION_LABEL: "1.0"} ) log_message(f"{test_name} test selected.", "info") + + def _add_test_type_dropdown(self, parent): + ttk.Label(parent, text="Type of Test:", font=("Arial", 10, "bold")).pack(anchor="w", padx=5, pady=(5, 2)) + + self.test_type_var = tk.StringVar(value="Drained") + self.test_type_menu = ttk.Combobox( + parent, + textvariable=self.test_type_var, + values=["Drained"], + state="readonly", + width=12 + ) + self.test_type_menu.pack(anchor="w", padx=10, pady=(0, 10)) + + self.test_type_menu.bind("<>") + def _run_simulation(self): - clear_log() try: log_message("Starting calculation... Please wait...", "info") log_message("Validating input...", "info") self.root.update_idletasks() - umat_params = [e.get() for e in self.entry_widgets.values()] - - if self.current_test.get() != "Triaxial": - raise NotImplementedError(f"{self.current_test.get()} simulation not yet implemented.") - - eps_max = float(self.triaxial_widgets[MAX_STRAIN_LABEL].get()) - sigma_init = float(self.triaxial_widgets[INIT_PRESSURE_LABEL].get()) - n_steps = float(self.triaxial_widgets[NUM_STEPS_LABEL].get()) - duration = float(self.triaxial_widgets[DURATION_LABEL].get()) - - if any(val <= 0 for val in [eps_max, n_steps, duration]) or sigma_init < 0: - raise ValueError("All values must be positive and non-zero.") + material_params = [e.get() for e in self.entry_widgets.values()] cohesion_phi_indices = None if not self.is_linear_elastic and self.mohr_checkbox.get(): cohesion_phi_indices = (int(self.cohesion_var.get()), int(self.phi_var.get())) - index = self.model_dict["model_name"].index(self.model_var.get()) + 1 if self.dll_path else -2 + index = self.model_dict["model_name"].index(self.model_var.get()) + 1 if self.dll_path else None + test_type = self.current_test.get() log_message("Calculating...", "info") self.root.update_idletasks() - run_triaxial_simulation( - dll_path=self.dll_path or "", - index=index, - umat_parameters=[float(x) for x in umat_params], - num_steps=n_steps, - end_time=duration, - maximum_strain=eps_max, - initial_effective_cell_pressure=sigma_init, - cohesion_phi_indices=cohesion_phi_indices, - axes=self.axes - ) + if test_type == TRIAXIAL: + run_gui_builder( + test_type="triaxial", + dll_path=self.dll_path or "", + index=index, + material_parameters=[float(x) for x in material_params], + input_widgets=self.triaxial_widgets, + cohesion_phi_indices=cohesion_phi_indices, + axes=self.axes + ) + + elif test_type == DIRECT_SHEAR: + run_gui_builder( + test_type="direct_shear", + dll_path=self.dll_path or "", + index=index, + material_parameters=[float(x) for x in material_params], + input_widgets=self.shear_widgets, + cohesion_phi_indices=cohesion_phi_indices, + axes=self.axes + ) + self.canvas.draw() - log_message("Simulation completed successfully.", "info") + log_message(f"{test_type} test completed successfully.", "info") except Exception: log_message("An error occurred during simulation:", "error") @@ -316,3 +331,11 @@ def _enable_gui(self): self.model_menu.configure(state="disabled") else: self.model_menu.configure(state="readonly") + + def _destroy_existing_plot_canvas(self): + if hasattr(self, "plot_frame") and self.plot_frame.winfo_exists(): + for widget in self.plot_frame.winfo_children(): + widget.destroy() + self.fig = None + self.canvas = None + self.axes = [] diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/ui_labels.py b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_labels.py new file mode 100644 index 000000000000..ae0895512352 --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_labels.py @@ -0,0 +1,59 @@ +#-----------------------UI labels--------------------------- + +# Application data +APP_TITLE = "Deltares Soil Element Test Suite" +APP_VERSION = "Version 0.1.0 ~ Alpha Release" +APP_NAME = "SoilElementSuite" +APP_AUTHOR = "Deltares" + +# General test types +TRIAXIAL = "Triaxial" +DIRECT_SHEAR= "Direct Shear" + +# Input labels +MAX_STRAIN_LABEL = "Maximum Strain |εᵧᵧ|" +INIT_PRESSURE_LABEL = "Initial effective cell pressure |σ'ₓₓ|" +STRESS_INC_LABEL = "Stress increment |σ'ᵧᵧ|" +NUM_STEPS_LABEL = "Number of steps" +DURATION_LABEL = "Duration" + +# Units +FL2_UNIT_LABEL = "kN/m²" +SECONDS_UNIT_LABEL = "s" +PERCENTAGE_UNIT_LABEL = "%" +WITHOUT_UNIT_LABEL = "" + +# Menu labels +SELECT_UDSM = "Select UDSM File" +LINEAR_ELASTIC = "Linear Elastic Model" + + +#-----------------------Plot labels-------------------------- + +# Axis labels +SIGMA1_LABEL = "σ₁ (Principal Stress 1) [kN/m²]" +SIGMA3_LABEL = "σ₃ (Principal Stress 3) [kN/m²]" +SIGMA1_SIGMA3_DIFF_LABEL = "|σ₁ - σ₃| [kN/m²]" +VERTICAL_STRAIN_LABEL = "εᵧᵧ (Vertical Strain) [-]" +VOLUMETRIC_STRAIN_LABEL = "εᵥ (Volumetric Strain) [-]" +SHEAR_STRAIN_LABEL = "γₓᵧ (Shear Strain) [-]" +SHEAR_STRESS_LABEL = "τₓᵧ (Shear Stress) [kN/m²]" +EFFECTIVE_STRESS_LABEL = "σ' (Effective Stress) [kN/m²]" +MOBILIZED_SHEAR_STRESS_LABEL = "τ (Mobilized Shear Stress) [kN/m²]" +P_STRESS_LABEL = "p' (Mean Effective Stress) [kN/m²]" +Q_STRESS_LABEL = "q (Deviatoric Stress) [kN/m²]" + +# Titles +TITLE_SIGMA1_VS_SIGMA3 = "σ₁ vs σ₃" +TITLE_DIFF_PRINCIPAL_SIGMA_VS_STRAIN = "|σ₁ - σ₃| vs εᵧᵧ" +TITLE_VOL_VS_VERT_STRAIN = "εᵥ vs εᵧᵧ" +TITLE_MOHR = "Mohr-Coulomb" +TITLE_P_VS_Q = "p' vs q" +TITLE_SHEAR_VS_STRAIN = "τₓᵧ vs εₓᵧ" + +# Legends +LEGEND_MC = "Mohr-Coulomb" +LEGEND_MC_FAILURE = "Failure Criterion: τ = σ' tan(φ°) + c'" + +# Font labels +FONT_SEGOE_UI = "Segoe UI" \ No newline at end of file diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/ui_menu.py b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_menu.py index 088baf16bb9b..3448af8ae644 100644 --- a/applications/GeoMechanicsApplication/python_scripts/element_test/ui_menu.py +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_menu.py @@ -1,16 +1,260 @@ import os import tkinter as tk -from tkinter import filedialog, messagebox, ttk +from tkinter import filedialog, messagebox, ttk, scrolledtext, Menu +from platformdirs import user_data_dir +from pathlib import Path from ui_builder import GeotechTestUI from ui_udsm_parser import udsm_parser +from ui_labels import APP_TITLE, APP_VERSION, APP_NAME, APP_AUTHOR, SELECT_UDSM, LINEAR_ELASTIC, FONT_SEGOE_UI +import ctypes +ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("deltares.ElementTestSuite.ui") + +data_dir = Path(user_data_dir(APP_NAME, APP_AUTHOR)) +data_dir.mkdir(parents=True, exist_ok=True) + +LICENSE_FLAG_PATH = data_dir / "license_accepted.flag" + + +def show_license_agreement(): + license_text = """ + Pre-Release Software Licensing Agreement for testing of Pre-Release Software + ---------------------------------------------------------------------------- + + THE PARTIES + 1. STICHTING DELTARES, having its registered office and place of business in + Delft (The Netherlands) at Boussinesqweg 1, listed in the Commercial Register under + number 41146461, hereinafter called “Deltares “, in this present matter legally + represented by C. van den Kieboom, Sales Officer Deltares Software Centre; and + 2. The end user, hereinafter called "Licensee" + + WHEREAS: + • Deltares owns the intellectual property of the computer program being developed by + Deltares, as described below, hereinafter referred to as “Pre-Release Software”; + • The Pre-Release Software is under development by Deltares and is not fit for any use + besides internal review, testing and evaluation; + • Licensee wishes to acquire a non-exclusive and non-transferable license, without the + right of sub-licensing, to use internally within the organization of Licensee, the + PreRelease Software for review, testing and evaluation of functionality of the Pre-Release + Software; + • Deltares grants Licensee a Pre Release Software Licensing Agreement for review, + testing and evaluation of Pre Release Software under the following conditions. + + AGREE AS FOLLOWS: + + Article 1 Definitions + --------------------- + Agreement This agreement, including the Appendices. + Pre Release Software The computer program or the computer programs under development as + described below under “Description Pre Release Software” and the Documentation and later + versions of said computer program and Documentation. + Documentation The manual or manuals and other documents that correspond to the + Pre Release Software. + + Article 2 License + ----------------- + Deltares grants Licensee until 1st of November 2022 starting at 1st of July 2022 and + without a monetary charge, a non-exclusive, non-transferable, non sub-licensable license + to use for the purpose and with the limitations specified in Article 3, the + Pre Release Software, as described below under “Description Pre Release Software” + for the purpose of research + + Article 3 Use of Pre Release Software and the Documentation + ----------------------------------------------------------- + 1. Licensee shall only be authorised to use the Pre Release Software within its own + organisation, for its own use and for review, testing and evaluation of functionality of + the Pre-Release Software. Licensee shall not be permitted to make any other use of + Pre Release Software, use the software for levee assignment or design, or to make + available or grant access to Pre Release Software to any third party, subject to article 5 + paragraph 1 under b. + 2. Licensee shall not be authorised to modify and/or adjust Pre Release Software and/or + to (otherwise) carry out alterations to it and/or to integrate Pre Release Software in + other software, unless and only in so far as Licensee has obtained express written + permission to that effect in advance from Deltares. + 3. Licensee shall not be authorised to (have others) copy Pre Release Software in any + manner whatsoever or to (have others) multiply it (in any other way), except for backup + purposes. + + Article 4 Intellectual Property Rights, Ownership + ------------------------------------------------- + Deltares owns the copyright to Pre Release Software. With the Agreement Deltares only + grants Licensee the license rights in connection with Pre Release Software as described + in Article 2. Licensee accepts that with this license granted no transfer of ownership + whatsoever, including the transfer of the intellectual property rights, is made + to Licensee. + + Article 5 Confidentiality + ------------------------- + 1. Licensee shall keep confidential Pre Release Software which Licensee has obtained + and/or obtains, in any manner, from Deltares under or in connection with the + Agreement. + This obligation shall at any rate include: + a. Treating of Pre Release Software confidentially; + b. releasing Pre Release Software solely to those employees of Licensees or a third party + acting on behalf of Licensee under the conditions of this Agreement who require access to + Pre Release Software, whereby Licensee will oblige these employees and third parties to + the same confidentiality as Licensee; + c. the non-disclosure of information and/or data related to the Agreement to third parties + and/or refraining from making such information and/or data public in any other way without + the prior express and written consent of Deltares, to be obtained for each separate event. + d. using information and/or data obtained solely for the purposes for which they were + obtained. + 2. Licensee's obligation of confidentiality referred to in Article 5.1 shall not apply to + information and/or data that were already at Licensee's free disposal, or were part of + the public domain, or were already included in generally accessible literature at the + time when they were obtained by Licensee, or that were obtained by Licensee from a + third party or third parties who was or were free to disclose the relevant information + and/or data and who had not obtained the information and/or data from Deltares. + 3. The provisions in this article and article 8 shall remain in full force after + termination of + the Agreement, as set forth in Article 7, as well. + + Article 6 No guarantee, no warrantee + ------------------------------------ + The Pre -Release Software is under development by Deltares. Licensee acknowledges and + agrees that the Pre Release Software may contain bugs, defects, errors and may not be + expected to function fully upon installation nor that results obtained with the + Pre Release Software are correct or of proper quality. Licensee also acknowledges + that Deltares is under no obligation to correct any bugs, defects, or errors in + the Pre Release Software or to otherwise support or maintain the Pre Release Software. + + Article 7 Duration, Termination + ------------------------------- + 1. The Agreement concluded for a period until 1st of November 2022 starting at + 1st of July 2022, subject to termination in accordance with the provisions of + article 7.2. + 2. Parties are entitled without cause being required, to terminate the Agreement in + writing with immediate effect, without judicial intervention being required. + 3. In the event of termination of the Agreement, Licensee shall immediately return to + Deltares all copies of Pre Release Software. + 4. In addition, in the event of termination of the Agreement, Licensee shall furthermore + immediately cease using (additional copies of) Pre Release Software and delete Pre + Release Software from (all) their computer(s). + 5. "In writing" as mentioned in this article shall also be a fax message but not an e-mail + message. + + Article 8 Liability + ------------------- + 1. Licensee agrees that Deltares (including its personnel and non-employees who (have) + undertake(n) activities for Deltares) shall not be responsible to Licensee for any + loss-of, direct, indirect, incidental, special or consequential damages arising out + of the license agreement or the use of Pre Release Software, to the extent permitted by + Netherlands law. + 2. Licensee shall indemnify, hold harmless and defend Deltares against any action + brought by a third party against Deltares to the extent that such a claim is connected to + the use of Pre Release Software by Licensee and/or third parties at whose disposal the + Licensee has placed Pre Release Software in accordance with this Agreement and/or + these results or to whom he has otherwise made them known the results, including use + of the results of use by Licensee and/or third parties and the installation of the Pre + Release Software by Licensee. + + Article 9 Other provisions + -------------------------- + 1. Changes in and/or deviations to the Agreement are valid only if they are explicitly + agreed between the parties in writing. + 2. The parties are not allowed to assign any rights and/or obligations under the + Agreement, entirely or in part, to third parties without the prior written consent of the + other party. + 3. Any disputes arising from the Agreement or from agreements arising there from, shall + be submitted solely to the competent court of The Hague. + 4. This Agreement and all the agreements arising there from are governed exclusively by + Netherlands law + """ + + license_window = tk.Toplevel() + license_window.title("Pre-Release License Agreement") + license_window.geometry("800x600") + license_window.grab_set() + license_window.protocol("WM_DELETE_WINDOW", lambda: os._exit(0)) + + tk.Label(license_window, text="Please review and accept the agreement to continue.", + font=("Arial", 12, "bold"), pady=10).pack() + + text_area = scrolledtext.ScrolledText(license_window, wrap="word", font=("Courier", 10)) + text_area.insert("1.0", license_text) + text_area.config(state="disabled") + text_area.pack(expand=True, fill="both", padx=10, pady=10) + + button_frame = tk.Frame(license_window) + button_frame.pack(pady=10) + + def accept(): + try: + with open(LICENSE_FLAG_PATH, "w") as f: + f.write("ACCEPTED") + except Exception as e: + messagebox.showerror("Error", f"Could not save license acceptance: {e}") + os._exit(1) + license_window.destroy() + + def decline(): + messagebox.showinfo("Exit", "You must accept the license agreement to use this software.") + os._exit(0) + + tk.Button(button_frame, text="Accept", width=15, command=accept).pack(side="left", padx=10) + tk.Button(button_frame, text="Decline", width=15, command=decline).pack(side="right", padx=10) + +def show_about_window(): + about_win = tk.Toplevel() + about_win.title("About") + about_win.geometry("500x400") + about_win.resizable(False, False) + about_win.grab_set() + + tk.Label(about_win, text=APP_TITLE, font=(FONT_SEGOE_UI, 14, "bold")).pack(pady=(20, 5)) + tk.Label(about_win, text=APP_VERSION, font=(FONT_SEGOE_UI, 12)).pack(pady=(0, 5)) + tk.Label(about_win, text="Powered by:", font=(FONT_SEGOE_UI, 12)).pack(pady=(0, 5)) + + image_frame = tk.Frame(about_win) + image_frame.pack(pady=10) + + try: + path1 = os.path.join(os.path.dirname(__file__), "assets", "kratos.png") + path2 = os.path.join(os.path.dirname(__file__), "assets", "deltares.png") + + photo1 = tk.PhotoImage(file=path1) + photo2 = tk.PhotoImage(file=path2) + + label1 = tk.Label(image_frame, image=photo1) + label1.image = photo1 + label1.pack(pady=2) + + label2 = tk.Label(image_frame, image=photo2) + label2.image = photo2 + label2.pack(pady=15) + + except Exception: + tk.Label(about_win, text="[One or both images could not be loaded]", fg="red").pack() + + tk.Label(about_win, text="Contact: kratos@deltares.nl", font=(FONT_SEGOE_UI, 12)).pack(pady=(0, 2)) + tk.Button(about_win, text="Close", command=about_win.destroy).pack(pady=10) -SELECT_DLL = "Select DLL File" -LINEAR_ELASTIC = "Linear Elastic Model" def create_menu(): root = tk.Tk() - root.title("Triaxial Test") + + menubar = Menu(root) + root.config(menu=menubar) + + file_menu = Menu(menubar, tearoff=0) + file_menu.add_command(label="Exit", command=lambda: root.quit()) + menubar.add_cascade(label="File", menu=file_menu) + + about_menu = Menu(menubar, tearoff=0) + about_menu.add_command(label="License", command=show_license_agreement) + about_menu.add_command(label="About", command=show_about_window) + menubar.add_cascade(label="Help", menu=about_menu) + + if not os.path.exists(LICENSE_FLAG_PATH): + show_license_agreement() + + try: + icon_path = os.path.join(os.path.dirname(__file__), "assets", "icon.ico") + root.iconbitmap(default=icon_path) + except Exception as e: + print(f"Could not set icon: {e}") + + root.title(f"{APP_TITLE} - {APP_VERSION}") root.state('zoomed') root.resizable(True, True) @@ -21,7 +265,7 @@ def create_menu(): main_frame.pack(side="top", fill="both", expand=True) def load_dll(): - dll_path = filedialog.askopenfilename(title=SELECT_DLL, filetypes=[("DLL files", "*.dll")]) + dll_path = filedialog.askopenfilename(title=SELECT_UDSM, filetypes=[("DLL files", "*.dll")]) if not dll_path: messagebox.showerror("Error", "No DLL file selected.") return @@ -54,7 +298,7 @@ def load_linear_elastic(): def handle_model_source_selection(event): choice = model_source_var.get() - if choice == SELECT_DLL: + if choice == SELECT_UDSM: load_dll() elif choice == LINEAR_ELASTIC: load_linear_elastic() @@ -63,7 +307,7 @@ def handle_model_source_selection(event): model_source_menu = ttk.Combobox( top_frame, textvariable=model_source_var, - values=[SELECT_DLL, LINEAR_ELASTIC], + values=[SELECT_UDSM, LINEAR_ELASTIC], state="readonly" ) model_source_menu.bind("<>", handle_model_source_selection) diff --git a/applications/GeoMechanicsApplication/python_scripts/element_test/ui_runner.py b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_runner.py new file mode 100644 index 000000000000..d9d7c1f08411 --- /dev/null +++ b/applications/GeoMechanicsApplication/python_scripts/element_test/ui_runner.py @@ -0,0 +1,31 @@ +import traceback +from ui_logger import log_message +from run_simulation import run_simulation + +def run_gui_builder(test_type, dll_path, index, material_parameters, input_widgets, cohesion_phi_indices, axes): + try: + + sigma_init = float(input_widgets["Initial effective cell pressure |σ'ₓₓ|"].get()) + eps_max = float(input_widgets["Maximum Strain |εᵧᵧ|"].get()) + n_steps = float(input_widgets["Number of steps"].get()) + duration = float(input_widgets["Duration"].get()) + + if any(val <= 0 for val in [eps_max, n_steps, duration]) or sigma_init < 0: + raise ValueError("All values must be positive and non-zero.") + + run_simulation( + test_type=test_type, + dll_path=dll_path or "", + index=index, + material_parameters=material_parameters, + num_steps=n_steps, + end_time=duration, + maximum_strain=eps_max, + initial_effective_cell_pressure=sigma_init, + cohesion_phi_indices=cohesion_phi_indices, + axes=axes + ) + + except Exception: + log_message("Error during simulation:", "error") + log_message(traceback.format_exc(), "error")