0% found this document useful (0 votes)
4 views56 pages

Fixed Geotechnical Report App

The document outlines a Python application for generating geotechnical reports using Tkinter for the GUI and ReportLab for PDF generation. It includes features for creating and managing project information, soil profiles, and test results, with a user-friendly interface and data visualization capabilities. The application is structured with tabs for different functionalities and includes error handling and user feedback mechanisms.

Uploaded by

koewan921
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
4 views56 pages

Fixed Geotechnical Report App

The document outlines a Python application for generating geotechnical reports using Tkinter for the GUI and ReportLab for PDF generation. It includes features for creating and managing project information, soil profiles, and test results, with a user-friendly interface and data visualization capabilities. The application is structured with tabs for different functionalities and includes error handling and user feedback mechanisms.

Uploaded by

koewan921
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 56

import tkinter as tk

from tkinter import ttk, messagebox, filedialog


from ttkthemes import ThemedTk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
from reportlab.lib import colors
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
import numpy as np
import datetime
import os

class GeotechnicalReportApp:
def __init__(self, root):
self.root = root
self.root.title("Geotechnical Report Generator")
self.root.geometry("1000x700")
self.root.minsize(900, 600)

# Color scheme
self.PRIMARY_COLOR = "#2c3e50" # Dark blue
self.SECONDARY_COLOR = "#3498db" # Light blue
self.ACCENT_COLOR = "#e74c3c" # Red
self.BACKGROUND_COLOR = "#ecf0f1" # Light gray
self.TEXT_COLOR = "#2c3e50" # Dark blue

# Data storage
self.project_data = {}
self.soil_layers = []
self.test_results = {}

# Initialize styles
self.style = ttk.Style()
self.style.configure('TFrame', background=self.BACKGROUND_COLOR)
self.style.configure('TLabel', background=self.BACKGROUND_COLOR,
foreground=self.TEXT_COLOR)
self.style.configure('TButton', background=self.SECONDARY_COLOR,
foreground=self.TEXT_COLOR)
self.style.configure('Accent.TButton', background=self.ACCENT_COLOR,
foreground='white')
self.style.configure('TNotebook', background=self.BACKGROUND_COLOR)
# Setup main menu
self.show_main_menu()

def show_main_menu(self):
# Clear the window
for widget in self.root.winfo_children():
widget.destroy()

# Create main frame


main_frame = ttk.Frame(self.root, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)

# Title
title_label = ttk.Label(main_frame, text="Geotechnical Report Generator",
font=("Arial", 24, "bold"), foreground=self.PRIMARY_COLOR)
title_label.pack(pady=30)

# Create menu buttons frame


button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=20)

# Menu buttons
btn_width = 25
btn_pady = 10

new_project_btn = ttk.Button(button_frame, text="Create New Project",


command=self.create_new_project, width=btn_width)
new_project_btn.pack(pady=btn_pady)

load_project_btn = ttk.Button(button_frame, text="Load Existing Project",


command=self.load_project, width=btn_width)
load_project_btn.pack(pady=btn_pady)

about_btn = ttk.Button(button_frame, text="About",


command=self.show_about, width=btn_width)
about_btn.pack(pady=btn_pady)

exit_btn = ttk.Button(button_frame, text="Exit",


command=self.root.quit, width=btn_width)
exit_btn.pack(pady=btn_pady)

# Footer
footer_frame = ttk.Frame(main_frame)
footer_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10)

footer_text = ttk.Label(footer_frame,
text="© 2025 Geotechnical Engineering Solutions",
font=("Arial", 10))
footer_text.pack(side=tk.RIGHT)

def load_project(self):
"""Load an existing project from a file"""
messagebox.showinfo("Load Project", "Project loading functionality will be implemented in
a future version.")
# In a complete implementation, this would open a file dialog to select a project file
# and load the data into the application

def show_about(self):
"""Display information about the application"""
about_window = tk.Toplevel(self.root)
about_window.title("About Geotechnical Report Generator")
about_window.geometry("400x300")
about_window.resizable(False, False)

frame = ttk.Frame(about_window, padding="20")


frame.pack(fill=tk.BOTH, expand=True)

ttk.Label(frame, text="Geotechnical Report Generator",


font=("Arial", 14, "bold")).pack(pady=10)
ttk.Label(frame, text="Version 1.0").pack()
ttk.Label(frame, text="© 2025 Geotechnical Engineering Solutions").pack(pady=5)
ttk.Label(frame, text="A professional tool for creating detailed geotechnical
reports.").pack(pady=10)

ttk.Button(frame, text="Close", command=about_window.destroy).pack(pady=10)

def create_new_project(self):
# Clear the window
for widget in self.root.winfo_children():
widget.destroy()

# Create main frame with notebook for tabs


main_frame = ttk.Frame(self.root, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)

# Header with back button


header_frame = ttk.Frame(main_frame)
header_frame.pack(fill=tk.X, pady=5)

back_btn = ttk.Button(header_frame, text="← Back to Menu",


command=self.show_main_menu)
back_btn.pack(side=tk.LEFT)

# Create tabs
self.notebook = ttk.Notebook(main_frame)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=10)

# Create all the tabs we need


self.project_info_tab = ttk.Frame(self.notebook)
self.soil_profile_tab = ttk.Frame(self.notebook)
self.test_results_tab = ttk.Frame(self.notebook)
self.analysis_tab = ttk.Frame(self.notebook)
self.report_tab = ttk.Frame(self.notebook)

# Add tabs to notebook


self.notebook.add(self.project_info_tab, text="Project Info")
self.notebook.add(self.soil_profile_tab, text="Soil Profile")
self.notebook.add(self.test_results_tab, text="Test Results")
self.notebook.add(self.analysis_tab, text="Analysis")
self.notebook.add(self.report_tab, text="Report")

# Set up the content for each tab


self.setup_project_info_tab()
self.setup_soil_profile_tab()
self.setup_test_results_tab()
self.setup_analysis_tab()
self.setup_report_tab()

def setup_project_info_tab(self):
# Create frame with padding
frame = ttk.Frame(self.project_info_tab, padding="20")
frame.pack(fill=tk.BOTH, expand=True)

# Title
title = ttk.Label(frame, text="Project Information", font=("Arial", 16, "bold"))
title.grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=10)

# Form fields
fields = [
("Project Name:", "project_name"),
("Client:", "client"),
("Location:", "location"),
("Project Number:", "project_number"),
("Date:", "project_date"),
("Engineer:", "engineer"),
("Project Description:", "description")
]

self.project_entries = {}

for i, (label_text, field_name) in enumerate(fields):


label = ttk.Label(frame, text=label_text)
label.grid(row=i+1, column=0, sticky=tk.W, pady=5)

# Create either a text box or a text field based on field name


if field_name == "description":
entry = tk.Text(frame, height=5, width=40)
entry.grid(row=i+1, column=1, sticky=tk.W, pady=5)
else:
entry = ttk.Entry(frame, width=40)
entry.grid(row=i+1, column=1, sticky=tk.W, pady=5)

self.project_entries[field_name] = entry

# Add date picker default value (today's date)


today = datetime.date.today().strftime("%Y-%m-%d")
self.project_entries["project_date"].insert(0, today)

# Save button
save_btn = ttk.Button(frame, text="Save Project Information",
command=self.save_project_info)
save_btn.grid(row=len(fields)+1, column=1, sticky=tk.E, pady=20)

def save_project_info(self):
"""Save project information from the form"""
try:
# Get data from text fields and entries
self.project_data["project_name"] = self.project_entries["project_name"].get()
self.project_data["client"] = self.project_entries["client"].get()
self.project_data["location"] = self.project_entries["location"].get()
self.project_data["project_number"] = self.project_entries["project_number"].get()
self.project_data["project_date"] = self.project_entries["project_date"].get()
self.project_data["engineer"] = self.project_entries["engineer"].get()
self.project_data["description"] = self.project_entries["description"].get("1.0",
tk.END).strip()
messagebox.showinfo("Success", "Project information saved successfully!")

# Move to next tab


self.notebook.select(1) # Go to soil profile tab
except Exception as e:
messagebox.showerror("Error", f"Failed to save project information: {str(e)}")

def setup_soil_profile_tab(self):
# Create frame with padding
frame = ttk.Frame(self.soil_profile_tab, padding="20")
frame.pack(fill=tk.BOTH, expand=True)

# Title
title = ttk.Label(frame, text="Soil Profile", font=("Arial", 16, "bold"))
title.grid(row=0, column=0, columnspan=4, sticky=tk.W, pady=10)

# Left frame for inputs


input_frame = ttk.Frame(frame, padding="10")
input_frame.grid(row=1, column=0, sticky=tk.NW)

# Soil layer input fields


fields = [
("Layer Depth (m):", "depth"),
("Thickness (m):", "thickness"),
("Soil Type:", "soil_type"),
("Color:", "color"),
("Moisture Content (%):", "moisture"),
("Unit Weight (kN/m³):", "unit_weight"),
("SPT N-Value:", "spt_n"),
("Description:", "description")
]

self.soil_entries = {}

for i, (label_text, field_name) in enumerate(fields):


label = ttk.Label(input_frame, text=label_text)
label.grid(row=i, column=0, sticky=tk.W, pady=5)

if field_name == "description":
entry = tk.Text(input_frame, height=3, width=30)
else:
entry = ttk.Entry(input_frame, width=30)
entry.grid(row=i, column=1, sticky=tk.W, pady=5)
self.soil_entries[field_name] = entry

# Soil type dropdown


soil_types = ["Clay", "Silt", "Sand", "Gravel", "Organic", "Fill", "Rock"]
self.soil_entries["soil_type"] = ttk.Combobox(input_frame, values=soil_types, width=27)
self.soil_entries["soil_type"].grid(row=2, column=1, sticky=tk.W, pady=5)

# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.grid(row=len(fields), column=0, columnspan=2, pady=10)

add_btn = ttk.Button(btn_frame, text="Add Layer", command=self.add_soil_layer)


add_btn.grid(row=0, column=0, padx=5)

update_btn = ttk.Button(btn_frame, text="Update Selected",


command=self.update_soil_layer)
update_btn.grid(row=0, column=1, padx=5)

delete_btn = ttk.Button(btn_frame, text="Delete Selected",


command=self.delete_soil_layer)
delete_btn.grid(row=0, column=2, padx=5)

# Right frame for soil layer display


display_frame = ttk.Frame(frame, padding="10")
display_frame.grid(row=1, column=1, sticky=tk.NE, padx=20)

# Treeview for soil layers


tree_columns = ("depth", "thickness", "soil_type", "unit_weight", "spt_n")
tree_headers = ("Depth (m)", "Thickness (m)", "Soil Type", "Unit Weight (kN/m³)", "SPT N")

self.soil_tree = ttk.Treeview(display_frame, columns=tree_columns, show="headings",


height=15)

for col, header in zip(tree_columns, tree_headers):


self.soil_tree.heading(col, text=header)
self.soil_tree.column(col, width=100)

self.soil_tree.pack(side=tk.LEFT, fill=tk.BOTH)

# Scrollbar
scrollbar = ttk.Scrollbar(display_frame, orient=tk.VERTICAL,
command=self.soil_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.soil_tree.configure(yscrollcommand=scrollbar.set)

# Bind selection event


self.soil_tree.bind("<<TreeviewSelect>>", self.on_soil_layer_select)

# Visualization frame (below inputs and table)


viz_frame = ttk.Frame(frame, padding="10")
viz_frame.grid(row=2, column=0, columnspan=2, sticky=tk.EW, pady=10)

# Canvas for soil profile visualization (will be populated once layers are added)
viz_label = ttk.Label(viz_frame, text="Soil Profile Visualization:")
viz_label.pack(anchor=tk.W)

self.viz_figure = plt.Figure(figsize=(8, 6), dpi=100)


self.viz_subplot = self.viz_figure.add_subplot(111)

self.viz_canvas = FigureCanvasTkAgg(self.viz_figure, viz_frame)


self.viz_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

def add_soil_layer(self):
"""Add a new soil layer to the profile"""
try:
# Get data from entries
depth = float(self.soil_entries["depth"].get())
thickness = float(self.soil_entries["thickness"].get())
soil_type = self.soil_entries["soil_type"].get()
color = self.soil_entries["color"].get()
moisture = float(self.soil_entries["moisture"].get()) if self.soil_entries["moisture"].get()
else 0
unit_weight = float(self.soil_entries["unit_weight"].get()) if
self.soil_entries["unit_weight"].get() else 0
spt_n = int(self.soil_entries["spt_n"].get()) if self.soil_entries["spt_n"].get() else 0
description = self.soil_entries["description"].get("1.0", tk.END).strip()

# Create layer data


layer = {
"depth": depth,
"thickness": thickness,
"soil_type": soil_type,
"color": color,
"moisture": moisture,
"unit_weight": unit_weight,
"spt_n": spt_n,
"description": description
}

# Add to soil layers list


self.soil_layers.append(layer)

# Add to treeview
self.soil_tree.insert("", "end", values=(depth, thickness, soil_type, unit_weight, spt_n))

# Clear entries
for entry in self.soil_entries.values():
if isinstance(entry, tk.Text):
entry.delete("1.0", tk.END)
else:
entry.delete(0, tk.END)

# Update visualization
self.update_soil_profile_viz()

messagebox.showinfo("Success", "Soil layer added successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Failed to add soil layer: {str(e)}")

def update_soil_layer(self):
"""Update the selected soil layer"""
selected = self.soil_tree.selection()
if not selected:
messagebox.showwarning("Warning", "Please select a soil layer to update.")
return

try:
# Get selected item index
index = self.soil_tree.index(selected[0])

# Get data from entries


depth = float(self.soil_entries["depth"].get())
thickness = float(self.soil_entries["thickness"].get())
soil_type = self.soil_entries["soil_type"].get()
color = self.soil_entries["color"].get()
moisture = float(self.soil_entries["moisture"].get()) if self.soil_entries["moisture"].get()
else 0
unit_weight = float(self.soil_entries["unit_weight"].get()) if
self.soil_entries["unit_weight"].get() else 0
spt_n = int(self.soil_entries["spt_n"].get()) if self.soil_entries["spt_n"].get() else 0
description = self.soil_entries["description"].get("1.0", tk.END).strip()

# Update layer data


self.soil_layers[index] = {
"depth": depth,
"thickness": thickness,
"soil_type": soil_type,
"color": color,
"moisture": moisture,
"unit_weight": unit_weight,
"spt_n": spt_n,
"description": description
}

# Update treeview
self.soil_tree.item(selected[0], values=(depth, thickness, soil_type, unit_weight, spt_n))

# Update visualization
self.update_soil_profile_viz()

messagebox.showinfo("Success", "Soil layer updated successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Failed to update soil layer: {str(e)}")

def delete_soil_layer(self):
"""Delete the selected soil layer"""
selected = self.soil_tree.selection()
if not selected:
messagebox.showwarning("Warning", "Please select a soil layer to delete.")
return

try:
# Get selected item index
index = self.soil_tree.index(selected[0])

# Remove from soil layers list


del self.soil_layers[index]

# Remove from treeview


self.soil_tree.delete(selected[0])
# Update visualization
self.update_soil_profile_viz()

messagebox.showinfo("Success", "Soil layer deleted successfully!")


except Exception as e:
messagebox.showerror("Error", f"Failed to delete soil layer: {str(e)}")

def on_soil_layer_select(self, event):


"""Handle soil layer selection in treeview"""
selected = self.soil_tree.selection()
if not selected:
return

# Get selected item index


index = self.soil_tree.index(selected[0])

# Get layer data


layer = self.soil_layers[index]

# Fill entries with selected layer data


self.soil_entries["depth"].delete(0, tk.END)
self.soil_entries["depth"].insert(0, str(layer["depth"]))

self.soil_entries["thickness"].delete(0, tk.END)
self.soil_entries["thickness"].insert(0, str(layer["thickness"]))

self.soil_entries["soil_type"].set(layer["soil_type"])

self.soil_entries["color"].delete(0, tk.END)
self.soil_entries["color"].insert(0, layer["color"])

self.soil_entries["moisture"].delete(0, tk.END)
self.soil_entries["moisture"].insert(0, str(layer["moisture"]))

self.soil_entries["unit_weight"].delete(0, tk.END)
self.soil_entries["unit_weight"].insert(0, str(layer["unit_weight"]))

self.soil_entries["spt_n"].delete(0, tk.END)
self.soil_entries["spt_n"].insert(0, str(layer["spt_n"]))

self.soil_entries["description"].delete("1.0", tk.END)
self.soil_entries["description"].insert("1.0", layer["description"])

def update_soil_profile_viz(self):
"""Update the soil profile visualization"""
if not self.soil_layers:
return

# Clear the plot


self.viz_subplot.clear()

# Sort layers by depth


sorted_layers = sorted(self.soil_layers, key=lambda x: x["depth"])

# Colors for soil types


soil_colors = {
"Clay": "#8B4513", # Brown
"Silt": "#D2B48C", # Tan
"Sand": "#F4A460", # Sandy brown
"Gravel": "#A9A9A9", # Dark gray
"Organic": "#2E8B57", # Sea green
"Fill": "#696969", # Dim gray
"Rock": "#708090" # Slate gray
}

# Plotting
depths = []
heights = []
colors = []
labels = []

for layer in sorted_layers:


depths.append(layer["depth"])
heights.append(layer["thickness"])
colors.append(soil_colors.get(layer["soil_type"], "#000000"))
labels.append(f"{layer['soil_type']} - {layer['description'][:20]}")

# Create horizontal bar chart


y_pos = range(len(depths))
self.viz_subplot.barh(y_pos, heights, left=depths, height=0.8, color=colors)

# Add layer labels


for i, label in enumerate(labels):
self.viz_subplot.text(depths[i] + heights[i]/2, i, label,
ha='center', va='center', fontsize=8)

# Set y-axis labels and invert (to have 0 depth at the top)
self.viz_subplot.set_yticks(y_pos)
self.viz_subplot.set_yticklabels([f"{d}m" for d in depths])
self.viz_subplot.invert_yaxis()

# Set title and labels


self.viz_subplot.set_title("Soil Profile")
self.viz_subplot.set_xlabel("Thickness (m)")
self.viz_subplot.set_ylabel("Depth (m)")

# Add grid
self.viz_subplot.grid(True, linestyle='--', alpha=0.7)

# Refresh canvas
self.viz_canvas.draw()

def setup_test_results_tab(self):
# Create frame with padding
frame = ttk.Frame(self.test_results_tab, padding="20")
frame.pack(fill=tk.BOTH, expand=True)

# Title
title = ttk.Label(frame, text="Soil Testing Results", font=("Arial", 16, "bold"))
title.grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=10)

# Create notebook for different test types


test_notebook = ttk.Notebook(frame)
test_notebook.grid(row=1, column=0, columnspan=3, sticky=tk.NSEW, pady=10)

# Add tabs for different test types


self.grain_size_tab = ttk.Frame(test_notebook)
self.atterberg_tab = ttk.Frame(test_notebook)
self.direct_shear_tab = ttk.Frame(test_notebook)
self.triaxial_tab = ttk.Frame(test_notebook)
self.consolidation_tab = ttk.Frame(test_notebook)

test_notebook.add(self.grain_size_tab, text="Grain Size")


test_notebook.add(self.atterberg_tab, text="Atterberg Limits")
test_notebook.add(self.direct_shear_tab, text="Direct Shear")
test_notebook.add(self.triaxial_tab, text="Triaxial")
test_notebook.add(self.consolidation_tab, text="Consolidation")

# Setup each test tab


self.setup_grain_size_tab()
self.setup_atterberg_limits_tab()
self.setup_direct_shear_tab()
self.setup_triaxial_tab()
self.setup_consolidation_tab()

# Add tab switch event


test_notebook.bind("<<NotebookTabChanged>>", self.on_test_tab_changed)

# Configure weight
frame.columnconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)

def setup_grain_size_tab(self):
frame = ttk.Frame(self.grain_size_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)

# Left side - input fields


input_frame = ttk.LabelFrame(frame, text="Grain Size Distribution Data")
input_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=5, pady=5)

# Sample selection
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)

ttk.Label(sample_frame, text="Sample ID:").pack(side=tk.LEFT, padx=5)


self.grain_size_sample = ttk.Entry(sample_frame, width=15)
self.grain_size_sample.pack(side=tk.LEFT, padx=5)

ttk.Label(sample_frame, text="Depth (m):").pack(side=tk.LEFT, padx=5)


self.grain_size_depth = ttk.Entry(sample_frame, width=10)
self.grain_size_depth.pack(side=tk.LEFT, padx=5)

# Data entry for sieve analysis


sieve_frame = ttk.Frame(input_frame)
sieve_frame.pack(fill=tk.BOTH, expand=True, pady=10)

# Two columns for data entry


col1 = ttk.Frame(sieve_frame)
col1.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)

col2 = ttk.Frame(sieve_frame)
col2.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)

# Column 1: Sieve sizes and percent passing


ttk.Label(col1, text="Sieve Size (mm)").grid(row=0, column=0, sticky=tk.W)
ttk.Label(col1, text="% Passing").grid(row=0, column=1, sticky=tk.W)
self.grain_size_entries = []

# Common sieve sizes


sieve_sizes = [75, 37.5, 19, 9.5, 4.75, 2, 0.85, 0.425, 0.25, 0.15, 0.075]

for i, size in enumerate(sieve_sizes):


ttk.Label(col1, text=str(size)).grid(row=i+1, column=0, sticky=tk.W, pady=2)
entry = ttk.Entry(col1, width=10)
entry.grid(row=i+1, column=1, sticky=tk.W, pady=2)
self.grain_size_entries.append((size, entry))

# Column 2: Summary data


ttk.Label(col2, text="Summary", font=("Arial", 11, "bold")).grid(row=0, column=0,
columnspan=2, sticky=tk.W, pady=5)

summary_labels = [
"D10 (mm):", "D30 (mm):", "D60 (mm):",
"Cu:", "Cc:", "% Gravel:",
"% Sand:", "% Silt and Clay:", "USCS:"
]

self.grain_size_summary = {}

for i, label in enumerate(summary_labels):


ttk.Label(col2, text=label).grid(row=i+1, column=0, sticky=tk.W, pady=2)
entry = ttk.Entry(col2, width=10)
entry.grid(row=i+1, column=1, sticky=tk.W, pady=2)
self.grain_size_summary[label.replace(":", "")] = entry

# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)
#LENI
ttk.Button(btn_frame, text="Calculate",
command=self.calculate_grain_size).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("grain_size")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_grain_size_data).pack(side=tk.LEFT, padx=5)

# Right side - visualization


viz_frame = ttk.LabelFrame(frame, text="Grain Size Distribution Curve")
viz_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=5, pady=5)
# Create matplotlib figure
self.gs_figure = plt.Figure(figsize=(6, 5), dpi=100)
self.gs_subplot = self.gs_figure.add_subplot(111)

# Create canvas
self.gs_canvas = FigureCanvasTkAgg(self.gs_figure, viz_frame)
self.gs_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)

def calculate_grain_size(self):
"""Calculate grain size distribution parameters"""
try:
# Get percent passing values
data = []
for size, entry in self.grain_size_entries:
if entry.get().strip():
percent = float(entry.get())
data.append((size, percent))

if not data:
messagebox.showwarning("Warning", "Please enter sieve analysis data.")
return

# Sort data by sieve size (descending)


data.sort(key=lambda x: x[0], reverse=True)

# Extract sorted arrays


x = np.array([item[0] for item in data])
y = np.array([item[1] for item in data])

# Perform interpolation for specific diameters (D10, D30, D60)


from scipy import interpolate

# Check if any percent passing values are below 10%


if min(y) > 10:
d10 = 0 # Cannot interpolate
else:
f = interpolate.interp1d(y, x)
d10 = float(f(10))
# D30
if min(y) > 30:
d30 = 0
else:
f = interpolate.interp1d(y, x)
d30 = float(f(30))

# D60
if min(y) > 60:
d60 = 0
else:
f = interpolate.interp1d(y, x)
d60 = float(f(60))

# Calculate Cu (uniformity coefficient) and Cc (coefficient of curvature)


if d10 > 0:
cu = d60 / d10
if d30 > 0:
cc = (d30 ** 2) / (d10 * d60)
else:
cc = 0
else:
cu = 0
cc = 0

# Calculate percentages of soil fractions


perc_gravel = 0
perc_sand = 0
perc_fines = 0

for i in range(len(data) - 1):


if data[i][0] >= 4.75: # Gravel: >4.75mm
if i == 0:
perc_gravel = 100 - data[i][1]
else:
perc_gravel += data[i-1][1] - data[i][1]
elif data[i][0] >= 0.075: # Sand: 0.075-4.75mm
perc_sand += data[i-1][1] - data[i][1]

# Fines (silt and clay): <0.075mm


perc_fines = data[-1][1] if data else 0

# Update summary fields


self.grain_size_summary["D10 (mm)"].delete(0, tk.END)
self.grain_size_summary["D10 (mm)"].insert(0, f"{d10:.3f}")

self.grain_size_summary["D30 (mm)"].delete(0, tk.END)


self.grain_size_summary["D30 (mm)"].insert(0, f"{d30:.3f}")

self.grain_size_summary["D60 (mm)"].delete(0, tk.END)


self.grain_size_summary["D60 (mm)"].insert(0, f"{d60:.3f}")

self.grain_size_summary["Cu"].delete(0, tk.END)
self.grain_size_summary["Cu"].insert(0, f"{cu:.2f}")

self.grain_size_summary["Cc"].delete(0, tk.END)
self.grain_size_summary["Cc"].insert(0, f"{cc:.2f}")

self.grain_size_summary["% Gravel"].delete(0, tk.END)


self.grain_size_summary["% Gravel"].insert(0, f"{perc_gravel:.1f}")

self.grain_size_summary["% Sand"].delete(0, tk.END)


self.grain_size_summary["% Sand"].insert(0, f"{perc_sand:.1f}")

self.grain_size_summary["% Silt and Clay"].delete(0, tk.END)


self.grain_size_summary["% Silt and Clay"].insert(0, f"{perc_fines:.1f}")

# Determine USCS classification (simplified)


uscs = "ML" # Default
if perc_fines < 5:
if cu >= 4 and 1 <= cc <= 3:
uscs = "SW" if perc_gravel < 50 else "GW"
else:
uscs = "SP" if perc_gravel < 50 else "GP"
elif 5 <= perc_fines <= 12:
if cu >= 4 and 1 <= cc <= 3:
uscs = "SW-SM" if perc_gravel < 50 else "GW-GM"
else:
uscs = "SP-SM" if perc_gravel < 50 else "GP-GM"
else: # perc_fines > 12
if perc_gravel >= 50:
uscs = "GM"
else:
uscs = "SM"

self.grain_size_summary["USCS"].delete(0, tk.END)
self.grain_size_summary["USCS"].insert(0, uscs)
# Plot grain size distribution curve
self.gs_subplot.clear()
self.gs_subplot.semilogx(x, y, 'o-', linewidth=2)
self.gs_subplot.set_xlabel('Particle Size (mm)')
self.gs_subplot.set_ylabel('Percent Passing (%)')
self.gs_subplot.set_title('Grain Size Distribution')
self.gs_subplot.grid(True, which="both", ls="-")
self.gs_subplot.set_ylim([0, 100])

# Add D10, D30, D60 lines


if d10 > 0:
self.gs_subplot.axhline(y=10, color='r', linestyle='--', alpha=0.7)
self.gs_subplot.axvline(x=d10, color='r', linestyle='--', alpha=0.7)
if d30 > 0:
self.gs_subplot.axhline(y=30, color='g', linestyle='--', alpha=0.7)
self.gs_subplot.axvline(x=d30, color='g', linestyle='--', alpha=0.7)
if d60 > 0:
self.gs_subplot.axhline(y=60, color='b', linestyle='--', alpha=0.7)
self.gs_subplot.axvline(x=d60, color='b', linestyle='--', alpha=0.7)

# Refresh canvas
self.gs_canvas.draw()

messagebox.showinfo("Success", "Grain size analysis calculated successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Calculation failed: {str(e)}")

def clear_grain_size_data(self):
"""Clear grain size analysis data"""
# Clear sieve entries
for _, entry in self.grain_size_entries:
entry.delete(0, tk.END)

# Clear summary entries


for entry in self.grain_size_summary.values():
entry.delete(0, tk.END)

# Clear sample ID and depth


self.grain_size_sample.delete(0, tk.END)
self.grain_size_depth.delete(0, tk.END)
# Clear plot
self.gs_subplot.clear()
self.gs_subplot.set_xlabel('Particle Size (mm)')
self.gs_subplot.set_ylabel('Percent Passing (%)')
self.gs_subplot.set_title('Grain Size Distribution')
self.gs_canvas.draw()

def save_test_data(self, test_type):


"""Save test data to the project"""
if test_type == "grain_size":
sample_id = self.grain_size_sample.get()
depth = self.grain_size_depth.get()

if not sample_id or not depth:


messagebox.showwarning("Warning", "Please enter Sample ID and Depth.")
return

# Create dictionary for grain size data


grain_size_data = {
"sample_id": sample_id,
"depth": depth,
"sieve_data": [],
"summary": {}
}

# Get sieve data


for size, entry in self.grain_size_entries:
if entry.get().strip():
grain_size_data["sieve_data"].append({
"size": size,
"percent_passing": float(entry.get())
})

# Get summary data


for key, entry in self.grain_size_summary.items():
if entry.get().strip():
try:
grain_size_data["summary"][key] = float(entry.get())
except ValueError:
grain_size_data["summary"][key] = entry.get()

# Store in test results


if "grain_size" not in self.test_results:
self.test_results["grain_size"] = []
self.test_results["grain_size"].append(grain_size_data)

messagebox.showinfo("Success", f"Grain size analysis for Sample {sample_id} saved


successfully!")

# Add similar methods for other test types

def setup_atterberg_limits_tab(self):
frame = ttk.Frame(self.atterberg_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)

# Left side - input fields


input_frame = ttk.LabelFrame(frame, text="Atterberg Limits Test Data")
input_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=5, pady=5)

# Sample selection
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)

ttk.Label(sample_frame, text="Sample ID:").pack(side=tk.LEFT, padx=5)


self.atterberg_sample = ttk.Entry(sample_frame, width=15)
self.atterberg_sample.pack(side=tk.LEFT, padx=5)

ttk.Label(sample_frame, text="Depth (m):").pack(side=tk.LEFT, padx=5)


self.atterberg_depth = ttk.Entry(sample_frame, width=10)
self.atterberg_depth.pack(side=tk.LEFT, padx=5)

# Liquid Limit test data


ll_frame = ttk.LabelFrame(input_frame, text="Liquid Limit Test")
ll_frame.pack(fill=tk.X, pady=10)

ll_header_frame = ttk.Frame(ll_frame)
ll_header_frame.pack(fill=tk.X)

ttk.Label(ll_header_frame, text="No. of Blows", width=10).pack(side=tk.LEFT, padx=5)


ttk.Label(ll_header_frame, text="Moisture Content (%)", width=20).pack(side=tk.LEFT,
padx=5)

self.ll_entries = []
for i in range(4): # 4 measurements for liquid limit
row_frame = ttk.Frame(ll_frame)
row_frame.pack(fill=tk.X)
blows_entry = ttk.Entry(row_frame, width=10)
blows_entry.pack(side=tk.LEFT, padx=5, pady=2)

mc_entry = ttk.Entry(row_frame, width=20)


mc_entry.pack(side=tk.LEFT, padx=5, pady=2)

self.ll_entries.append((blows_entry, mc_entry))

# Plastic Limit test data


pl_frame = ttk.LabelFrame(input_frame, text="Plastic Limit Test")
pl_frame.pack(fill=tk.X, pady=10)

pl_header_frame = ttk.Frame(pl_frame)
pl_header_frame.pack(fill=tk.X)

ttk.Label(pl_header_frame, text="Trial No.", width=10).pack(side=tk.LEFT, padx=5)


ttk.Label(pl_header_frame, text="Moisture Content (%)", width=20).pack(side=tk.LEFT,
padx=5)

self.pl_entries = []
for i in range(3): # 3 measurements for plastic limit
row_frame = ttk.Frame(pl_frame)
row_frame.pack(fill=tk.X)

trial_label = ttk.Label(row_frame, text=f"Trial {i+1}", width=10)


trial_label.pack(side=tk.LEFT, padx=5, pady=2)

mc_entry = ttk.Entry(row_frame, width=20)


mc_entry.pack(side=tk.LEFT, padx=5, pady=2)

self.pl_entries.append(mc_entry)

# Results section
results_frame = ttk.LabelFrame(input_frame, text="Results")
results_frame.pack(fill=tk.X, pady=10)

results_grid = ttk.Frame(results_frame)
results_grid.pack(fill=tk.X, padx=5, pady=5)

ttk.Label(results_grid, text="Liquid Limit (LL):").grid(row=0, column=0, sticky=tk.W, padx=5,


pady=2)
self.ll_result = ttk.Entry(results_grid, width=10)
self.ll_result.grid(row=0, column=1, padx=5, pady=2)
ttk.Label(results_grid, text="%").grid(row=0, column=2, sticky=tk.W)
ttk.Label(results_grid, text="Plastic Limit (PL):").grid(row=1, column=0, sticky=tk.W,
padx=5, pady=2)
self.pl_result = ttk.Entry(results_grid, width=10)
self.pl_result.grid(row=1, column=1, padx=5, pady=2)
ttk.Label(results_grid, text="%").grid(row=1, column=2, sticky=tk.W)

ttk.Label(results_grid, text="Plasticity Index (PI):").grid(row=2, column=0, sticky=tk.W,


padx=5, pady=2)
self.pi_result = ttk.Entry(results_grid, width=10)
self.pi_result.grid(row=2, column=1, padx=5, pady=2)
ttk.Label(results_grid, text="%").grid(row=2, column=2, sticky=tk.W)

ttk.Label(results_grid, text="Classification:").grid(row=3, column=0, sticky=tk.W, padx=5,


pady=2)
self.atterberg_class = ttk.Entry(results_grid, width=10)
self.atterberg_class.grid(row=3, column=1, padx=5, pady=2)

# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)

ttk.Button(btn_frame, text="Calculate",
command=self.calculate_atterberg).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("atterberg")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_atterberg_data).pack(side=tk.LEFT, padx=5)

# Right side - visualization


viz_frame = ttk.LabelFrame(frame, text="Plasticity Chart")
viz_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=5, pady=5)

# Create matplotlib figure


self.at_figure = plt.Figure(figsize=(6, 5), dpi=100)
self.at_subplot = self.at_figure.add_subplot(111)

# Create canvas
self.at_canvas = FigureCanvasTkAgg(self.at_figure, viz_frame)
self.at_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)

def calculate_atterberg(self):
"""Calculate Atterberg limits"""
try:
# Process liquid limit data
ll_data = []
for blows_entry, mc_entry in self.ll_entries:
if blows_entry.get().strip() and mc_entry.get().strip():
blows = float(blows_entry.get())
mc = float(mc_entry.get())
ll_data.append((blows, mc))

if not ll_data:
messagebox.showwarning("Warning", "Please enter liquid limit test data.")
return

# Process plastic limit data


pl_values = []
for entry in self.pl_entries:
if entry.get().strip():
pl_values.append(float(entry.get()))

if not pl_values:
messagebox.showwarning("Warning", "Please enter plastic limit test data.")
return

# Calculate liquid limit (at 25 blows)


# Using semi-log method (log blows vs. moisture content)
from scipy import stats

x = np.log10([point[0] for point in ll_data])


y = np.array([point[1] for point in ll_data])

slope, intercept, _, _, _ = stats.linregress(x, y)

# LL is moisture content at 25 blows


ll = slope * np.log10(25) + intercept

# Calculate plastic limit (average of trials)


pl = sum(pl_values) / len(pl_values)

# Calculate plasticity index


pi = ll - pl
# Update result fields
self.ll_result.delete(0, tk.END)
self.ll_result.insert(0, f"{ll:.1f}")

self.pl_result.delete(0, tk.END)
self.pl_result.insert(0, f"{pl:.1f}")

self.pi_result.delete(0, tk.END)
self.pi_result.insert(0, f"{pi:.1f}")

# Determine classification
if pi < 4:
classification = "ML" # Low plasticity silt
elif pi >= 4 and pi < 7:
classification = "CL-ML" # Silty clay
elif pi >= 7 and ll < 50:
classification = "CL" # Low plasticity clay
elif pi >= 7 and ll >= 50:
classification = "CH" # High plasticity clay
else:
classification = "--"

self.atterberg_class.delete(0, tk.END)
self.atterberg_class.insert(0, classification)

# Plot flow curve and plasticity chart


self.at_subplot.clear()

# Plot flow curve in small subplot


ax1 = self.at_figure.add_subplot(221)
ax1.set_xscale('log')
ax1.scatter([point[0] for point in ll_data], [point[1] for point in ll_data], color='blue')

# Add regression line


x_reg = np.linspace(min(x), max(x), 100)
y_reg = slope * x_reg + intercept
ax1.plot(10**x_reg, y_reg, 'r-')

# Mark 25 blows
ax1.axvline(x=25, color='green', linestyle='--')
ax1.axhline(y=ll, color='green', linestyle='--')

ax1.set_xlabel('Number of Blows')
ax1.set_ylabel('Moisture Content (%)')
ax1.set_title('Flow Curve')
ax1.grid(True)

# Plot plasticity chart


ax2 = self.at_figure.add_subplot(212)

# Draw A-line and U-line


ll_range = np.linspace(0, 100, 100)
a_line = 0.73 * (ll_range - 20)
u_line = 0.9 * (ll_range - 8)

ax2.plot(ll_range, a_line, 'k-', label='A-line')


ax2.plot(ll_range, u_line, 'k--', label='U-line')

# Vertical line at LL=50


ax2.axvline(x=50, color='gray', linestyle='-')

# Plot data point


ax2.plot(ll, pi, 'ro', markersize=8)

# Add text labels for soil types


ax2.text(30, 10, 'CL', fontsize=12)
ax2.text(30, 4, 'ML', fontsize=12)
ax2.text(70, 30, 'CH', fontsize=12)
ax2.text(70, 10, 'MH', fontsize=12)

ax2.set_xlim([0, 100])
ax2.set_ylim([0, 60])
ax2.set_xlabel('Liquid Limit (%)')
ax2.set_ylabel('Plasticity Index (%)')
ax2.set_title('Plasticity Chart')
ax2.grid(True)
ax2.legend()

# Adjust layout
self.at_figure.tight_layout()

# Refresh canvas
self.at_canvas.draw()

messagebox.showinfo("Success", "Atterberg limits calculated successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Calculation failed: {str(e)}")

def clear_atterberg_data(self):
"""Clear Atterberg limits test data"""
# Clear liquid limit entries
for blows_entry, mc_entry in self.ll_entries:
blows_entry.delete(0, tk.END)
mc_entry.delete(0, tk.END)

# Clear plastic limit entries


for entry in self.pl_entries:
entry.delete(0, tk.END)

# Clear result entries


self.ll_result.delete(0, tk.END)
self.pl_result.delete(0, tk.END)
self.pi_result.delete(0, tk.END)
self.atterberg_class.delete(0, tk.END)

# Clear sample ID and depth


self.atterberg_sample.delete(0, tk.END)
self.atterberg_depth.delete(0, tk.END)

# Clear plot
self.at_subplot.clear()
self.at_canvas.draw()

def setup_direct_shear_tab(self):
frame = ttk.Frame(self.direct_shear_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)

# Left side - input fields


input_frame = ttk.LabelFrame(frame, text="Direct Shear Test Data")
input_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=5, pady=5)

# Sample info
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)

ttk.Label(sample_frame, text="Sample ID:").pack(side=tk.LEFT, padx=5)


self.ds_sample = ttk.Entry(sample_frame, width=15)
self.ds_sample.pack(side=tk.LEFT, padx=5)
ttk.Label(sample_frame, text="Depth (m):").pack(side=tk.LEFT, padx=5)
self.ds_depth = ttk.Entry(sample_frame, width=10)
self.ds_depth.pack(side=tk.LEFT, padx=5)

# Test data
test_frame = ttk.Frame(input_frame)
test_frame.pack(fill=tk.X, pady=10)

ttk.Label(test_frame, text="Normal Stress (kPa)").grid(row=0, column=0, padx=5, pady=2)


ttk.Label(test_frame, text="Peak Shear Stress (kPa)").grid(row=0, column=1, padx=5,
pady=2)
ttk.Label(test_frame, text="Residual Shear Stress (kPa)").grid(row=0, column=2, padx=5,
pady=2)

self.ds_entries = []
for i in range(3): # 3 normal stress levels
normal_entry = ttk.Entry(test_frame, width=15)
normal_entry.grid(row=i+1, column=0, padx=5, pady=2)

peak_entry = ttk.Entry(test_frame, width=15)


peak_entry.grid(row=i+1, column=1, padx=5, pady=2)

resid_entry = ttk.Entry(test_frame, width=15)


resid_entry.grid(row=i+1, column=2, padx=5, pady=2)

self.ds_entries.append((normal_entry, peak_entry, resid_entry))

# Results
results_frame = ttk.LabelFrame(input_frame, text="Shear Strength Parameters")
results_frame.pack(fill=tk.X, pady=10)

result_grid = ttk.Frame(results_frame)
result_grid.pack(fill=tk.X, padx=5, pady=5)

ttk.Label(result_grid, text="Peak Cohesion (c'):").grid(row=0, column=0, sticky=tk.W,


padx=5, pady=2)
self.peak_cohesion = ttk.Entry(result_grid, width=10)
self.peak_cohesion.grid(row=0, column=1, padx=5, pady=2)
ttk.Label(result_grid, text="kPa").grid(row=0, column=2, sticky=tk.W)

ttk.Label(result_grid, text="Peak Friction Angle (φ'):").grid(row=1, column=0, sticky=tk.W,


padx=5, pady=2)
self.peak_friction = ttk.Entry(result_grid, width=10)
self.peak_friction.grid(row=1, column=1, padx=5, pady=2)
ttk.Label(result_grid, text="degrees").grid(row=1, column=2, sticky=tk.W)

ttk.Label(result_grid, text="Residual Cohesion (c'r):").grid(row=2, column=0, sticky=tk.W,


padx=5, pady=2)
self.resid_cohesion = ttk.Entry(result_grid, width=10)
self.resid_cohesion.grid(row=2, column=1, padx=5, pady=2)
ttk.Label(result_grid, text="kPa").grid(row=2, column=2, sticky=tk.W)

ttk.Label(result_grid, text="Residual Friction Angle (φ'r):").grid(row=3, column=0,


sticky=tk.W, padx=5, pady=2)
self.resid_friction = ttk.Entry(result_grid, width=10)
self.resid_friction.grid(row=3, column=1, padx=5, pady=2)
ttk.Label(result_grid, text="degrees").grid(row=3, column=2, sticky=tk.W)

# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)

ttk.Button(btn_frame, text="Calculate",
command=self.calculate_direct_shear).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("direct_shear")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_direct_shear_data).pack(side=tk.LEFT, padx=5)

# Right side - visualization


viz_frame = ttk.LabelFrame(frame, text="Direct Shear Results")
viz_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=5, pady=5)
#LENII
# Create matplotlib figure
self.ds_figure = plt.Figure(figsize=(6, 5), dpi=100)
self.ds_subplot = self.ds_figure.add_subplot(111)

# Create canvas
self.ds_canvas = FigureCanvasTkAgg(self.ds_figure, viz_frame)
self.ds_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)

def calculate_direct_shear(self):
"""Calculate direct shear parameters"""
try:
# Extract test data
data = []
for normal_entry, peak_entry, resid_entry in self.ds_entries:
if normal_entry.get().strip() and peak_entry.get().strip():
normal_stress = float(normal_entry.get())
peak_stress = float(peak_entry.get())

# Residual stress is optional


if resid_entry.get().strip():
resid_stress = float(resid_entry.get())
else:
resid_stress = None

data.append((normal_stress, peak_stress, resid_stress))

if len(data) < 2:
messagebox.showwarning("Warning", "Please enter at least two data points.")
return

# Linear regression for peak parameters


normal_stresses = np.array([point[0] for point in data])
peak_stresses = np.array([point[1] for point in data])

peak_slope, peak_intercept, _, _, _ = stats.linregress(normal_stresses, peak_stresses)

# Calculate peak parameters


peak_c = peak_intercept # Cohesion is the y-intercept
peak_phi = np.degrees(np.arctan(peak_slope)) # Friction angle is the slope

# Update peak parameters


self.peak_cohesion.delete(0, tk.END)
self.peak_cohesion.insert(0, f"{peak_c:.2f}")

self.peak_friction.delete(0, tk.END)
self.peak_friction.insert(0, f"{peak_phi:.1f}")

# Calculate residual parameters if data available


if all(point[2] is not None for point in data):
resid_stresses = np.array([point[2] for point in data])

resid_slope, resid_intercept, _, _, _ = stats.linregress(normal_stresses, resid_stresses)

resid_c = resid_intercept
resid_phi = np.degrees(np.arctan(resid_slope))

# Update residual parameters


self.resid_cohesion.delete(0, tk.END)
self.resid_cohesion.insert(0, f"{resid_c:.2f}")

self.resid_friction.delete(0, tk.END)
self.resid_friction.insert(0, f"{resid_phi:.1f}")
else:
# Clear residual parameters if not available
self.resid_cohesion.delete(0, tk.END)
self.resid_friction.delete(0, tk.END)

# Plot results
self.ds_subplot.clear()

# Plot peak data points


self.ds_subplot.scatter(normal_stresses, peak_stresses, color='blue', label='Peak')

# Plot peak failure envelope


x_range = np.linspace(0, max(normal_stresses) * 1.1, 100)
self.ds_subplot.plot(x_range, peak_slope * x_range + peak_intercept, 'b-')

# Plot residual data if available


if all(point[2] is not None for point in data):
self.ds_subplot.scatter(normal_stresses, resid_stresses, color='red', label='Residual')
self.ds_subplot.plot(x_range, resid_slope * x_range + resid_intercept, 'r-')

self.ds_subplot.set_xlabel('Normal Stress (kPa)')


self.ds_subplot.set_ylabel('Shear Stress (kPa)')
self.ds_subplot.set_title('Direct Shear Test Results')
self.ds_subplot.grid(True)
self.ds_subplot.legend()

# Refresh canvas
self.ds_canvas.draw()

messagebox.showinfo("Success", "Direct shear parameters calculated successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Calculation failed: {str(e)}")

def clear_direct_shear_data(self):
"""Clear direct shear test data"""
# Clear test entries
for normal_entry, peak_entry, resid_entry in self.ds_entries:
normal_entry.delete(0, tk.END)
peak_entry.delete(0, tk.END)
resid_entry.delete(0, tk.END)

# Clear result entries


self.peak_cohesion.delete(0, tk.END)
self.peak_friction.delete(0, tk.END)
self.resid_cohesion.delete(0, tk.END)
self.resid_friction.delete(0, tk.END)

# Clear sample ID and depth


self.ds_sample.delete(0, tk.END)
self.ds_depth.delete(0, tk.END)

# Clear plot
self.ds_subplot.clear()
self.ds_subplot.set_xlabel('Normal Stress (kPa)')
self.ds_subplot.set_ylabel('Shear Stress (kPa)')
self.ds_subplot.set_title('Direct Shear Test Results')
self.ds_canvas.draw()

def save_test_data(self, test_type):


"""Save test data to the project"""
if test_type == "grain_size":
# This was already implemented in the original code
pass
elif test_type == "atterberg":
sample_id = self.atterberg_sample.get()
depth = self.atterberg_depth.get()

if not sample_id or not depth:


messagebox.showwarning("Warning", "Please enter Sample ID and Depth.")
return

# Create dictionary for Atterberg limits data


atterberg_data = {
"sample_id": sample_id,
"depth": depth,
"liquid_limit_tests": [],
"plastic_limit_tests": [],
"results": {}
}

# Get liquid limit test data


for blows_entry, mc_entry in self.ll_entries:
if blows_entry.get().strip() and mc_entry.get().strip():
atterberg_data["liquid_limit_tests"].append({
"blows": float(blows_entry.get()),
"moisture_content": float(mc_entry.get())
})

# Get plastic limit test data


for i, entry in enumerate(self.pl_entries):
if entry.get().strip():
atterberg_data["plastic_limit_tests"].append({
"trial": i+1,
"moisture_content": float(entry.get())
})

# Get results
if self.ll_result.get().strip():
atterberg_data["results"]["liquid_limit"] = float(self.ll_result.get())

if self.pl_result.get().strip():
atterberg_data["results"]["plastic_limit"] = float(self.pl_result.get())

if self.pi_result.get().strip():
atterberg_data["results"]["plasticity_index"] = float(self.pi_result.get())

if self.atterberg_class.get().strip():
atterberg_data["results"]["classification"] = self.atterberg_class.get()

# Store in test results


if "atterberg" not in self.test_results:
self.test_results["atterberg"] = []

self.test_results["atterberg"].append(atterberg_data)

messagebox.showinfo("Success", f"Atterberg limits for Sample {sample_id} saved


successfully!")

elif test_type == "direct_shear":


sample_id = self.ds_sample.get()
depth = self.ds_depth.get()
if not sample_id or not depth:
messagebox.showwarning("Warning", "Please enter Sample ID and Depth.")
return

# Create dictionary for direct shear data


ds_data = {
"sample_id": sample_id,
"depth": depth,
"test_data": [],
"results": {}
}

# Get test data


for normal_entry, peak_entry, resid_entry in self.ds_entries:
if normal_entry.get().strip() and peak_entry.get().strip():
test_point = {
"normal_stress": float(normal_entry.get()),
"peak_shear_stress": float(peak_entry.get())
}

if resid_entry.get().strip():
test_point["residual_shear_stress"] = float(resid_entry.get())

ds_data["test_data"].append(test_point)

# Get results
if self.peak_cohesion.get().strip():
ds_data["results"]["peak_cohesion"] = float(self.peak_cohesion.get())

if self.peak_friction.get().strip():
ds_data["results"]["peak_friction_angle"] = float(self.peak_friction.get())

if self.resid_cohesion.get().strip():
ds_data["results"]["residual_cohesion"] = float(self.resid_cohesion.get())

if self.resid_friction.get().strip():
ds_data["results"]["residual_friction_angle"] = float(self.resid_friction.get())

# Store in test results


if "direct_shear" not in self.test_results:
self.test_results["direct_shear"] = []

self.test_results["direct_shear"].append(ds_data)
messagebox.showinfo("Success", f"Direct shear test for Sample {sample_id} saved
successfully!")

def setup_consolidation_tab(self):
frame = ttk.Frame(self.consolidation_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)

# Left side - input fields


input_frame = ttk.LabelFrame(frame, text="Consolidation Test Data")
input_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=5, pady=5)

# Sample info
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)

ttk.Label(sample_frame, text="Sample ID:").pack(side=tk.LEFT, padx=5)


self.cons_sample = ttk.Entry(sample_frame, width=15)
self.cons_sample.pack(side=tk.LEFT, padx=5)

ttk.Label(sample_frame, text="Depth (m):").pack(side=tk.LEFT, padx=5)


self.cons_depth = ttk.Entry(sample_frame, width=10)
self.cons_depth.pack(side=tk.LEFT, padx=5)

# Test data
test_frame = ttk.LabelFrame(input_frame, text="Loading Stages")
test_frame.pack(fill=tk.X, pady=10)

# Headers
ttk.Label(test_frame, text="Pressure (kPa)").grid(row=0, column=0, padx=5, pady=2)
ttk.Label(test_frame, text="Void Ratio").grid(row=0, column=1, padx=5, pady=2)

# Create 8 rows for loading stages


self.cons_entries = []
for i in range(8):
pressure_entry = ttk.Entry(test_frame, width=15)
pressure_entry.grid(row=i+1, column=0, padx=5, pady=2)

void_ratio_entry = ttk.Entry(test_frame, width=15)


void_ratio_entry.grid(row=i+1, column=1, padx=5, pady=2)

self.cons_entries.append((pressure_entry, void_ratio_entry))

# Results
results_frame = ttk.LabelFrame(input_frame, text="Consolidation Parameters")
results_frame.pack(fill=tk.X, pady=10)

result_grid = ttk.Frame(results_frame)
result_grid.pack(fill=tk.X, padx=5, pady=5)

ttk.Label(result_grid, text="Compression Index (Cc):").grid(row=0, column=0, sticky=tk.W,


padx=5, pady=2)
self.compress_index = ttk.Entry(result_grid, width=10)
self.compress_index.grid(row=0, column=1, padx=5, pady=2)

ttk.Label(result_grid, text="Recompression Index (Cr):").grid(row=1, column=0, sticky=tk.W,


padx=5, pady=2)
self.recompress_index = ttk.Entry(result_grid, width=10)
self.recompress_index.grid(row=1, column=1, padx=5, pady=2)

ttk.Label(result_grid, text="Preconsolidation Pressure (kPa):").grid(row=2, column=0,


sticky=tk.W, padx=5, pady=2)
self.precons_pressure = ttk.Entry(result_grid, width=10)
self.precons_pressure.grid(row=2, column=1, padx=5, pady=2)

ttk.Label(result_grid, text="Initial Void Ratio (e0):").grid(row=3, column=0, sticky=tk.W,


padx=5, pady=2)
self.initial_void_ratio = ttk.Entry(result_grid, width=10)
self.initial_void_ratio.grid(row=3, column=1, padx=5, pady=2)

# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)

ttk.Button(btn_frame, text="Calculate",
command=self.calculate_consolidation).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("consolidation")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_consolidation_data).pack(side=tk.LEFT, padx=5)

# Right side - visualization


viz_frame = ttk.LabelFrame(frame, text="e-log p Curve")
viz_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=5, pady=5)

# Create matplotlib figure


self.cons_figure = plt.Figure(figsize=(6, 5), dpi=100)
self.cons_subplot = self.cons_figure.add_subplot(111)
# Create canvas
self.cons_canvas = FigureCanvasTkAgg(self.cons_figure, viz_frame)
self.cons_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)

def calculate_consolidation(self):
"""Calculate consolidation parameters"""
try:
# Extract test data
data = []
for pressure_entry, void_ratio_entry in self.cons_entries:
if pressure_entry.get().strip() and void_ratio_entry.get().strip():
pressure = float(pressure_entry.get())
void_ratio = float(void_ratio_entry.get())
data.append((pressure, void_ratio))

if len(data) < 4:
messagebox.showwarning("Warning", "Please enter at least four data points.")
return

# Sort data by pressure


data.sort(key=lambda x: x[0])

# Extract arrays
pressures = np.array([point[0] for point in data])
void_ratios = np.array([point[1] for point in data])

# Initial void ratio is the first value


e0 = void_ratios[0]

# Calculate preconsolidation pressure using Casagrande method


# This is a simplified approach - in real applications, this would require
# more sophisticated curve fitting and graphical methods

# For demonstration, we'll estimate it as the pressure where there's


# the largest change in slope on a semi-log plot
log_pressures = np.log10(pressures)
slopes = []

for i in range(len(log_pressures) - 1):


slope = (void_ratios[i+1] - void_ratios[i]) / (log_pressures[i+1] - log_pressures[i])
slopes.append((i, slope))

# Find largest change in slope


max_change_idx = 0
max_change = 0

for i in range(len(slopes) - 1):


change = abs(slopes[i+1][1] - slopes[i][1])
if change > max_change:
max_change = change
max_change_idx = i

# Estimate preconsolidation pressure


pc_idx = max_change_idx + 1
pc = pressures[pc_idx]

# Calculate compression index (Cc) from virgin compression line


# Use data points after preconsolidation pressure
virgin_data = [(np.log10(p), e) for p, e in zip(pressures, void_ratios) if p >= pc]

if len(virgin_data) >= 2:
log_p_virgin = np.array([point[0] for point in virgin_data])
e_virgin = np.array([point[1] for point in virgin_data])

# Linear regression
slope_virgin, _, _, _, _ = stats.linregress(log_p_virgin, e_virgin)

# Cc is the negative of the slope


cc = -slope_virgin
else:
cc = 0

# Calculate recompression index (Cr) from data points before PC


recomp_data = [(np.log10(p), e) for p, e in zip(pressures, void_ratios) if p < pc]

if len(recomp_data) >= 2:
log_p_recomp = np.array([point[0] for point in recomp_data])
e_recomp = np.array([point[1] for point in recomp_data])

# Linear regression
slope_recomp, _, _, _, _ = stats.linregress(log_p_recomp, e_recomp)

# Cr is the negative of the slope


cr = -slope_recomp
else:
cr = 0

# Update results
self.compress_index.delete(0, tk.END)
self.compress_index.insert(0, f"{cc:.3f}")

self.recompress_index.delete(0, tk.END)
self.recompress_index.insert(0, f"{cr:.3f}")

self.precons_pressure.delete(0, tk.END)
self.precons_pressure.insert(0, f"{pc:.1f}")

self.initial_void_ratio.delete(0, tk.END)
self.initial_void_ratio.insert(0, f"{e0:.3f}")

# Plot e-log p curve


self.cons_subplot.clear()

# Plot data points


self.cons_subplot.semilogx(pressures, void_ratios, 'o-', color='blue')

# Mark preconsolidation pressure


self.cons_subplot.axvline(x=pc, color='red', linestyle='--', alpha=0.7)

self.cons_subplot.set_xlabel('Pressure (kPa) - Log Scale')


self.cons_subplot.set_ylabel('Void Ratio')
self.cons_subplot.set_title('e-log p Curve')
self.cons_subplot.grid(True, which="both")

# Add construction lines for Casagrande method


if pc_idx > 0 and pc_idx < len(pressures) - 1:
e_pc = void_ratios[pc_idx]
self.cons_subplot.plot([pc], [e_pc], 'ro', markersize=8)
self.cons_subplot.text(pc*1.1, e_pc, f'Pc ≈ {pc:.1f} kPa', verticalalignment='bottom')

# Refresh canvas
self.cons_canvas.draw()

messagebox.showinfo("Success", "Consolidation parameters calculated successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Calculation failed: {str(e)}")

def clear_consolidation_data(self):
"""Clear consolidation test data"""
# Clear test entries
for pressure_entry, void_ratio_entry in self.cons_entries:
pressure_entry.delete(0, tk.END)
void_ratio_entry.delete(0, tk.END)

# Clear result entries


self.compress_index.delete(0, tk.END)
self.recompress_index.delete(0, tk.END)
self.precons_pressure.delete(0, tk.END)
self.initial_void_ratio.delete(0, tk.END)

# Clear sample ID and depth


self.cons_sample.delete(0, tk.END)
self.cons_depth.delete(0, tk.END)

# Clear plot
self.cons_subplot.clear()
self.cons_subplot.set_xlabel('Pressure (kPa) - Log Scale')
self.cons_subplot.set_ylabel('Void Ratio')
self.cons_subplot.set_title('e-log p Curve')
self.cons_canvas.draw()

def generate_report(self):
"""Generate a comprehensive report with all test results"""
if not hasattr(self, 'project_info') or not self.project_info:
messagebox.showwarning("Warning", "Please enter project information first.")
return

if not self.test_results:
messagebox.showwarning("Warning", "No test results to generate report.")
return

try:
# Ask for save location
file_path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF Files", "*.pdf"), ("All Files", "*.*")],
title="Save Report As"
)
if not file_path:
return

# Generate PDF report using ReportLab


from reportlab.lib.pagesizes import letter
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
Image
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors

# Create document
doc = SimpleDocTemplate(file_path, pagesize=letter)
elements = []

# Get styles
styles = getSampleStyleSheet()
title_style = styles['Heading1']
heading2_style = styles['Heading2']
normal_style = styles['Normal']

# Add title
elements.append(Paragraph("Geotechnical Laboratory Test Report", title_style))
elements.append(Spacer(1, 12))

# Add project information


elements.append(Paragraph("Project Information", heading2_style))
elements.append(Spacer(1, 6))

project_data = [
["Project Name:", self.project_info.get("name", "")],
["Project Number:", self.project_info.get("number", "")],
["Location:", self.project_info.get("location", "")],
["Client:", self.project_info.get("client", "")],
["Engineer:", self.project_info.get("engineer", "")],
["Date:", self.project_info.get("date", "")]
]

project_table = Table(project_data, colWidths=[100, 300])


project_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
]))

elements.append(project_table)
elements.append(Spacer(1, 12))

# Add test results


# Grain Size Analysis
if "grain_size" in self.test_results and self.test_results["grain_size"]:
elements.append(Paragraph("Grain Size Analysis Results", heading2_style))
elements.append(Spacer(1, 6))

for i, test in enumerate(self.test_results["grain_size"]):


elements.append(Paragraph(f"Sample {i+1}: ID {test['sample_id']}, Depth
{test['depth']} m", styles['Heading3']))

# Summary data
summary_data = [["Parameter", "Value"]]
for key, value in test["summary"].items():
summary_data.append([key, str(value)])

summary_table = Table(summary_data, colWidths=[200, 200])


summary_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
]))

elements.append(summary_table)
elements.append(Spacer(1, 12))

# Atterberg Limits
if "atterberg" in self.test_results and self.test_results["atterberg"]:
elements.append(Paragraph("Atterberg Limits Results", heading2_style))
elements.append(Spacer(1, 6))

for i, test in enumerate(self.test_results["atterberg"]):


elements.append(Paragraph(f"Sample {i+1}: ID {test['sample_id']}, Depth
{test['depth']} m", styles['Heading3']))
# Results
if "results" in test:
results_data = [["Parameter", "Value"]]
for key, value in test["results"].items():
results_data.append([key, str(value)])

results_table = Table(results_data, colWidths=[200, 200])


results_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
]))

elements.append(results_table)
elements.append(Spacer(1, 12))

# Direct Shear
if "direct_shear" in self.test_results and self.test_results["direct_shear"]:
elements.append(Paragraph("Direct Shear Test Results", heading2_style))
elements.append(Spacer(1, 6))

for i, test in enumerate(self.test_results["direct_shear"]):


elements.append(Paragraph(f"Sample {i+1}: ID {test['sample_id']}, Depth
{test['depth']} m", styles['Heading3']))

# Results
if "results" in test:
results_data = [["Parameter", "Value"]]
for key, value in test["results"].items():
results_data.append([key, str(value)])

results_table = Table(results_data, colWidths=[200, 200])


results_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
]))

elements.append(results_table)
elements.append(Spacer(1, 12))

# Consolidation123
# Continuation of the report generation function for consolidation tests
if "consolidation" in self.test_results and self.test_results["consolidation"]:
elements.append(Paragraph("Consolidation Test Results", heading2_style))
elements.append(Spacer(1, 6))

for i, test in enumerate(self.test_results["consolidation"]):


elements.append(Paragraph(f"Sample {i+1}: ID {test['sample_id']}, Depth
{test['depth']} m", styles['Heading3']))

# Results
if "results" in test:
results_data = [["Parameter", "Value"]]
for key, value in test["results"].items():
results_data.append([key, str(value)])

results_table = Table(results_data, colWidths=[200, 200])


results_table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey),
('TEXTCOLOR', (0, 0), (-1, 0), colors.black),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 0), (-1, -1), 10),
('BOTTOMPADDING', (0, 0), (-1, -1), 6),
('GRID', (0, 0), (-1, -1), 0.5, colors.black)
]))

elements.append(results_table)
elements.append(Spacer(1, 12))

# Add conclusion
elements.append(Paragraph("Conclusions and Recommendations", heading2_style))
elements.append(Spacer(1, 6))
elements.append(Paragraph("This report provides a summary of laboratory test results
for the project. Additional analysis and interpretation may be required for specific design
purposes.", normal_style))

# Add footer with date and page numbers


footer_text = f"Report generated on {datetime.datetime.now().strftime('%Y-%m-%d')} |
GeotechLab Analysis Software"
elements.append(Spacer(1, 24))
elements.append(Paragraph(footer_text, ParagraphStyle(name='Footer',
parent=normal_style, fontSize=8, alignment=1)))

# Build the document


doc.build(elements)

messagebox.showinfo("Success", f"Report successfully generated and saved to


{file_path}")

except Exception as e:
messagebox.showerror("Error", f"Failed to generate report: {str(e)}")

def save_test_data(self, test_type):


"""Save test data to the project"""
# This is already implemented for grain_size, atterberg, and direct_shear
# Let's add the consolidation part
if test_type == "grain_size" or test_type == "atterberg" or test_type == "direct_shear":
# These cases were already implemented in the provided code
pass
elif test_type == "consolidation":
sample_id = self.cons_sample.get()
depth = self.cons_depth.get()

if not sample_id or not depth:


messagebox.showwarning("Warning", "Please enter Sample ID and Depth.")
return

# Create dictionary for consolidation data


cons_data = {
"sample_id": sample_id,
"depth": depth,
"test_data": [],
"results": {}
}

# Get test data


for pressure_entry, void_ratio_entry in self.cons_entries:
if pressure_entry.get().strip() and void_ratio_entry.get().strip():
test_point = {
"pressure": float(pressure_entry.get()),
"void_ratio": float(void_ratio_entry.get())
}
cons_data["test_data"].append(test_point)

# Get results
if self.compress_index.get().strip():
cons_data["results"]["compression_index"] = float(self.compress_index.get())

if self.recompress_index.get().strip():
cons_data["results"]["recompression_index"] = float(self.recompress_index.get())

if self.precons_pressure.get().strip():
cons_data["results"]["preconsolidation_pressure"] = float(self.precons_pressure.get())

if self.initial_void_ratio.get().strip():
cons_data["results"]["initial_void_ratio"] = float(self.initial_void_ratio.get())

# Store in test results


if "consolidation" not in self.test_results:
self.test_results["consolidation"] = []

self.test_results["consolidation"].append(cons_data)

messagebox.showinfo("Success", f"Consolidation test for Sample {sample_id} saved


successfully!")

def setup_triaxial_tab(self):
"""Set up the triaxial test tab"""
frame = ttk.Frame(self.triaxial_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)

# Left side - input fields


input_frame = ttk.LabelFrame(frame, text="Triaxial Test Data")
input_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=5, pady=5)

# Sample info
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)

ttk.Label(sample_frame, text="Sample ID:").pack(side=tk.LEFT, padx=5)


self.tx_sample = ttk.Entry(sample_frame, width=15)
self.tx_sample.pack(side=tk.LEFT, padx=5)

ttk.Label(sample_frame, text="Depth (m):").pack(side=tk.LEFT, padx=5)


self.tx_depth = ttk.Entry(sample_frame, width=10)
self.tx_depth.pack(side=tk.LEFT, padx=5)

# Test type
type_frame = ttk.Frame(input_frame)
type_frame.pack(fill=tk.X, pady=5)

ttk.Label(type_frame, text="Test Type:").pack(side=tk.LEFT, padx=5)


self.tx_type = ttk.Combobox(type_frame, width=20)
self.tx_type['values'] = ('UU - Unconsolidated Undrained', 'CU - Consolidated Undrained', 'CD
- Consolidated Drained')
self.tx_type.pack(side=tk.LEFT, padx=5)
self.tx_type.current(0)

# Test data
test_frame = ttk.LabelFrame(input_frame, text="Test Results for Each Cell Pressure")
test_frame.pack(fill=tk.X, pady=10)

# Headers
headers_frame = ttk.Frame(test_frame)
headers_frame.pack(fill=tk.X)

ttk.Label(headers_frame, text="Cell Pressure (kPa)").grid(row=0, column=0, padx=5, pady=2)


ttk.Label(headers_frame, text="Dev. Stress (kPa)").grid(row=0, column=1, padx=5, pady=2)
ttk.Label(headers_frame, text="Pore Press. (kPa)").grid(row=0, column=2, padx=5, pady=2)

# Create rows for test data


scrollable_frame = ttk.Frame(test_frame)
scrollable_frame.pack(fill=tk.BOTH, expand=True)

canvas = tk.Canvas(scrollable_frame)
scrollbar = ttk.Scrollbar(scrollable_frame, orient="vertical", command=canvas.yview)
scrollable_inner = ttk.Frame(canvas)

canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
canvas.create_window((0, 0), window=scrollable_inner, anchor="nw")

scrollable_inner.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)

# Create 6 rows for test data


self.tx_entries = []
for i in range(6):
cell_entry = ttk.Entry(scrollable_inner, width=15)
cell_entry.grid(row=i, column=0, padx=5, pady=2)

dev_entry = ttk.Entry(scrollable_inner, width=15)


dev_entry.grid(row=i, column=1, padx=5, pady=2)

pore_entry = ttk.Entry(scrollable_inner, width=15)


pore_entry.grid(row=i, column=2, padx=5, pady=2)

self.tx_entries.append((cell_entry, dev_entry, pore_entry))

# Results
results_frame = ttk.LabelFrame(input_frame, text="Triaxial Strength Parameters")
results_frame.pack(fill=tk.X, pady=10)

result_grid = ttk.Frame(results_frame)
result_grid.pack(fill=tk.X, padx=5, pady=5)

ttk.Label(result_grid, text="Cohesion (kPa):").grid(row=0, column=0, sticky=tk.W, padx=5,


pady=2)
self.tx_cohesion = ttk.Entry(result_grid, width=10)
self.tx_cohesion.grid(row=0, column=1, padx=5, pady=2)

ttk.Label(result_grid, text="Friction Angle (°):").grid(row=1, column=0, sticky=tk.W, padx=5,


pady=2)
self.tx_friction = ttk.Entry(result_grid, width=10)
self.tx_friction.grid(row=1, column=1, padx=5, pady=2)

ttk.Label(result_grid, text="Undrained Cohesion (kPa):").grid(row=2, column=0, sticky=tk.W,


padx=5, pady=2)
self.tx_cu = ttk.Entry(result_grid, width=10)
self.tx_cu.grid(row=2, column=1, padx=5, pady=2)

# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)

ttk.Button(btn_frame, text="Calculate", command=self.calculate_triaxial).pack(side=tk.LEFT,


padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("triaxial")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear", command=self.clear_triaxial_data).pack(side=tk.LEFT,
padx=5)

# Right side - visualization


viz_frame = ttk.LabelFrame(frame, text="Mohr Circles")
viz_frame.grid(row=0, column=1, sticky=tk.NSEW, padx=5, pady=5)

# Create matplotlib figure


self.tx_figure = plt.Figure(figsize=(6, 5), dpi=100)
self.tx_subplot = self.tx_figure.add_subplot(111)

# Create canvas
self.tx_canvas = FigureCanvasTkAgg(self.tx_figure, viz_frame)
self.tx_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)

def calculate_triaxial(self):
"""Calculate triaxial test parameters"""
try:
# Extract test data
data = []
for cell_entry, dev_entry, pore_entry in self.tx_entries:
if cell_entry.get().strip() and dev_entry.get().strip():
cell_pressure = float(cell_entry.get())
dev_stress = float(dev_entry.get())

# Pore pressure is optional for UU tests


if pore_entry.get().strip():
pore_pressure = float(pore_entry.get())
else:
pore_pressure = 0.0

data.append((cell_pressure, dev_stress, pore_pressure))

if len(data) < 2:
messagebox.showwarning("Warning", "Please enter at least two data points.")
return

# Get test type


test_type = self.tx_type.get()
# Calculate p and q values based on test type
p_values = []
q_values = []

for cell, dev, pore in data:


if "UU" in test_type:
# For UU tests, use total stresses
# p = (σ1 + σ3) / 2, where σ1 = σ3 + dev
sigma3 = cell
sigma1 = sigma3 + dev
p = (sigma1 + sigma3) / 2
q = (sigma1 - sigma3) / 2
elif "CU" in test_type:
# For CU tests, use effective stresses
# p' = (σ1' + σ3') / 2
sigma3_eff = cell - pore
sigma1_eff = sigma3_eff + dev
p = (sigma1_eff + sigma3_eff) / 2
q = (sigma1_eff - sigma3_eff) / 2
else: # CD test
# For CD tests, also use effective stresses
sigma3_eff = cell
sigma1_eff = sigma3_eff + dev
p = (sigma1_eff + sigma3_eff) / 2
q = (sigma1_eff - sigma3_eff) / 2

p_values.append(p)
q_values.append(q)

# Linear regression to find friction and cohesion parameters


# q = α + p * tan(ϕ)
p_array = np.array(p_values)
q_array = np.array(q_values)

slope, intercept, _, _, _ = stats.linregress(p_array, q_array)

# Convert to c and phi


# tanϕ = sin(ϕ) / cos(ϕ)
# α = c * cos(ϕ)
phi = np.degrees(np.arcsin(slope))
c = intercept / np.cos(np.radians(phi))

# For UU tests, this is Cu (undrained cohesion)


if "UU" in test_type:
cu = c
# Update undrained cohesion
self.tx_cu.delete(0, tk.END)
self.tx_cu.insert(0, f"{cu:.1f}")

# Clear effective parameters


self.tx_cohesion.delete(0, tk.END)
self.tx_friction.delete(0, tk.END)
else:
# Update effective parameters
self.tx_cohesion.delete(0, tk.END)
self.tx_cohesion.insert(0, f"{c:.1f}")

self.tx_friction.delete(0, tk.END)
self.tx_friction.insert(0, f"{phi:.1f}")

# Clear undrained cohesion


self.tx_cu.delete(0, tk.END)

# Plot Mohr circles


self.tx_subplot.clear()

# For each test


for i, (cell, dev, _) in enumerate(data):
# Calculate principal stresses
if "UU" in test_type:
sigma3 = cell
sigma1 = sigma3 + dev
else:
sigma3 = cell - (0 if "CD" in test_type else data[i][2])
sigma1 = sigma3 + dev

# Calculate center and radius of Mohr circle


center = (sigma1 + sigma3) / 2
radius = (sigma1 - sigma3) / 2

# Plot Mohr circle


circle = plt.Circle((center, 0), radius, fill=False, edgecolor=f'C{i}')
self.tx_subplot.add_artist(circle)

# Plot failure envelope


x_range = np.linspace(0, max([d[0] + d[1] for d in data]) * 1.2, 100)
if "UU" in test_type:
# For UU, just show horizontal line at Cu
self.tx_subplot.plot(x_range, [cu] * len(x_range), 'r-')
else:
# For CU/CD, show tau = c + sigma * tan(phi)
y_range = c + x_range * np.tan(np.radians(phi))
self.tx_subplot.plot(x_range, y_range, 'r-')

# Set axes labels and title


if "UU" in test_type:
self.tx_subplot.set_xlabel('Normal Stress (kPa)')
self.tx_subplot.set_ylabel('Shear Stress (kPa)')
self.tx_subplot.set_title('UU Triaxial Test - Total Stress')
elif "CU" in test_type:
self.tx_subplot.set_xlabel('Effective Normal Stress (kPa)')
self.tx_subplot.set_ylabel('Shear Stress (kPa)')
self.tx_subplot.set_title('CU Triaxial Test - Effective Stress')
else:
self.tx_subplot.set_xlabel('Effective Normal Stress (kPa)')
self.tx_subplot.set_ylabel('Shear Stress (kPa)')
self.tx_subplot.set_title('CD Triaxial Test - Effective Stress')

# Set equal aspect ratio and add grid


self.tx_subplot.set_aspect('equal')
self.tx_subplot.grid(True)

# Set y-axis to start at 0


self.tx_subplot.set_ylim(bottom=0)

# Refresh canvas
self.tx_canvas.draw()

messagebox.showinfo("Success", "Triaxial parameters calculated successfully!")


except ValueError as e:
messagebox.showerror("Error", f"Invalid numeric value: {str(e)}")
except Exception as e:
messagebox.showerror("Error", f"Calculation failed: {str(e)}")

def clear_triaxial_data(self):
"""Clear triaxial test data"""
# Clear test entries
for cell_entry, dev_entry, pore_entry in self.tx_entries:
cell_entry.delete(0, tk.END)
dev_entry.delete(0, tk.END)
pore_entry.delete(0, tk.END)
# Clear result entries
self.tx_cohesion.delete(0, tk.END)
self.tx_friction.delete(0, tk.END)
self.tx_cu.delete(0, tk.END)

# Clear sample ID and depth


self.tx_sample.delete(0, tk.END)
self.tx_depth.delete(0, tk.END)

# Reset test type


self.tx_type.current(0)

# Clear plot
self.tx_subplot.clear()
self.tx_subplot.set_xlabel('Normal Stress (kPa)')
self.tx_subplot.set_ylabel('Shear Stress (kPa)')
self.tx_subplot.set_title('Triaxial Test Results')
self.tx_canvas.draw()

def save_project(self):
"""Save all project data to a JSON file"""
if not hasattr(self, 'project_info') or not self.project_info:
messagebox.showwarning("Warning", "Please enter project information first.")
return

try:
# Ask for save location
file_path = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
title="Save Project As"
)

if not file_path:
return

# Prepare data to save


project_data = {
"project_info": self.project_info,
"test_results": self.test_results
}

# Save to file
with open(file_path, 'w') as f:
json.dump(project_data, f, indent=4)

messagebox.showinfo("Success", f"Project saved successfully to {file_path}")


except Exception as e:
messagebox.showerror("Error", f"Failed to save project: {str(e)}")

def load_project(self):
"""Load project data from a JSON file"""
try:
# Ask for file to open
file_path = filedialog.askopenfilename(
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
title="Open Project"
)

if not file_path:
return

# Load data from file


with open(file_path, 'r') as f:
project_data = json.load(f)

# Update project info and test results


if "project_info" in project_data:
self.project_info = project_data["project_info"]
self.update_project_info_display()

if "test_results" in project_data:
self.test_results = project_data["test_results"]

messagebox.showinfo("Success", f"Project loaded successfully from {file_path}")


except Exception as e:
messagebox.showerror("Error", f"Failed to load project: {str(e)}")

def update_project_info_display(self):
"""Update UI to show loaded project info"""
if hasattr(self, 'project_info') and self.project_info:
# Update project info display in the UI
if hasattr(self, 'project_name'):
self.project_name.delete(0, tk.END)
self.project_name.insert(0, self.project_info.get("name", ""))

if hasattr(self, 'project_number'):
self.project_number.delete(0, tk.END)
self.project_number.insert(0, self.project_info.get("number", ""))

if hasattr(self, 'project_location'):
self.project_location.delete(0, tk.END)
self.project_location.insert(0, self.project_info.get("location", ""))

if hasattr(self, 'project_client'):
self.project_client.delete(0, tk.END)
self.project_client.insert(0, self.project_info.get("client", ""))

if hasattr(self, 'project_engineer'):
self.project_engineer.delete(0, tk.END)
self.project_engineer.insert(0, self.project_info.get("engineer", ""))

if hasattr(self, 'project_date'):
self.project_date.delete(0, tk.END)
self.project_date.insert(0, self.project_info.get("date", ""))

def about_dialog(self):
"""Show about dialog"""
about_window = tk.Toplevel(self.master)
about_window.title("About GeotechLab")
about_window.geometry("400x300")
about_window.resizable(False, False)

# Center window
about_window.update_idletasks()
x = (about_window.winfo_screenwidth() - about_window.winfo_width()) // 2
y = (about_window.winfo_screenheight() - about_window.winfo_height()) // 2
about_window.geometry(f"+{x}+{y}")

# Content
frame = ttk.Frame(about_window, padding="20")
frame.pack(fill=tk.BOTH, expand=True)

ttk.Label(frame, text="GeotechLab", font=("Arial", 16, "bold")).pack(pady=10)


ttk.Label(frame, text="Geotechnical Laboratory Test Analysis Software", font=("Arial",
10)).pack()
ttk.Label(frame, text="Version 1.0").pack(pady=10)
ttk.Label(frame, text="© 2025 Geotechnical Engineering Solutions").pack()
ttk.Label(frame, text="For educational and professional use").pack(pady=10)

ttk.Button(frame, text="Close", command=about_window.destroy).pack(pady=10)

You might also like