Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions mikeplus/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should raise an error instead of silently returning

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:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the case where a user opens the database, starts the database, and then calls close() without ending the transaction?

I think a sensible default is to auto-commit (i.e. assume transactions will always be applied when calling end_transaction or close, unless the user explicitly wants to discard the changes). It is probably most common that a user will want to commit changes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ryan-kipawa The database will be locked when the "begin transaction" happens. So "begin transaction" should always go with "end transaction".

raise ValueError("Database is not open")

self._data_table_container.EndTransaction(commit)

def __enter__(self):
"""Context manager entry."""
self.open()
Expand Down
17 changes: 17 additions & 0 deletions mikeplus/utilities/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
70 changes: 70 additions & 0 deletions mikeplus/utilities/spatial_analysis_util.py
Original file line number Diff line number Diff line change
@@ -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
141 changes: 141 additions & 0 deletions notebooks/couple river junction node to river.ipynb
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an error in this notebook - it should run cleanly

Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ exclude = ["mikeplus/tables/auto_generated/"]
[[tool.mypy.overrides]]
module = [
"DHI.*",
"ThinkGeo.*",
"System.*",
"clr",
"pythonnet",
Expand Down
30 changes: 30 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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"
)
21 changes: 21 additions & 0 deletions tests/database/test_util.py
Original file line number Diff line number Diff line change
@@ -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)
6 changes: 3 additions & 3 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Binary file added tests/testdata/RiverCS/100y_sections.xns11
Binary file not shown.
Binary file added tests/testdata/RiverCS/River_CS.sqlite
Binary file not shown.
Loading