diff --git a/mikeplus/database.py b/mikeplus/database.py index d9a6795..bf58452 100644 --- a/mikeplus/database.py +++ b/mikeplus/database.py @@ -204,6 +204,45 @@ def close(self): f"Failed to close model database: {self._db_path}.\n{str(e)}" ) + def begin_transaction(self): + """Begin the data transaction. + + Using a BEGIN/END transaction significantly improves batch commit performance. + + Examples + -------- + >>> from mikeplus import Database + >>> db = Database("path/to/model.sqlite") + >>> db.begin_transaction() + >>> commit = True + >>> try: + >>> db._tables.msm_Node.update({"Diameter": 0.35}).by_muid("Node_1").execute() + >>> db._tables.msm_Node.update({"Diameter": 0.40}).by_muid("Node_2").execute() + >>> ... [Update more data] + >>> except RuntimeError as e: + >>> print(f"An error occurred: {e}") + >>> commit = False + >>> finally: + >>> db.end_transaction(commit) + """ + if not self._is_open: + raise ValueError("Database is not open") + + self._data_table_container.BeginTransaction() + + def end_transaction(self, commit: bool = True): + """End the data transaction. + + Parameters + ---------- + commit : bool + true is to commit the data into database, false is to rollback the commit. + """ + if not self._is_open: + raise ValueError("Database is not open") + + self._data_table_container.EndTransaction(commit) + def __enter__(self): """Context manager entry.""" self.open() diff --git a/mikeplus/utilities/__init__.py b/mikeplus/utilities/__init__.py new file mode 100644 index 0000000..2abe4bc --- /dev/null +++ b/mikeplus/utilities/__init__.py @@ -0,0 +1,17 @@ +"""Package containing spatial analysis for MIKE+ geometries.""" + +import clr + +clr.AddReference("DHI.Amelia.Infrastructure.Interface") +clr.AddReference("ThinkGeo.Core") + + +from .spatial_analysis_util import ( # noqa: E402 + get_nearest_river_at, + get_nearest_river_chainage_at, +) + +__all__ = [ + "get_nearest_river_at", + "get_nearest_river_chainage_at", +] diff --git a/mikeplus/utilities/spatial_analysis_util.py b/mikeplus/utilities/spatial_analysis_util.py new file mode 100644 index 0000000..d39cd35 --- /dev/null +++ b/mikeplus/utilities/spatial_analysis_util.py @@ -0,0 +1,70 @@ +"""Util to do spatial analysis for MIKE+ geometry data.""" + +from ThinkGeo.Core import BaseShape # noqa: E402 +from ThinkGeo.Core import PointShape # noqa: E402 +from ThinkGeo.Core import GeographyUnit # noqa: E402 +from DHI.Amelia.Infrastructure.Interface.UtilityHelper import GeoAPIHelper # noqa: E402 + + +def get_nearest_river_chainage_at(database, x: float, y: float, tolorance: float): + """Get the nearest river chainage location by a give (x,y) location. + + Parameters + ---------- + database : Database or DataTables + x : float + x coordinate of target search location + y : float + y coordinate of target search location + tolorance : float + search radiu distance + + Returns + ------- + [str, float] + first value is the river name, second value is the chainage value + + """ + river_id = database.tables.mrm_Branch._net_table.GetNearestMuid( + x, y, tolorance, None, None + ) + if river_id is not None: + riverGeom = database.tables.mrm_Branch._net_table.GetGeometry(river_id) + lineGeom = GeoAPIHelper.GetWKBIGeometry(riverGeom) + lineShape = BaseShape.CreateShapeFromWellKnownData(lineGeom) + pointshp = PointShape(x, y) + locationPnt = lineShape.GetClosestPointTo(pointshp, GeographyUnit.Meter) + river_name = database.tables.mrm_Branch._net_table.GetName(river_id) + chainageMngr = database._data_table_container.GetChainageManager( + "mrm_Branch", river_name + ) + chainage = chainageMngr.GetChainageAt(locationPnt, tolorance) + return [river_name, chainage] + else: + return None + + +def get_nearest_river_at(database, x: float, y: float, tolorance: float): + """Get the nearest river name by a give (x,y) location. + + Parameters + ---------- + database : Database or DataTables + x : float + x coordinate of target search location + y : float + y coordinate of target search location + tolorance : float + search radiu distance + + Returns + ------- + str: + river name + + """ + river_id = database.tables.mrm_Branch._net_table.GetNearestMuid( + x, y, tolorance, None, None + ) + river_name = database.tables.mrm_Branch._net_table.GetName(river_id) + return river_name diff --git a/notebooks/couple river junction node to river.ipynb b/notebooks/couple river junction node to river.ipynb new file mode 100644 index 0000000..a758132 --- /dev/null +++ b/notebooks/couple river junction node to river.ipynb @@ -0,0 +1,141 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d61696a6", + "metadata": {}, + "source": [ + "# Couple river junction node to nearest river chainage lication\n", + "\n", + "This notebook demonstrates how to batch‑configure river chainage locations for river junction nodes that are missing chainage information.\n", + "The tool helps reduce manual configuration work and is especially useful when you need to configure hundreds or even thousands of river junctions." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "3ea70e7e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Database<'River_CS.sqlite'>" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import mikeplus as mp\n", + "from mikeplus.utilities import get_nearest_river_chainage_at\n", + "from mikeplus.dotnet import DotNetConverter\n", + "\n", + "db = mp.open(\"../tests/testdata/RiverCS/River_CS.sqlite\")\n", + "db" + ] + }, + { + "cell_type": "markdown", + "id": "997c968e", + "metadata": {}, + "source": [ + "## Analysis process\n", + "\n", + "The process consists of the following steps:\n", + "\n", + "1) Identify all river‑junction nodes for which the BranchID field is NULL.\n", + "2) For each of these nodes, determine the nearest river and calculate the corresponding river chainage.\n", + "3) Update the node table by assigning the derived river name and chainage values." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "881839e2", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "River junction of 'Node_33' has been coupled to 'River' at 755.0359876508826\n", + "River junction of 'Pump_3_Outlet' has been coupled to 'River' at 735.7046165978604\n" + ] + } + ], + "source": [ + "df = (\n", + " db._tables.msm_Node.select([\"GeomX\", \"GeomY\"])\n", + " .where(\"TypeNo=6 AND BranchID is NULL\")\n", + " .execute()\n", + " )\n", + "db.begin_transaction()\n", + "commit = True\n", + "try:\n", + " for key, value in df.items():\n", + " x = value[0]\n", + " y = value[1]\n", + " river_chainage = get_nearest_river_chainage_at(\n", + " db, x, y, 100.0\n", + " )\n", + " if river_chainage is not None:\n", + " river = river_chainage[0]\n", + " chainage = river_chainage[1]\n", + " db._tables.msm_Node.update({\"BranchID\": river, \"BranchChainage\": chainage}).by_muid(key).execute()\n", + " print(\n", + " f\"River junction of '{key}' has been coupled to '{river}' at {chainage}\"\n", + " )\n", + "\n", + "except RuntimeError as e:\n", + " print(f\"An error occurred: {e}\")\n", + " commit = False\n", + "\n", + "finally:\n", + " db.end_transaction(commit)" + ] + }, + { + "cell_type": "markdown", + "id": "66a9422c", + "metadata": {}, + "source": [ + "# Close database" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f31060ad", + "metadata": {}, + "outputs": [], + "source": [ + "# Close the database when you're done using it.\n", + "db.close()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/pyproject.toml b/pyproject.toml index 3244b40..16a6b57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,7 @@ exclude = ["mikeplus/tables/auto_generated/"] [[tool.mypy.overrides]] module = [ "DHI.*", + "ThinkGeo.*", "System.*", "clr", "pythonnet", diff --git a/tests/conftest.py b/tests/conftest.py index 9168bee..2b2cc10 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,6 +77,7 @@ def test_write_example(sirius_db): CONNECTION_REPAIR_DB = TEST_DATA_DIR / "connectionRepair" / "repair.sqlite" CATCH_SLOPE_LEN_DB = TEST_DATA_DIR / "catchSlopeLen" / "catch.sqlite" IMPORT_DB = TEST_DATA_DIR / "import" / "import.sqlite" +RIVER_JUNCTION_COUPLE_DB = TEST_DATA_DIR / "RiverCS" / "River_CS.sqlite" def copy_database_folder(source_db_path, target_dir) -> Path: @@ -186,6 +187,13 @@ def session_import_db(session_temp_dir) -> Path: return copy_database_folder(IMPORT_DB, target_dir) +@pytest.fixture(scope="session") +def session_river_junction_couple_db(session_temp_dir) -> Path: + """Copy the river junction couple folder once per test session.""" + target_dir = session_temp_dir / "session" / "riverJunctionCouple" + return copy_database_folder(RIVER_JUNCTION_COUPLE_DB, target_dir) + + @pytest.fixture(scope="module") def module_temp_dir(): """Create a temporary directory for test files that persists for the entire test module.""" @@ -256,6 +264,13 @@ def module_import_db(module_temp_dir) -> Path: return copy_database_folder(IMPORT_DB, target_dir) +@pytest.fixture(scope="module") +def module_river_junction_couple_db(module_temp_dir) -> Path: + """Copy the river junction couple database folder once per test module.""" + target_dir = module_temp_dir / "module" / "riverJunctionCouple" + return copy_database_folder(RIVER_JUNCTION_COUPLE_DB, target_dir) + + @pytest.fixture(scope="class") def class_temp_dir(): """Create a temporary directory for test files that persists for the entire test class.""" @@ -326,6 +341,13 @@ def class_import_db(class_temp_dir) -> Path: return copy_database_folder(IMPORT_DB, target_dir) +@pytest.fixture(scope="class") +def class_river_junction_couple_db(class_temp_dir) -> Path: + """Copy the river junction couple database folder once per test class.""" + target_dir = class_temp_dir / "class" / "riverJunctionCouple" + return copy_database_folder(RIVER_JUNCTION_COUPLE_DB, target_dir) + + @pytest.fixture(scope="function") def sirius_db(tmp_path) -> Path: """Create a test-specific copy of the Sirius database folder.""" @@ -382,3 +404,11 @@ def catch_slope_len_db(tmp_path) -> Path: def import_db(tmp_path) -> Path: """Create a test-specific copy of the import database folder.""" return create_test_specific_db_copy(IMPORT_DB, tmp_path, "test_import") + + +@pytest.fixture(scope="function") +def river_junction_couple_db(tmp_path) -> Path: + """Create a test-specific copy of the river junction couple database folder.""" + return create_test_specific_db_copy( + RIVER_JUNCTION_COUPLE_DB, tmp_path, "test_river_junction_couple" + ) diff --git a/tests/database/test_util.py b/tests/database/test_util.py new file mode 100644 index 0000000..05809fb --- /dev/null +++ b/tests/database/test_util.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +import pytest +from mikeplus import Database +from mikeplus.utilities import get_nearest_river_chainage_at + + +def test_get_nearest_river_chainage_at(river_junction_couple_db): + muid = "Node_33" + db = Database(river_junction_couple_db) + field_val_get = ( + db._tables.msm_Node.select(["GeomX", "GeomY"]).by_muid(muid).execute() + ) + x = field_val_get[muid][0] + y = field_val_get[muid][1] + + river_chainage = get_nearest_river_chainage_at(db, x, y, 100.0) + river = river_chainage[0] + chainage = river_chainage[1] + assert river == "River" + assert chainage == pytest.approx(755.035988, abs=1e-6) diff --git a/tests/test_tools.py b/tests/test_tools.py index 6a6874b..0b989c9 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -94,9 +94,9 @@ def test_catch_slope_len_tool(catch_slope_len_db): shp_file = os.path.join(db_dir, "Catch_Slope.shp") dem_file = os.path.join(db_dir, "dem.dfs2") - assert os.path.exists(catch_slope_len_db), ( - f"Database file does not exist: {catch_slope_len_db}" - ) + assert os.path.exists( + catch_slope_len_db + ), f"Database file does not exist: {catch_slope_len_db}" assert os.path.exists(shp_file), "Catch_Slope.shp does not exist" assert os.path.exists(dem_file), "dem.dfs2 does not exist" diff --git a/tests/testdata/RiverCS/100y_sections.xns11 b/tests/testdata/RiverCS/100y_sections.xns11 new file mode 100644 index 0000000..8937c08 Binary files /dev/null and b/tests/testdata/RiverCS/100y_sections.xns11 differ diff --git a/tests/testdata/RiverCS/River_CS.sqlite b/tests/testdata/RiverCS/River_CS.sqlite new file mode 100644 index 0000000..3c0b7e7 Binary files /dev/null and b/tests/testdata/RiverCS/River_CS.sqlite differ