diff --git a/.github/workflows/testing_main_dev.yml b/.github/workflows/testing_main_dev.yml new file mode 100644 index 0000000..1d96ec2 --- /dev/null +++ b/.github/workflows/testing_main_dev.yml @@ -0,0 +1,48 @@ +name: Automated testing and coverage (in protected branches) + + +on: + push: + branches: [ "main", "dev" ] + workflow_dispatch: + + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest coverage coverage-badge + + - name: Run tests with coverage + run: | + coverage run --source=pv_tool -m pytest + coverage report -m +# if [ -f readme_images/coverage.svg ]; then rm readme_images/coverage.svg; fi +# coverage-badge -o readme_images/coverage.svg + +# - name: Commit changes +# run: | +# git config --local user.email "action@github.com" +# git config --local user.name "GitHub Actions" +# git pull +# git add . +# if ! git diff --cached --quiet; then +# git commit -m "Testing and coverage" +# git push +# else +# echo "No changes to commit" +# fi +# env: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/testing_pull_request.yml b/.github/workflows/testing_pull_request.yml new file mode 100644 index 0000000..865861d --- /dev/null +++ b/.github/workflows/testing_pull_request.yml @@ -0,0 +1,31 @@ +name: Automated testing and coverage (during pull request) + + +on: + pull_request: + branches: [ "dev" ] + workflow_dispatch: + + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.12' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + pip install pytest coverage coverage-badge + + - name: Run tests with coverage + run: | + coverage run --source=pv_tool -m pytest + coverage report -m diff --git a/.gitignore b/.gitignore index b61b84e..cf535e5 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,3 @@ dmypy.json data/output # GitHub specific -.github/ diff --git a/example_files/Template_PVtool5_0.xlsx b/example_files/Template_PVtool5_0.xlsx index cba1275..386b4b8 100644 Binary files a/example_files/Template_PVtool5_0.xlsx and b/example_files/Template_PVtool5_0.xlsx differ diff --git a/example_files/Template_PVtool5_0_aanpassingen_handmatig.xlsx b/example_files/Template_PVtool5_0_aanpassingen_handmatig.xlsx new file mode 100644 index 0000000..2aead13 Binary files /dev/null and b/example_files/Template_PVtool5_0_aanpassingen_handmatig.xlsx differ diff --git a/example_files/Template_PVtool5_0_backup.xlsx b/example_files/Template_PVtool5_0_backup.xlsx new file mode 100644 index 0000000..dcae356 Binary files /dev/null and b/example_files/Template_PVtool5_0_backup.xlsx differ diff --git a/example_files/Template_PVtool5_0_output_van_pvtool_import.xlsx b/example_files/Template_PVtool5_0_output_van_pvtool_import.xlsx new file mode 100644 index 0000000..58f9cbb Binary files /dev/null and b/example_files/Template_PVtool5_0_output_van_pvtool_import.xlsx differ diff --git a/example_files/Template_PVtool5_0_met_voorbeeld_resultaten.xlsx b/example_files/Template_PVtool5_0_zonder_resultaten.xlsx similarity index 93% rename from example_files/Template_PVtool5_0_met_voorbeeld_resultaten.xlsx rename to example_files/Template_PVtool5_0_zonder_resultaten.xlsx index ac17dfb..cba1275 100644 Binary files a/example_files/Template_PVtool5_0_met_voorbeeld_resultaten.xlsx and b/example_files/Template_PVtool5_0_zonder_resultaten.xlsx differ diff --git a/main_nathan.py b/main_nathan.py index 80acc33..e6604d6 100644 --- a/main_nathan.py +++ b/main_nathan.py @@ -1,30 +1,48 @@ +""" +Test script voor PV-tool functionaliteiten. + +Dit script bevat verschillende test cases voor de PV-tool, inclusief: +- Repository root bepaling +- Database import en validatie +- C-phi analyses (regulier en schematiseringshandleiding) +""" + import os -from pv_tool.imports.validation import Validation +from pathlib import Path from typing import Optional import git +from pv_tool.imports.validation import Validation +from pv_tool.imports.import_data import Dbase +from pv_tool.cphi_analysis.c_phi_analysis import CPhiAnalyse +from pv_tool.imports.import_options import * def get_repo_root(root_search_dir: Optional[str] = None) -> str: - """Returns the repository root by searching in the given directory and its subdirectories. - - :param root_search_dir: The given directory in which it will search for the repository root. It will also - search in the subdirectories of this given directory. If not provided (i.e. None) then function will use - os.getcwd(). - :return: """ - - # Determine search directory + Bepaalt de repository root door te zoeken in de gegeven directory en zijn subdirectories. + + Parameters + ---------- + root_search_dir : str, optioneel + De directory waarin gezocht moet worden naar de repository root. + Als niet opgegeven (None) wordt os.getcwd() gebruikt. + + Returns + ------- + str + Pad naar de repository root + """ if root_search_dir is None: root_search_dir = os.getcwd() - # Initial search at that directory + # Zoek eerst in de opgegeven directory try: repo = git.Repo(root_search_dir, search_parent_directories=False) return repo.working_tree_dir except git.InvalidGitRepositoryError: pass - # After that subdirectories + # Zoek vervolgens in subdirectories for subdir, dirs, files in os.walk(os.getcwd()): for directory in dirs: try: @@ -33,97 +51,175 @@ def get_repo_root(root_search_dir: Optional[str] = None) -> str: except git.InvalidGitRepositoryError: continue - # Last resort: search parent directories + # Als laatste optie: zoek in parent directories repo = git.Repo(root_search_dir, search_parent_directories=True) return repo.working_tree_dir -## Test the import + validate -from pv_tool.imports.import_options import * -from pv_tool.imports.import_data import Dbase +def test_database_import(): + """Test de database import en validatie functionaliteit.""" + repo_root = Path(get_repo_root()) + path_to_data = repo_root / "example_files" / "Template_PVtool5_0.xlsx" + save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Test output") + + # Database import + dbase = Dbase() + dbase.import_data(source='Dbase', source_dir=path_to_data) + + # Print unieke verzamelingen + print('\nUnieke verzamelingen:') + for pvnaam in dbase.dbase_df['PV_NAAM'].unique(): + print(pvnaam) + + return dbase + + +def test_cphi_analysis_txt(dbase: Dbase): + """ + Test een TXT C-phi analyse. + + Parameters + ---------- + dbase : Dbase + Database instance met testdata + """ + save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Test output") + + # Initialiseer analyse + analyse = CPhiAnalyse( + dbase=dbase, + investigation_groups=['TXT_SAFE_klei_licht_16_175'], + effective_stress='15% rek', + analysis_type='TXT_CPhi' + ) + + # Pas instellingen toe + analyse.apply_settings(alpha=0.75) + analyse.apply_parameters(cohesie_kar=0) + + # Print en exporteer resultaten + print('\nResultaten TXT C-phi analyse:') + print(analyse.print_short_results()) + analyse.add_results_to_dbase(path=str(save_test)) + + # Visualisatie + analyse.show_figure() + analyse.save_to_pdf(path=str(save_test)) + + +def test_cphi_analysis_dss(dbase: Dbase): + """ + Test een DSS C-phi analyse. + + Parameters + ---------- + dbase : Dbase + Database instance met testdata + """ + save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Test output") + + # Initialiseer analyse + analyse = CPhiAnalyse( + dbase=dbase, + investigation_groups=['DSS_SAFE_veen'], + effective_stress='20% rek', + analysis_type='DSS_CPhi' + ) + + # Pas instellingen toe + analyse.apply_settings(alpha=0.75) + + # Print en exporteer resultaten + print('\nResultaten DSS C-phi analyse:') + print(analyse.print_short_results()) + analyse.add_results_to_dbase(path=str(save_test)) + + # Visualisatie + analyse.show_figure() + analyse.save_to_pdf(path=str(save_test)) + + +def test_cphi_analysis_txt_sh(dbase: Dbase): + """ + Test een TXT C-phi analyse volgens schematiseringshandleiding (SH). + + Parameters + ---------- + dbase : Dbase + Database instance met testdata + """ + save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Test output") + + # Initialiseer analyse + analyse = CPhiAnalyse( + dbase=dbase, + investigation_groups=['TXT_SAFE_klei_licht_16_175'], + effective_stress='15% rek', + analysis_type='TXT_SH' # SH = schematiseringshandleiding + ) + + # Pas instellingen toe + analyse.apply_settings(alpha=0.75) + + # Print en exporteer resultaten + print('\nResultaten TXT C-phi analyse (schematiseringshandleiding):') + print(analyse.print_short_results()) + analyse.add_results_to_dbase(path=str(save_test)) + + # Visualisatie + analyse.show_figure() + analyse.save_to_pdf(path=str(save_test)) + + +def test_cphi_analysis_dss_sh(dbase: Dbase): + """ + Test een DSS C-phi analyse volgens schematiseringshandleiding (SH). + + Parameters + ---------- + dbase : Dbase + Database instance met testdata + """ + save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Test output") + + # Initialiseer analyse + analyse = CPhiAnalyse( + dbase=dbase, + investigation_groups=['DSS_SAFE_veen'], + effective_stress='20% rek', + analysis_type='DSS_SH' # SH = schematiseringshandleiding + ) + + # Pas instellingen toe + analyse.apply_settings(alpha=0.75) + + # Print en exporteer resultaten + print('\nResultaten DSS C-phi analyse (schematiseringshandleiding):') + print(analyse.print_short_results()) + analyse.add_results_to_dbase(path=str(save_test)) + + # Visualisatie + analyse.show_figure() + analyse.save_to_pdf(path=str(save_test)) + + +if __name__ == "__main__": + # Test database import + dbase = test_database_import() + + # Test verschillende analyses + print("\nUitvoeren van verschillende test cases...") + + print("\n1. TXT C-phi analyse test") + test_cphi_analysis_txt(dbase) + # print("\n2. DSS C-phi analyse test") + # test_cphi_analysis_dss(dbase) + # + # print("\n3. TXT C-phi analyse test (schematiseringshandleiding)") + # test_cphi_analysis_txt_sh(dbase) + # + # print("\n4. DSS C-phi analyse test (schematiseringshandleiding)") + # test_cphi_analysis_dss_sh(dbase) -path_to_data = Path(get_repo_root()) / "example_files" / "SAFE 2022 Proevenverzameling_tool_v4.2n_test_zonder_functies.xlsm" -# path_to_data = Path(get_repo_root()) / "example_files" / "SAFE 2022 Proevenverzameling_tool_v4.2n_test_zonder_functies.xlsm" -# path_to_data = Path(get_repo_root()) / "example_files" / "Dbase-template.xlsx" - -save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Test output") - -dbase = Dbase() -dbase.import_data_and_validate(source='PV-tool', source_dir=path_to_data, export_path=save_test) - -dbase.export_dbase_to_excel(export_dir=save_test) - -# ## Test invoegen analyse kolommmen handmatig -# from pv_tool.imports.import_options import * -# from pv_tool.imports.import_data import Dbase -# -# -# # path_to_data = Path(get_repo_root()) / "example_files" / "SAFE 2022 Proevenverzameling_tool_v4.2n_test_zonder_functies.xlsm" -# # path_to_data = Path(get_repo_root()) / "example_files" / "SAFE 2022 Proevenverzameling_tool_v4.2n_test_zonder_functies.xlsm" -# # path_to_data = Path(get_repo_root()) / "example_files" / "Dbase-template.xlsx" -# -# path_to_data = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie\Template_PVtool5_0 voorstel ana kolommen.xlsx") -# -# save_test = Path(r"c:\Users\gebraadn0645\ARCADIS\103076457 - STOWA PV Tool - 05 Project execution\Deliverables\2. validatie") -# -# dbase = Dbase() -# dbase.import_data_and_validate(source='Dbase', source_dir=path_to_data, export_path=save_test) -# -# dbase.export_dbase_to_excel(export_dir=save_test) - - -## -print('Unieke verzamelingen:') -for pvnaam in dbase.dbase_df['PV_NAAM'].unique(): - print(pvnaam) - - -## initiate cphi or dss analysis with different options -# CPHI-analyse -from pv_tool.cphi_analysis.c_phi_analysis import * -from pv_tool.cphi_analysis.variables import * - -# analyse = CPhiAnalyse(dbase=dbase, investigation_groups=['DSS_SAFE_veen'], effective_stress='20% rek', -# analysis_type='DSS_CPhi') - -analyse = CPhiAnalyse(dbase=dbase, investigation_groups=['TXT_SAFE_klei_licht_16_175'], effective_stress='15% rek', - analysis_type='TXT_CPhi') -# -# analyse = CPhiAnalyse(dbase=dbase, investigation_groups=['DSS_SAFE_veen'], effective_stress='20% rek', -# analysis_type='DSS_SH') - -# analyse = CPhiAnalyse(dbase=dbase, investigation_groups=['TXT_SAFE_klei_licht_16_175'], effective_stress='15% rek', -# analysis_type='TXT_SH') - -analyse.apply_settings(alpha=0.75) -# analyse.apply_parameters(cohesie_gem=8, phi_kar=0.53, cohesie_kar=6.72) - -# ## -# print(analyse.get_previous_results(path = str(save_test))) -# print(analyse.print_short_results()) -# analyse.add_results_to_dbase(path = str(save_test)) -# -# analyse.save_to_pdf(path = str(save_test)) -# analyse.show_figure() - -## -analyse.apply_parameters(cohesie_kar = 0) - -print(analyse.print_short_results()) -analyse.add_results_to_dbase(path = str(save_test)) - -analyse.save_to_pdf(path = str(save_test)) -analyse.show_figure() - -# ## -# analyse = CPhiAnalyse(dbase=dbase, investigation_groups=['DSS_SAFE_veen'], effective_stress='20% rek', -# analysis_type='DSS_SH') -# -# analyse.apply_settings(alpha=0.75) -# -# ## -# print(analyse.print_short_results()) -# analyse.add_results_to_dbase(path = str(save_test)) -# analyse.save_to_pdf(path = str(save_test)) -# analyse.show_figure() + print("\nAlle tests zijn voltooid!") diff --git a/pv_tool/cphi_analysis/c_phi_analysis.py b/pv_tool/cphi_analysis/c_phi_analysis.py index 5179036..a5ac8ee 100644 --- a/pv_tool/cphi_analysis/c_phi_analysis.py +++ b/pv_tool/cphi_analysis/c_phi_analysis.py @@ -1,10 +1,9 @@ -from operator import index from typing import Optional, List, Literal from datetime import datetime from pandas import DataFrame, ExcelWriter, concat, read_excel, isna - from pv_tool.cphi_analysis.globals import (TEXTUAL_NAMES, ALL_TEXTUAL_NAMES, NEW_COLUMN_NAMES, TEXTUAL_NAMES_DSS, ALL_TEXTUAL_NAMES_DSS) +from pv_tool.cphi_analysis.save_and_export import save_total_to_excel, save_to_pdf from pv_tool.imports.import_data import Dbase import plotly.graph_objects as go @@ -29,18 +28,7 @@ calc_a2_phi_gem_sh, calc_a2_phi_kar_boven_sh, calc_a2_phi_kar_onder_sh, calc_tan_phi_kar_sh) from openpyxl import load_workbook - - -from reportlab.lib import colors - -from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet -from reportlab.lib.enums import TA_LEFT -from reportlab.lib.pagesizes import A4, landscape -from reportlab.platypus import SimpleDocTemplate, Table, LongTable, TableStyle, Paragraph, Spacer, Image - -from openpyxl.worksheet.table import Table as XLTable, TableStyleInfo -from openpyxl.utils import get_column_letter - +from pv_tool.imports.excel_utils import format_excel_sheet class CPhiAnalyse: """ @@ -216,12 +204,14 @@ def apply_parameters(self, cohesie_gem: Optional[float] = None, def plot_spanningspaden(self): """ - Plot de spanningspaden voor alle beschikbare effective stress waarden + Initieert de spanningspaden voor alle beschikbare effective stress waarden binnen de geselecteerde investigation groups. Dit helpt bij het visualiseren van de spanningsveranderingen tijdens de proeven. Voor elk monster (uit de index van de dataframes) wordt een apart spanningspad gemaakt met alle beschikbare spanningsstappen. + + Wordt aangeroepen binnen set_figure(). Het is aan de gebruiker om de spanningspaden wel of niet toe te voegen aan de figuur """ if self.analysis_type in ['TXT_CPhi', 'TXT_SH']: relevant_df = self.dbase_df[self.dbase_df['ALG__TRIAXIAAL']] @@ -565,7 +555,7 @@ def print_short_results(self): analyse_output_df['cohesie [kPa]'] = [self.c_gem, self.c_kar, self.c_d, self.st_dev_c] return analyse_output_df - def add_results_to_dbase(self, path): # TODO check bij Leo of a1 en a2 gem en goed weg geschreven worden als handmatige waardes + def add_results_to_dbase(self, path): """ Voegt analyseresultaten toe aan de database export. @@ -644,7 +634,10 @@ def add_results_to_dbase(self, path): # TODO check bij Leo of a1 en a2 gem en g if 'Resultaten' in workbook.sheetnames: print('Tabblad resultaten in dbase excel bestaat al en wordt aangevuld') df_existing = read_excel(file_path, sheet_name='Resultaten') - df_updated = concat([df_existing, DataFrame([new_row])], ignore_index=True) + # Filter out empty rows and ensure consistent types before concatenation + df_existing = df_existing.dropna(how='all') + new_row_df = DataFrame([new_row], columns=df_existing.columns) + df_updated = concat([df_existing, new_row_df], ignore_index=True) else: print('Tabblad resultaten in dbase excel bestaat nog niet en wordt aangemaakt') df_updated = DataFrame([new_row], columns=expected_columns) @@ -656,366 +649,21 @@ def add_results_to_dbase(self, path): # TODO check bij Leo of a1 en a2 gem en g # Formatting num_columns = df_updated.shape[1] num_rows = df_updated.shape[0] - self.format_excel_sheet( + format_excel_sheet( file_path=file_path, sheet_name='Resultaten', num_columns=num_columns, num_rows=num_rows, - table_name='ResultatenTable' + table_name='ResultatenTable', + index=False # Changed from True to False to match the to_excel call ) return df_updated - @staticmethod - def format_excel_sheet(file_path: str, sheet_name: str, num_columns: int, num_rows: int, table_name: str = None): - """ - Formatteert een Excel werkblad als een tabel met filters en aangepaste kolombreedtes. - - Parameters - ---------- - file_path : str - Het volledige pad naar het Excel bestand - sheet_name : str - Naam van het werkblad dat geformatteerd moet worden - num_columns : int - Aantal kolommen in de tabel - num_rows : int - Aantal rijen in de tabel (exclusief de header) - table_name : str, optioneel - Naam voor de Excel tabel. Als None wordt opgegeven, wordt sheet_name + "Table" gebruikt. - """ - - workbook = load_workbook(file_path) - worksheet = workbook[sheet_name] - - # Auto-adjust column widths based on content - for column in worksheet.columns: - max_length = 0 - column_letter = get_column_letter(column[0].column) - - for cell in column: - if cell.value: - max_length = max(max_length, len(str(cell.value))) - - adjusted_width = max_length + 2 - worksheet.column_dimensions[column_letter].width = adjusted_width - - # Define table range - table_range = f"A1:{get_column_letter(num_columns)}{num_rows + 1}" - - # Create table with filters - if table_name is None: - table_name = f"{sheet_name}Table" - - # Remove spaces and special characters from table name - table_name = "".join(c for c in table_name if c.isalnum()) - - table = XLTable(displayName=table_name, ref=table_range) - - # Add a default style - style = TableStyleInfo( - name="TableStyleMedium2", - showFirstColumn=False, - showLastColumn=False, - showRowStripes=True, - showColumnStripes=False - ) - table.tableStyleInfo = style - - # Remove existing table if it exists - for existing_table in worksheet.tables.values(): - if existing_table.name == table_name: - del worksheet.tables[existing_table.name] - break - - # Add the table to the worksheet - worksheet.add_table(table) - - workbook.save(file_path) - - def save_total_to_excel(self, path): - """ - Exporteert alle analysegegevens naar Excel. - - Slaat de volledige dataset met alle berekende kolommen op in een Excel bestand. - De bestandsnaam wordt automatisch gegenereerd op basis van de analyse-instellingen. - - Parameters - ---------- - path : str - Map locatie waar het Excel-bestand moet worden opgeslagen - """ - # pas de effective stress naam aan zodat het weggeschreven kan worden in de bestandsnaam - effective_stress = str(self.effective_stress).replace('%', 'procent_') - effective_stress = str(effective_stress).replace(' ', '') - - # exporteer onder de juiste naam - file_name = f"c_phi_export_test_{self.investigation_groups[0]}_{self.analysis_type}_{effective_stress}.xlsx" - file_path = f"{path}/{file_name}" - - # Hernoem de kolommen voor een ander analyse type - if self.analysis_type in ['DSS_CPhi', 'DSS_SH']: - self.cphi_analyses_data_df = self.cphi_analyses_data_df.rename(columns={'S\'': '\u03C3 \'', 'T': '\u03C4'}) - - # schrijf het totaal weg - df_totaal = self.cphi_analyses_data_df - with ExcelWriter(file_path, engine='openpyxl') as writer: - df_totaal.to_excel(writer) - - @staticmethod - def _df_to_table_with_index(df, index_name='Index'): - """ - Zet een DataFrame om naar een lijst voor gebruik in een PDF tabel. Gebruikt in save_to_pdf. - - Parameters - ---------- - df : DataFrame - De DataFrame die moet worden omgezet - index_name : str, optioneel - Naam voor de index kolom (default='Index') - - Returns - ------- - list - Lijst met header en data rijen voor een PDF tabel - """ - header = [df.index.name or index_name] + df.columns.tolist() - data = [[idx] + row.tolist() for idx, row in df.iterrows()] - return [header] + data - - def _create_input_table(self) -> Table: - """ - Maakt een tabel met de invoerselectie informatie. Gebruikt in save_to_pdf. - - Returns - ------- - Table - ReportLab tabel object met de invoerselectie informatie - """ - columns_base = [ - 'PV_NAAM', 'BORING_POSITIE', 'MONSTER_NIVEAU_NAP_VANAF', 'MONSTER_NIVEAU_NAP_TOT' - ] - if self.analysis_type in ['TXT_CPhi', 'TXT_SH']: - columns_extra = ['TXT_SS_VOLUMEGEWICHT_NAT', 'TXT_SS_VOLUMEGEWICHT_DRG', 'TXT_SS_WATERGEHALTE_VOOR'] - else: - columns_extra = ['DSS_VOLUMEGEWICHT_NAT', 'DSS_VOLUMEGEWICHT_DRG', 'DSS_WATERGEHALTE_VOOR'] - - columns_data = self.cphi_analyses_data_df.iloc[:, 1:3].copy() - table1_cols = columns_base + columns_extra - table1_df = self.total_cphi_analyses_data_df[table1_cols].copy() - table1_df.columns = ['Groep', 'Positie', 'NAP Vanaf [m]', 'NAP Tot [m]', 'VGW nat', 'VGW droog', 'Watergehalte voor'] - table1_df = concat([table1_df, columns_data], axis=1) - table1_df = table1_df.map(lambda x: f"{x:.2f}" if isinstance(x, (float, int)) else x) - - t1_data = self._df_to_table_with_index(table1_df, index_name="alg_boring_monsternummer_id") - t1 = LongTable(t1_data, repeatRows=1, hAlign='LEFT') - t1.setStyle(TableStyle([ - - ('ALIGN', (0,0), (-1,-1), 'LEFT'), - ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), - ('GRID', (0, 0), (-1, -1), 0.5, colors.black), - ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), - ('FONTSIZE', (0, 0), (-1, 0), 9), - ('FONTSIZE', (0, 1), (-1, -1), 8), - ])) - return t1 - - def _create_initial_values_table(self) -> Table: - """ - Maakt een tabel met de initiële waarden van de analyse. Gebruikt in save_to_pdf. - - Returns - ------- - Table - ReportLab tabel object met de initiële waarden - """ - # name_gem_a1 = 'a1 gem = snijpunt y-as (cohesie gemiddeld)' if self.cohesie_gem_handmatig is None else 'a1 gem = cohesie gemiddeld (handmatig)' - # name_gem_a2 = 'a2 gem = tan(phi) gemiddeld' - # name_kar_a1 = 'a1 kar = snijpunt y-as (cohesie karakteristiek)' if self.cohesie_kar_handmatig is None else 'a1 kar = cohesie karakteristiek (handmatig)' - # name_kar_a2 = 'a2 kar = tan(phi) karakteristiek' if self.phi_kar_handmatig is None else 'a2 kar = tan(phi) karakteristiek (handmatig)' - name_phi_kar_onder = 'a2 kar onder = tan(phi) karakteristiek ondergrens' - name_phi_kar_boven = 'a2 kar boven = tan(phi) karakteristiek bovengrens' - - initial_values = [] - - if self.cohesie_gem_handmatig is not None: initial_values.append(['a1 gem = cohesie gemiddeld (handmatig)', round(self.cohesie_gem_handmatig,3)]) - elif self.gem_a1 is not None: initial_values.append(['a1 gem = snijpunt y-as (cohesie gemiddeld)', round(self.gem_a1,3)]) - - if self.gem_a2 is not None: initial_values.append(['a2 gem = tan(phi) gemiddeld', round(self.gem_a2,3)]) - - if self.cohesie_kar_handmatig is not None: initial_values.append(['a1 kar = cohesie karakteristiek (handmatig)', round(self.cohesie_kar_handmatig,3)]) - elif self.kar_a1 is not None: initial_values.append(['a1 kar = snijpunt y-as (cohesie karakteristiek)' , round(self.kar_a1,3)]) - - if self.phi_kar_handmatig is not None: initial_values.append(['a2 kar = tan(phi) karakteristiek (handmatig)', round(self.phi_kar_handmatig,3)]) - elif self.kar_a2 is not None: initial_values.append(['a2 kar = tan(phi) karakteristiek', round(self.kar_a2,3)]) - - if hasattr(self, 'a2_phi_kar_onder') and self.a2_phi_kar_onder is not None: - initial_values.append([name_phi_kar_onder, round(self.a2_phi_kar_onder,3)]) - if hasattr(self, 'a2_phi_kar_boven') and self.a2_phi_kar_boven is not None: - initial_values.append([name_phi_kar_boven, round(self.a2_phi_kar_boven,3)]) - - - initial_values.append(['Type verzameling: lokaal = 1.0; regionaal = 0.75', self.alpha]) - initial_values.append(['Partiële materiaalfactor cohesie [-]', self.material_cohesie]) - initial_values.append(['Partiële materiaalfactor tan phi [-]', self.material_tan_phi]) - - t3 = Table([['Parameter', 'Waarde']] + initial_values, hAlign='LEFT') - t3.setStyle(TableStyle([ - ('ALIGN', (0,0), (-1,-1), 'LEFT'), - ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), - ('GRID', (0, 0), (-1, -1), 0.5, colors.black), - ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), - ])) - return t3 - - def _create_results_table(self) -> Table: - """ - Maakt een tabel met de eindresultaten van de analyse. Gebruikt in save_to_pdf. - - Returns - ------- - Table - ReportLab tabel object met de resultaten - """ - output_table_df = self.print_short_results().copy() - output_table_df.index.name = 'Parameter' - output_table_df = output_table_df.map(lambda x: f"{x:.2f}" if isinstance(x, (float, int)) else x) - output_table_data = self._df_to_table_with_index(output_table_df) - output_table = Table(output_table_data, repeatRows=1, hAlign='LEFT') - output_table.setStyle(TableStyle([ - ('ALIGN', (0,0), (-1,-1), 'LEFT'), - ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), - ('GRID', (0, 0), (-1, -1), 0.5, colors.black), - ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), - ])) - return output_table - - def _get_manual_values_paragraphs(self, styles) -> list: - """ - Maakt een lijst van paragrafen met handmatig opgegeven waarden. - - Parameters - ---------- - styles : dict - ReportLab stylesheet met opmaakstijlen - - Returns - ------- - list - Lijst met ReportLab Paragraph objecten - """ - paragraphs = [] - manual_texts = [] - if self.cohesie_gem_handmatig is not None: - manual_texts.append(f"handmatig opgegeven: cohesie_gem_handmatig = {self.cohesie_gem_handmatig}") - if self.phi_kar_handmatig is not None: - manual_texts.append(f"handmatig opgegeven: phi_kar_handmatig = {self.phi_kar_handmatig}") - if self.cohesie_kar_handmatig is not None: - manual_texts.append(f"handmatig opgegeven: cohesie_kar_handmatig = {self.cohesie_kar_handmatig}") - - if manual_texts: - paragraphs.append(Paragraph("Handmatig opgegeven waarden:", styles['Heading3'])) - for txt in manual_texts: - paragraphs.append(Paragraph(txt, styles['Normal'])) - else: - paragraphs.append(Paragraph("Geen handmatig opgegeven waarden, figuur gebaseerd op eerste inschatting", styles['Normal'])) - - return paragraphs - - def save_to_pdf(self, path: str) -> str: - """ - Slaat de analyseresultaten op in een PDF-document, inclusief figuren, datatabellen en numerieke resultaten. - - De PDF bevat: - - Titel met analysedetails - - Overzichtsfiguur van de analyse - - Tabel met invoerselectie informatie - - Tabel met initiële waarden - - Eventueel handmatig opgegeven waarden - - Tabel met eindresultaten - - Parameters - ---------- - path : str - Map locatie waar het PDF-bestand moet worden opgeslagen - - Returns - ------- - str - Het absolute bestandspad van het aangemaakte PDF-bestand - """ - # Maak titel en bestandsnaam - title = f'{self.analysis_type.split('_')[0]} {self.analysis_type.split('_')[1]} analyse met {self.effective_stress} op {self.investigation_groups[0]}' - file_name = f"c_phi_pdf_export_{self.investigation_groups[0]}_{self.analysis_type}_{str(self.effective_stress).replace('%', 'procent_').replace(' ', '')}.pdf" - file_path = f"{path}/{file_name}" - - # Maak en bewaar de figuur alleen als deze nog niet bestaat - fig_path = f"{path}/temp_plot.png" - if not hasattr(self, 'figure') or len(self.figure.data) == 0: - self.show_title = False - self.show_figure() - - self.show_title = True - fig_width = 1280 - fig_height = 720 - self.figure.write_image(fig_path, width=fig_width, height=fig_height, scale=4, format="png") - - # Maak het PDF document - doc = SimpleDocTemplate(file_path, pagesize=landscape(A4)) - styles = getSampleStyleSheet() - styles.add(ParagraphStyle(name='Left', parent=styles['Normal'], alignment=TA_LEFT)) - styles.add(ParagraphStyle(name='TitleLeft', parent=styles['Title'], alignment=TA_LEFT)) - story = [] - - # Voeg titel toe - story.append(Paragraph(title, styles['TitleLeft'])) - story.append(Spacer(width=1, height=12)) - - # Voeg figuur toe met aangepaste grootte - from PIL import Image as PILImage - from reportlab.platypus import Image as RLImage - - fig_path = f"{path}/temp_plot.png" - - # Laad PNG en bepaal pixelafmetingen - with PILImage.open(fig_path) as im: - img_width_px, img_height_px = im.size - - # Stel gewenste breedte in punten (bijv. 95% van PDF breedte) - max_width_pt = doc.width * 0.95 - - # Bereken hoogte zodat verhouding gelijk blijft - aspect = img_height_px / img_width_px - img_width_pt = min(max_width_pt, doc.width) # niet breder dan pagina - img_height_pt = img_width_pt * aspect - - # Maak ReportLab Image aan - img = RLImage(fig_path) - img.drawWidth = img_width_pt - img.drawHeight = img_height_pt - img.hAlign = 'LEFT' - - story.append(img) - story.append(Spacer(width=1, height=12)) - - # Voeg initiële waarden toe - story.append(Paragraph("Parameter bepaling fysisch realiseerbare ondergrens en gemiddelde waarden", styles['Heading2'])) - story.append(self._create_initial_values_table()) - story.append(Spacer(1, 12)) - - # Voeg resultaten toe - story.append(Paragraph("Resultaten", styles['Heading2'])) - story.append(self._create_results_table()) - story.append(Spacer(1, 12)) - - # Voeg invoertabel toe - story.append(Paragraph("Informatietabel invoerselectie", styles['Heading2'])) - story.append(self._create_input_table()) - story.append(Spacer(1, 12)) - - # Bouw de PDF - doc.build(story) - - print(f"PDF succesvol opgeslagen op: {file_path}") - return file_path + @property + def save_total_to_excel(self): + return lambda path: save_total_to_excel(self, path) + @property + def save_to_pdf(self): + return lambda path: save_to_pdf(self, path) diff --git a/pv_tool/cphi_analysis/expand_analysis_df.py b/pv_tool/cphi_analysis/expand_analysis_df.py index 00524df..c8f0706 100644 --- a/pv_tool/cphi_analysis/expand_analysis_df.py +++ b/pv_tool/cphi_analysis/expand_analysis_df.py @@ -1,5 +1,6 @@ from __future__ import annotations from typing import TYPE_CHECKING +import numpy as np if TYPE_CHECKING: from pv_tool.cphi_analysis.c_phi_analysis import CPhiAnalyse @@ -7,14 +8,17 @@ def calculate_tan_a(self: CPhiAnalyse): """Berekent de tangens van hoek a voor alle rijen in de DataFrame.""" - formule = self.cphi_analyses_data_df['T'] / self.cphi_analyses_data_df['S\''] - self.cphi_analyses_data_df['tan(a)'] = formule + # Create a copy to avoid chained assignment + df = self.cphi_analyses_data_df.copy() + df.loc[:, 'tan(a)'] = df['T'] / df['S\''] + self.cphi_analyses_data_df = df def calculate_ln_tan_a(self: CPhiAnalyse): """Berekent de natuurlijke logaritme van tan(a) voor alle rijen in de DataFrame.""" - self.cphi_analyses_data_df['LN(tan(a))'] = ( - self.cphi_analyses_data_df['tan(a)'].apply - (lambda x: np.log(x) if x is not None and x > 0 else "")) + # Create a copy to avoid chained assignment + df = self.cphi_analyses_data_df.copy() + df.loc[:, 'LN(tan(a))'] = df['tan(a)'].apply(lambda x: np.log(x) if x is not None and x > 0 else "") + self.cphi_analyses_data_df = df def calculate_s_tt(self: CPhiAnalyse): """Berekent de s_tt waarden voor de statistische analyse.""" diff --git a/pv_tool/cphi_analysis/save_and_export.py b/pv_tool/cphi_analysis/save_and_export.py new file mode 100644 index 0000000..bd74f6b --- /dev/null +++ b/pv_tool/cphi_analysis/save_and_export.py @@ -0,0 +1,312 @@ +from typing import TYPE_CHECKING, List +from pandas import ExcelWriter, concat +from reportlab.lib.pagesizes import A4, landscape +from reportlab.lib import colors +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.enums import TA_LEFT +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, LongTable + +if TYPE_CHECKING: + from pv_tool.cphi_analysis.c_phi_analysis import CPhiAnalyse + + +def save_total_to_excel(self: "CPhiAnalyse", path: str): + """ + Exporteert alle analysegegevens naar Excel. + + Slaat de volledige dataset met alle berekende kolommen op in een Excel bestand. + De bestandsnaam wordt automatisch gegenereerd op basis van de analyse-instellingen. + + Parameters + ---------- + path: str + Map locatie waar het Excel-bestand moet worden opgeslagen + self: CPhiAnalyse + Instantie van de CPhiAnalyse klasse + """ + # pas de effective stress naam aan zodat het weggeschreven kan worden in de bestandsnaam + effective_stress = str(self.effective_stress).replace('%', 'procent_') + effective_stress = str(effective_stress).replace(' ', '') + + # exporteer onder de juiste naam + file_name = f"c_phi_export_test_{self.investigation_groups[0]}_{self.analysis_type}_{effective_stress}.xlsx" + file_path = f"{path}/{file_name}" + + # Hernoem de kolommen voor een ander analyse type + if self.analysis_type in ['DSS_CPhi', 'DSS_SH']: + self.cphi_analyses_data_df = self.cphi_analyses_data_df.rename(columns={'S\'': '\u03C3 \'', 'T': '\u03C4'}) + + # schrijf het totaal weg + df_totaal = self.cphi_analyses_data_df + with ExcelWriter(file_path, engine='openpyxl') as writer: + df_totaal.to_excel(writer) + +def _df_to_table_with_index(df, index_name='Index'): + """ + Zet een DataFrame om naar een lijst voor gebruik in een PDF tabel. Gebruikt in save_to_pdf. + + Parameters + ---------- + df : DataFrame + De DataFrame die moet worden omgezet + index_name : str, optioneel + Naam voor de index kolom (default='Index') + + Returns + ------- + list + Lijst met header en data rijen voor een PDF tabel + """ + header = [df.index.name or index_name] + df.columns.tolist() + data = [[idx] + row.tolist() for idx, row in df.iterrows()] + return [header] + data + +def _create_input_table(self: "CPhiAnalyse") -> Table: + """ + Maakt een tabel met de invoerselectie informatie. Gebruikt in save_to_pdf. + + Returns + ------- + Table + ReportLab tabel object met de invoerselectie informatie + """ + columns_base = [ + 'PV_NAAM', 'BORING_POSITIE', 'MONSTER_NIVEAU_NAP_VANAF', 'MONSTER_NIVEAU_NAP_TOT' + ] + if self.analysis_type in ['TXT_CPhi', 'TXT_SH']: + columns_extra = ['TXT_SS_VOLUMEGEWICHT_NAT', 'TXT_SS_VOLUMEGEWICHT_DRG', 'TXT_SS_WATERGEHALTE_VOOR'] + else: + columns_extra = ['DSS_VOLUMEGEWICHT_NAT', 'DSS_VOLUMEGEWICHT_DRG', 'DSS_WATERGEHALTE_VOOR'] + + columns_data = self.cphi_analyses_data_df.iloc[:, 1:3].copy() + table1_cols = columns_base + columns_extra + table1_df = self.total_cphi_analyses_data_df[table1_cols].copy() + table1_df.columns = ['Groep', 'Positie', 'NAP Vanaf [m]', 'NAP Tot [m]', 'VGW nat', 'VGW droog', 'Watergehalte voor'] + table1_df = concat([table1_df, columns_data], axis=1) + table1_df = table1_df.map(lambda x: f"{x:.2f}" if isinstance(x, (float, int)) else x) + + t1_data = _df_to_table_with_index(table1_df, index_name="alg_boring_monsternummer_id") + t1 = LongTable(t1_data, repeatRows=1, hAlign='LEFT') + t1.setStyle(TableStyle([ + + ('ALIGN', (0 ,0), (-1, -1), 'LEFT'), + ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 9), + ('FONTSIZE', (0, 1), (-1, -1), 8), + ])) + return t1 + + +def _create_initial_values_table(self: "CPhiAnalyse") -> Table: + """ + Maakt een tabel met de initiële waarden van de analyse. Gebruikt in save_to_pdf. + + Returns + ------- + Table + ReportLab tabel object met de initiële waarden + """ + # name_gem_a1 = 'a1 gem = snijpunt y-as (cohesie gemiddeld)' if self.cohesie_gem_handmatig is None else 'a1 gem = cohesie gemiddeld (handmatig)' + # name_gem_a2 = 'a2 gem = tan(phi) gemiddeld' + # name_kar_a1 = 'a1 kar = snijpunt y-as (cohesie karakteristiek)' if self.cohesie_kar_handmatig is None else 'a1 kar = cohesie karakteristiek (handmatig)' + # name_kar_a2 = 'a2 kar = tan(phi) karakteristiek' if self.phi_kar_handmatig is None else 'a2 kar = tan(phi) karakteristiek (handmatig)' + name_phi_kar_onder = 'a2 kar onder = tan(phi) karakteristiek ondergrens' + name_phi_kar_boven = 'a2 kar boven = tan(phi) karakteristiek bovengrens' + + initial_values: List[list] = [] + + if self.cohesie_gem_handmatig is not None: + initial_values.append(['a1 gem = cohesie gemiddeld (handmatig)', round(self.cohesie_gem_handmatig, 3)]) + elif self.gem_a1 is not None: + initial_values.append(['a1 gem = snijpunt y-as (cohesie gemiddeld)', round(self.gem_a1, 3)]) + + if self.gem_a2 is not None: initial_values.append(['a2 gem = tan(phi) gemiddeld', round(self.gem_a2, 3)]) + + if self.cohesie_kar_handmatig is not None: + initial_values.append(['a1 kar = cohesie karakteristiek (handmatig)', round(self.cohesie_kar_handmatig, 3)]) + elif self.kar_a1 is not None: + initial_values.append(['a1 kar = snijpunt y-as (cohesie karakteristiek)', round(self.kar_a1, 3)]) + + if self.phi_kar_handmatig is not None: + initial_values.append(['a2 kar = tan(phi) karakteristiek (handmatig)', round(self.phi_kar_handmatig, 3)]) + elif self.kar_a2 is not None: + initial_values.append(['a2 kar = tan(phi) karakteristiek', round(self.kar_a2, 3)]) + + if hasattr(self, 'a2_phi_kar_onder') and self.a2_phi_kar_onder is not None: + initial_values.append([name_phi_kar_onder, round(self.a2_phi_kar_onder, 3)]) + if hasattr(self, 'a2_phi_kar_boven') and self.a2_phi_kar_boven is not None: + initial_values.append([name_phi_kar_boven, round(self.a2_phi_kar_boven, 3)]) + + initial_values.append(['Type verzameling: lokaal = 1.0; regionaal = 0.75', self.alpha]) + initial_values.append(['Partiële materiaalfactor cohesie [-]', self.material_cohesie]) + initial_values.append(['Partiële materiaalfactor tan phi [-]', self.material_tan_phi]) + + t3 = Table([['Parameter', 'Waarde']] + initial_values, hAlign='LEFT') + t3.setStyle(TableStyle([ + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ])) + return t3 + + +def _create_results_table(self: "CPhiAnalyse") -> Table: + """ + Maakt een tabel met de eindresultaten van de analyse. Gebruikt in save_to_pdf. + + Returns + ------- + Table + ReportLab tabel object met de resultaten + """ + output_table_df = self.print_short_results().copy() + output_table_df.index.name = 'Parameter' + output_table_df = output_table_df.map(lambda x: f"{x:.2f}" if isinstance(x, (float, int)) else x) + output_table_data = _df_to_table_with_index(output_table_df) + output_table = Table(output_table_data, repeatRows=1, hAlign='LEFT') + output_table.setStyle(TableStyle([ + ('ALIGN', (0, 0), (-1, -1), 'LEFT'), + ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), + ('GRID', (0, 0), (-1, -1), 0.5, colors.black), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ])) + return output_table + + +def _get_manual_values_paragraphs(self: "CPhiAnalyse", styles) -> list: + """ + Maakt een lijst van paragrafen met handmatig opgegeven waarden. + + Parameters + ---------- + styles : dict + ReportLab stylesheet met opmaakstijlen + + Returns + ------- + list + Lijst met ReportLab Paragraph objecten + """ + paragraphs = [] + manual_texts = [] + if self.cohesie_gem_handmatig is not None: + manual_texts.append(f"handmatig opgegeven: cohesie_gem_handmatig = {self.cohesie_gem_handmatig}") + if self.phi_kar_handmatig is not None: + manual_texts.append(f"handmatig opgegeven: phi_kar_handmatig = {self.phi_kar_handmatig}") + if self.cohesie_kar_handmatig is not None: + manual_texts.append(f"handmatig opgegeven: cohesie_kar_handmatig = {self.cohesie_kar_handmatig}") + + if manual_texts: + paragraphs.append(Paragraph("Handmatig opgegeven waarden:", styles['Heading3'])) + for txt in manual_texts: + paragraphs.append(Paragraph(txt, styles['Normal'])) + else: + paragraphs.append \ + (Paragraph("Geen handmatig opgegeven waarden, figuur gebaseerd op eerste inschatting", styles['Normal'])) + + return paragraphs + + +def save_to_pdf(self: "CPhiAnalyse", path: str) -> str: + """ + Slaat de analyseresultaten op in een PDF-document, inclusief figuren, datatabellen en numerieke resultaten. + + De PDF bevat: + - Titel met analysedetails + - Overzichtsfiguur van de analyse + - Tabel met invoerselectie informatie + - Tabel met initiële waarden + - Eventueel handmatig opgegeven waarden + - Tabel met eindresultaten + + Parameters + ---------- + path: str + Map locatie waar het PDF-bestand moet worden opgeslagen + + self: CPhiAnalyse + Instantie van de CPhiAnalyse klasse + + Returns + ------- + str + Het absolute bestandspad van het aangemaakte PDF-bestand + """ + # Maak titel en bestandsnaam + title = f'{self.analysis_type.split('_')[0]} {self.analysis_type.split('_')[1]} analyse met {self.effective_stress} op {self.investigation_groups[0]}' + file_name = f"c_phi_pdf_export_{self.investigation_groups[0]}_{self.analysis_type}_{str(self.effective_stress).replace('%', 'procent_').replace(' ', '')}.pdf" + file_path = f"{path}/{file_name}" + + # Maak en bewaar de figuur alleen als deze nog niet bestaat + fig_path = f"{path}/temp_plot.png" + if not hasattr(self, 'figure') or len(self.figure.data) == 0: + self.show_title = False + self.show_figure() + + self.show_title = True + fig_width = 1280 + fig_height = 720 + self.figure.write_image(fig_path, width=fig_width, height=fig_height, scale=4, format="png") + + # Maak het PDF document + doc = SimpleDocTemplate(file_path, pagesize=landscape(A4)) + styles = getSampleStyleSheet() + styles.add(ParagraphStyle(name='Left', parent=styles['Normal'], alignment=TA_LEFT)) + styles.add(ParagraphStyle(name='TitleLeft', parent=styles['Title'], alignment=TA_LEFT)) + story = [] + + # Voeg titel toe + story.append(Paragraph(title, styles['TitleLeft'])) + story.append(Spacer(width=1, height=12)) + + # Voeg figuur toe met aangepaste grootte + from PIL import Image as PILImage + from reportlab.platypus import Image as RLImage + + fig_path = f"{path}/temp_plot.png" + + # Laad PNG en bepaal pixelafmetingen + with PILImage.open(fig_path) as im: + img_width_px, img_height_px = im.size + + # Stel gewenste breedte in punten (bijv. 95% van PDF breedte) + max_width_pt = doc.width * 0.95 + + # Bereken hoogte zodat verhouding gelijk blijft + aspect = img_height_px / img_width_px + img_width_pt = min(max_width_pt, doc.width) # niet breder dan pagina + img_height_pt = img_width_pt * aspect + + # Maak ReportLab Image aan + img = RLImage(fig_path) + img.drawWidth = img_width_pt + img.drawHeight = img_height_pt + img.hAlign = 'LEFT' + + story.append(img) + story.append(Spacer(width=1, height=12)) + + # Voeg initiële waarden toe + story.append \ + (Paragraph("Parameter bepaling fysisch realiseerbare ondergrens en gemiddelde waarden", styles['Heading2'])) + story.append(_create_initial_values_table(self)) + story.append(Spacer(1, 12)) + + # Voeg resultaten toe + story.append(Paragraph("Resultaten", styles['Heading2'])) + story.append(_create_results_table(self)) + story.append(Spacer(1, 12)) + + # Voeg invoertabel toe + story.append(Paragraph("Informatietabel invoerselectie", styles['Heading2'])) + story.append(_create_input_table(self)) + story.append(Spacer(1, 12)) + + # Bouw de PDF + doc.build(story) + + print(f"PDF succesvol opgeslagen op: {file_path}") + return file_path \ No newline at end of file diff --git a/pv_tool/cphi_analysis/test_c_phi_analysis.py b/pv_tool/cphi_analysis/test_c_phi_analysis.py new file mode 100644 index 0000000..4a10a3a --- /dev/null +++ b/pv_tool/cphi_analysis/test_c_phi_analysis.py @@ -0,0 +1,5 @@ + +def test_pv_tool2(): + # from pv_tool.cphi_analysis.c_phi_analysis import CPhiAnalyse + # _ = CPhiAnalyse(dbase=None, analysis_type=None, investigation_groups=[], effective_stress="") + pass diff --git a/pv_tool/imports/add_ana_columns.py b/pv_tool/imports/add_ana_columns.py index 1f94cbf..258c5e6 100644 --- a/pv_tool/imports/add_ana_columns.py +++ b/pv_tool/imports/add_ana_columns.py @@ -8,14 +8,55 @@ def add_columns(self: Dbase): - """Voegt de kolommen toe in de gewenste volgorde zodat ze later gevuld kunnen worden.""" - columns = ['ANA_TERREINSPANNING', 'ANA_TXT_MAX_VERTICALE_CONSOLIDATIE_SPANNING', - 'ANA_DSS_MAX_CONSOLIDATIE_SPANNING', 'ANA_TXT_CONSOLIDATIE_TYPE_VOORSTEL', - 'ANA_TXT_CONSOLIDATIE_TYPE_HANDMATIG', 'ANA_DSS_CONSOLIDATIE_TYPE_VOORSTEL', - 'ANA_DSS_CONSOLIDATIE_TYPE_HANDMATIG', 'ANA_GRENSSPANNING_PROEF', 'ANA_POP_VELD', - 'ANA_POP_VELD_GEMIDDELD', 'ANA_GRENSSPANNING_VOORSTEL', 'ANA_GRENSSPANNING_HANDMATIG', # TODO: ANA_GRENSSPANNING_HANDMATIG wordt al eerder ingeladen vanaf de import. Dus volgorde op andere manier veranderen, want ANA_GRENSSPANNING_HANDMATIG staat nu nog niet op de juiste plek - 'ANA_GRENSSPANNING_REKEN', 'OCR_TXT', 'OCR_DSS'] - self.dbase_df[columns] = None + """ + Voegt analyse kolommen toe in de gespecificeerde volgorde aan het einde van de DataFrame, + waarbij data in 'preserve_cols' behouden blijft indien aanwezig. + """ + analysis_columns = [ + 'ANA_TERREINSPANNING', 'ANA_TXT_MAX_VERTICALE_CONSOLIDATIE_SPANNING', + 'ANA_DSS_MAX_CONSOLIDATIE_SPANNING', 'ANA_TXT_CONSOLIDATIE_TYPE_VOORSTEL', + 'ANA_TXT_CONSOLIDATIE_TYPE_HANDMATIG', 'ANA_DSS_CONSOLIDATIE_TYPE_VOORSTEL', + 'ANA_DSS_CONSOLIDATIE_TYPE_HANDMATIG', 'ANA_GRENSSPANNING_PROEF', 'ANA_POP_VELD', + 'ANA_POP_VELD_GEMIDDELD', 'ANA_GRENSSPANNING_VOORSTEL', 'ANA_GRENSSPANNING_HANDMATIG', + 'ANA_GRENSSPANNING_REKEN', 'OCR_TXT', 'OCR_DSS' + ] + preserve_cols = [ + 'ANA_TXT_CONSOLIDATIE_TYPE_HANDMATIG', + 'ANA_DSS_CONSOLIDATIE_TYPE_HANDMATIG', + 'ANA_GRENSSPANNING_HANDMATIG' + ] + df = self.dbase_df + + # Store preserved values with proper type conversion + preserved_data = {} + for col in preserve_cols: + if col in df.columns: + if col.endswith('_HANDMATIG') and 'CONSOLIDATIE_TYPE' in col: + # For consolidation type columns, preserve as string + preserved_data[col] = df[col].astype(str).where(df[col].notna(), None) + elif col == 'ANA_GRENSSPANNING_HANDMATIG': + # For numerical columns, convert to float + preserved_data[col] = pd.to_numeric(df[col], errors='coerce') + else: + preserved_data[col] = df[col].copy() + + # Get non-analysis columns + other_columns = [col for col in df.columns if col not in analysis_columns] + + # Create new DataFrame with correct order + new_df = df[other_columns].copy() + + # Add analysis columns with proper types + for col in analysis_columns: + if col in preserve_cols and col in preserved_data: + new_df[col] = preserved_data[col] + else: + if 'CONSOLIDATIE_TYPE' in col: + new_df[col] = None # Will be filled with 'OC' or 'NC' later + else: + new_df[col] = pd.Series(dtype='float64') # For numerical columns + + self.dbase_df = new_df def add_terreinspanning(self: Dbase): diff --git a/pv_tool/imports/create_dbase.py b/pv_tool/imports/create_dbase.py index 6ad59f2..2163522 100644 --- a/pv_tool/imports/create_dbase.py +++ b/pv_tool/imports/create_dbase.py @@ -44,20 +44,29 @@ def alg_columns(self): def add_ana_columns(self): - """Voegt ANA-kolommen toe aan het dataframe""" + """Voegt ANA-kolommen toe aan het dataframe in de juiste volgorde om afhankelijkheden te respecteren""" + # First add the structure for all columns add_columns(self) + + # Calculate independent values first add_terreinspanning(self) add_txt_max_vert_consol_sp(self) add_dss_max_consol_sp(self) + + # Calculate consolidation types (these don't depend on preserved values) add_txt_consol_type(self) add_dss_consol_type(self) - add_grensspanning_proef(self) - calc_pop_veld(self) + + # Now that preserved values are in place from add_columns, calculate dependent values + add_grensspanning_proef(self) # Uses preserved ANA_GRENSSPANNING_HANDMATIG + calc_pop_veld(self) # Depends on grensspanning_proef calc_pop_average(self) add_grensspanning_voorstel(self) - calc_grensspanning_reken(self) - calc_ocr_txt(self) - calc_ocr_dss(self) + + # Calculate final values that depend on preserved data + calc_grensspanning_reken(self) # Uses preserved ANA_GRENSSPANNING_HANDMATIG + calc_ocr_txt(self) # Uses preserved ANA_TXT_CONSOLIDATIE_TYPE_HANDMATIG + calc_ocr_dss(self) # Uses preserved ANA_DSS_CONSOLIDATIE_TYPE_HANDMATIG def add_pv_naam(self): diff --git a/pv_tool/imports/excel_utils.py b/pv_tool/imports/excel_utils.py new file mode 100644 index 0000000..c114d1d --- /dev/null +++ b/pv_tool/imports/excel_utils.py @@ -0,0 +1,84 @@ +from openpyxl import load_workbook +from openpyxl.worksheet.table import Table as XLTable, TableStyleInfo +from openpyxl.utils import get_column_letter + + +def format_excel_sheet( + file_path: str, + sheet_name: str, + num_columns: int, + num_rows: int, + table_name: str = None, + index: bool = True # <--- NEW: whether the index was saved +): + """ + Format an Excel worksheet as a table with filters and custom column widths. + + Parameters + ---------- + file_path : str + Full path to the Excel file + sheet_name : str + Name of the worksheet to format + num_columns : int + Number of data columns (excluding the index) + num_rows : int + Number of rows in the table (excluding the header) + table_name : str, optional + Name for the Excel table. If None, will use sheet_name + "Table". + index : bool, default True + Whether the index column is present + """ + + from openpyxl import load_workbook + from openpyxl.worksheet.table import Table as XLTable, TableStyleInfo + from openpyxl.utils import get_column_letter + + workbook = load_workbook(file_path) + worksheet = workbook[sheet_name] + + # Adjust for index column if present + total_columns = num_columns + (1 if index else 0) + + # Auto-adjust column widths + for column in worksheet.columns: + max_length = 0 + column_letter = get_column_letter(column[0].column) + for cell in column: + if cell.value: + max_length = max(max_length, len(str(cell.value))) + adjusted_width = max_length + 2 + worksheet.column_dimensions[column_letter].width = adjusted_width + + # Define table range + table_range = f"A1:{get_column_letter(total_columns)}{num_rows + 1}" + + # Create table with filters + if table_name is None: + table_name = f"{sheet_name}Table" + + # Remove spaces and special characters from table name + table_name = "".join(c for c in table_name if c.isalnum()) + + table = XLTable(displayName=table_name, ref=table_range) + + # Add a default style + style = TableStyleInfo( + name="TableStyleMedium2", + showFirstColumn=False, + showLastColumn=False, + showRowStripes=True, + showColumnStripes=False + ) + table.tableStyleInfo = style + + # Remove existing table if it exists + for existing_table in list(worksheet.tables.values()): + if existing_table.name == table_name: + del worksheet.tables[existing_table.name] + break + + # Add the table to the worksheet + worksheet.add_table(table) + + workbook.save(file_path) \ No newline at end of file diff --git a/pv_tool/imports/import_data.py b/pv_tool/imports/import_data.py index 18f7a9f..1987e74 100644 --- a/pv_tool/imports/import_data.py +++ b/pv_tool/imports/import_data.py @@ -5,6 +5,7 @@ from pv_tool.imports.create_dbase import add_missing_columns, select_columns, alg_columns, add_ana_columns, add_pv_naam from pv_tool.imports.import_options import import_dbase, import_pv_tool, import_stowa from pv_tool.imports.validation import Validation +from pv_tool.imports.excel_utils import format_excel_sheet class Dbase: @@ -32,8 +33,18 @@ def _create_dbase(self, source: Literal['Stowa', 'PV-tool', 'Dbase']): add_ana_columns(self) add_pv_naam(self) - def import_data_and_validate(self, source: Literal['Stowa', 'PV-tool', 'Dbase'], - source_dir: Path, export_path: Path): + def import_dbase_short(self, source: Literal['Stowa', 'PV-tool', 'Dbase'], + source_dir: Path): + """Importeert data uit de Stowa-database, de oude pv-tool of de Dbase (template) en voegt kolommen toe""" + if source == 'Dbase': + import_dbase(self, dbase_dir=source_dir) + return self.dbase_df + else: + return f"Short import only available for 'Dbase' source, not for '{source}'" + + def import_data(self, source: Literal['Stowa', 'PV-tool', 'Dbase'], + source_dir: Path): + """Importeert data uit de Stowa-database, de oude pv-tool of de Dbase (template) en voegt kolommen toe""" if source == 'Stowa': import_stowa(self, stowa_dir=source_dir) self.dbase_df = self.stowa_df @@ -42,53 +53,75 @@ def import_data_and_validate(self, source: Literal['Stowa', 'PV-tool', 'Dbase'], self.dbase_df = self.pv_tool elif source == 'Dbase': import_dbase(self, dbase_dir=source_dir) - self.validation.validation_export(export_path=export_path) # TODO splits op in import en validate - maar behoud wel de analyse kolommen etc. - self.validation.print_critical_errors() - self._create_dbase(source=source) # kijk of deze snapt als de input niet gevalideerd is + self._create_dbase(source=source) return self.dbase_df - from datetime import datetime - from pandas import read_excel, ExcelWriter - import os + def validate_data(self, export_path: Path): + self.validation.validation_export(export_path=export_path) + self.validation.print_critical_errors() + return self.dbase_df def export_dbase_to_excel(self, export_dir: Path, filename: str = 'Template_PVtool5_0.xlsx'): """ - Exports the Dbase DataFrame to an Excel file. - :param export_dir: The directory where the file will be saved. - :param filename: The name of the file. Default is 'Template_PVtool5_0.xlsx'. + Exports the Dbase DataFrame to an Excel file, maintaining the correct column order + and preserving specified columns if they exist. """ export_path = export_dir / filename sheet_name = 'Dbase5_0' + preserve_cols = [ + 'ANA_GRENSSPANNING_HANDMATIG', + 'ANA_TXT_CONSOLIDATIE_TYPE_HANDMATIG', + 'ANA_DSS_CONSOLIDATIE_TYPE_HANDMATIG' + ] # Ensure the export directory exists - if not export_dir.exists(): - export_dir.mkdir(parents=True) - print(f"Directory created: {export_dir}") + export_dir.mkdir(parents=True, exist_ok=True) - # Check if the file exists + # Try to preserve columns if the file & sheet exist + preserved_data = {} if export_path.exists(): - print(f"File already exists: {export_path}") try: existing_df = read_excel(export_path, sheet_name=sheet_name) - # Compare the existing dataframe with the new dataframe - if existing_df.equals(self.dbase_df): - print("Dbase is already present at this location.") - return - else: - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - print(f"Template_PVtool5_0.xlsx is already present but the dbase is different. " - f"The sheet 'Dbase5_0' will be overwritten at {timestamp}.") - except ValueError: # Raised if the sheet does not exist - print(f"Sheet '{sheet_name}' does not exist in the file. Adding it.") + for col in preserve_cols: + if col in existing_df.columns and col in self.dbase_df.columns: + preserved_data[col] = existing_df[col] + except Exception: + pass + + # Restore preserved columns + for col, data in preserved_data.items(): + self.dbase_df[col] = data + + # Ensure correct column order based on PV_TOOL_DBASE_COLUMNS + from pv_tool.imports.globals import PV_TOOL_DBASE_COLUMNS + ordered_columns = [col for col in PV_TOOL_DBASE_COLUMNS if col in self.dbase_df.columns] + extra_columns = [col for col in self.dbase_df.columns if col not in PV_TOOL_DBASE_COLUMNS] + final_columns = ordered_columns + extra_columns + + # Reorder columns + self.dbase_df = self.dbase_df[final_columns] + + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + print(f"Excel sheet Dbase5_0 wordt overschreven met een nieuwe database op {timestamp}") + + # Write the DataFrame to Excel + if export_path.exists(): + with ExcelWriter(export_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer: + self.dbase_df.to_excel(writer, sheet_name=sheet_name, index=True) else: - print(f"Creating new file: {export_path}") - # Ensure the file is created if it doesn't exist with ExcelWriter(export_path, engine='openpyxl', mode='w') as writer: self.dbase_df.to_excel(writer, sheet_name=sheet_name, index=True) - print(f"Excel file created: {export_path}") - return - # If the file already exists, append or replace the sheet - with ExcelWriter(export_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer: - self.dbase_df.to_excel(writer, sheet_name=sheet_name, index=True) print(f"Excel file exported to: {export_path}") + + # Formatting + num_columns = self.dbase_df.shape[1] + num_rows = self.dbase_df.shape[0] + format_excel_sheet( + file_path=str(export_path), + sheet_name='Dbase5_0', + num_columns=num_columns, + num_rows=num_rows, + table_name='Dbase', + index=True + ) diff --git a/pv_tool/imports/test_imports.py b/pv_tool/imports/test_imports.py index 85115b1..2ce1c0f 100644 --- a/pv_tool/imports/test_imports.py +++ b/pv_tool/imports/test_imports.py @@ -1,9 +1,10 @@ import unittest from pv_tool.imports.import_data import Dbase -def test_import_data_and_validate() -> bool: - dbase = Dbase() - - stowa_dir = ... - export_path = ... - dbase.import_data_and_validate(source='Stowa', source_dir=stowa_dir, export_path=export_path) +def test_import_data_and_validate(): + # dbase = Dbase() + # + # stowa_dir = ... + # export_path = ... + # dbase.import_data_and_validate(source='Stowa', source_dir=stowa_dir, export_path=export_path) + pass diff --git a/pv_tool/imports/validation.py b/pv_tool/imports/validation.py index 17f116c..8b72553 100644 --- a/pv_tool/imports/validation.py +++ b/pv_tool/imports/validation.py @@ -1,5 +1,5 @@ from __future__ import annotations - +from pv_tool.imports.excel_utils import format_excel_sheet import math from typing import TYPE_CHECKING, Dict from typing import Optional, List, Literal @@ -70,7 +70,7 @@ def validation_selection( 'Triaxiaalproeven single stage' ] ): - """Deze functie verwijderd rijen uit de dataframes waar geen proef is gedaan.""" + """Deze functie verwijdert rijen uit de dataframes waar geen proef is gedaan.""" df_alg = self.split_dbase()['Algemene kenmerken'] df_to_check = self.split_dbase()[category] if category == 'Classificatie': @@ -216,17 +216,41 @@ def validation_log(self, export_path: Path, critical: Optional[bool] = True): try: for func_name, func in validation_mapping.items(): validation_df, error_log = func(self) + # Ensure all column names are strings + validation_df.columns = validation_df.columns.astype(str) validation_results[func_name] = validation_df self.error_totals.append(f"number of {c} in {func_name} = {len(error_log)}") error_logs.extend(error_log) sheet_names_print = [] + # First write all sheets to Excel with pd.ExcelWriter(str(export_path), engine="xlsxwriter") as writer: for func_name, validation_df in validation_results.items(): sheet_name = func_name.upper() sheet_names_print.append(sheet_name) + # Ensure index is also strings when writing + validation_df.index = validation_df.index.astype(str) + # Set index name if it's empty to prevent Excel warnings + if validation_df.index.name is None: + validation_df.index.name = 'ID' validation_df.to_excel(writer, sheet_name=sheet_name, index=True) + # After the file is completely written and closed, then format each sheet + for sheet_name in sheet_names_print: + try: + num_rows, num_columns = validation_results[sheet_name.lower()].shape + format_excel_sheet( + file_path=str(export_path), + sheet_name=sheet_name, + num_columns=num_columns, + num_rows=num_rows, + table_name=f'tabel_{sheet_name.lower()}', + index=True + ) + except Exception as format_error: + print(f"Warning: Could not format sheet {sheet_name}: {str(format_error)}") + # Continue with other sheets even if one fails to format + except Exception as e: print(f"Er trad een fout op tijdens validatie of het schrijven van Excel: {str(e)}") raise e diff --git a/pv_tool/test_pv_tool.py b/pv_tool/test_pv_tool.py new file mode 100644 index 0000000..163cf86 --- /dev/null +++ b/pv_tool/test_pv_tool.py @@ -0,0 +1,12 @@ + + +def test_pv_tool(): + ## + assert 1 + 2 == 3 + ## + + +def test_pv_tool2(): + # from pv_tool.cphi_analysis.c_phi_analysis import CPhiAnalyse + # _ = CPhiAnalyse(dbase=None, analysis_type=None, investigation_groups=[], effective_stress="") + pass