From 7bd45971fd752435baccf5cec905806b04457e6d Mon Sep 17 00:00:00 2001 From: Alexander Rossmanith Date: Fri, 7 Aug 2026 19:59:27 +0530 Subject: [PATCH] initial commit --- .gitignore | 9 + .vscode/settings.json | 12 + .vscode/surveyladder.code-workspace | 22 ++ Readme.md | 17 + data/2016_3_correction_sketch_ladder.xlsx | Bin 0 -> 6495 bytes data/2016_3_revised_sketch_ladder.xlsx | Bin 0 -> 6742 bytes pdm.lock | 369 ++++++++++++++++++++++ pyproject.toml | 15 + scripts/generate_dotenv.py | 10 + surveyladder/dxf.py | 170 ++++++++++ surveyladder/geo.py | 127 ++++++++ surveyladder/ladder.py | 165 ++++++++++ surveyladder/main.py | 369 ++++++++++++++++++++++ surveyladder/plot.py | 39 +++ surveyladder/remove_empty_layers.py | 48 +++ surveyladder/sketch.py | 65 ++++ surveyladder/solver.py | 217 +++++++++++++ surveyladder/util.py | 19 ++ tests/data/ladder.csv | 10 + tests/data/ladder.xlsx | Bin 0 -> 6091 bytes tests/test_geo.py | 78 +++++ tests/test_ladder.py | 42 +++ tests/test_solver.py | 121 +++++++ 23 files changed, 1924 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 .vscode/surveyladder.code-workspace create mode 100644 Readme.md create mode 100644 data/2016_3_correction_sketch_ladder.xlsx create mode 100644 data/2016_3_revised_sketch_ladder.xlsx create mode 100644 pdm.lock create mode 100644 pyproject.toml create mode 100644 scripts/generate_dotenv.py create mode 100644 surveyladder/dxf.py create mode 100644 surveyladder/geo.py create mode 100644 surveyladder/ladder.py create mode 100644 surveyladder/main.py create mode 100644 surveyladder/plot.py create mode 100644 surveyladder/remove_empty_layers.py create mode 100644 surveyladder/sketch.py create mode 100644 surveyladder/solver.py create mode 100644 surveyladder/util.py create mode 100644 tests/data/ladder.csv create mode 100644 tests/data/ladder.xlsx create mode 100644 tests/test_geo.py create mode 100644 tests/test_ladder.py create mode 100644 tests/test_solver.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42de9cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.py[codz] +__pycache__/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.pdm-python +\#*.dxf +*.dxf~ +.env diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..38fe920 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "python.envFile": "${workspaceFolder}/.env", + "python.testing.unittestArgs": [ + "-v", + "-s", + "./tests", + "-p", + "test_*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/.vscode/surveyladder.code-workspace b/.vscode/surveyladder.code-workspace new file mode 100644 index 0000000..3c6124e --- /dev/null +++ b/.vscode/surveyladder.code-workspace @@ -0,0 +1,22 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": {}, + "launch": { + "version": "0.2.0", + "configurations": [{ + "name": "Python: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "cwd": "${fileDirname}", + "env": { + "PYTHONPATH": "${workspaceFolder}${pathSeparator}${env:PYTHONPATH}" + } + }] + } +} diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..c7d3882 --- /dev/null +++ b/Readme.md @@ -0,0 +1,17 @@ +# Readme + +Surveying terms can be checked in https://surveyingpedia.com/glossary + +## Terms + +- Reference point: A fixed survey station or control point used as the origin for measurements +- Baseline: The main survey line between two reference points, along which chainages are measured +- Chainage: The linear distance measured along the baseline from a fixed starting point +- Offset: A perpendicular distance measured from the baseline to locate a feature +- Tie measurement: General term for measurements taken to “tie” features to the baseline, often recorded in a tie table. +- Detail point: A sub-point or feature (tree, culvert, corner) located by offsets from the baseline +- Tie table / Offset table: The tabular record of chainages and offsets used to reconstruct the sketch. +- Tie sketch: The graphical sketch showing how offsets are taken from the baseline to locate features. +- Plot corners: The boundary-defining points of the parcel. Corners are usually labeled A, B, C, D, … in clockwise sequence around the plot +- Field book: The official record where chainages, offsets, and sketches are entered during survey work. + diff --git a/data/2016_3_correction_sketch_ladder.xlsx b/data/2016_3_correction_sketch_ladder.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c43c4b362525c2501b655bb3080be7f1183155b1 GIT binary patch literal 6495 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 I>FuZg1MG}55&!@I literal 0 HcmV?d00001 diff --git a/data/2016_3_revised_sketch_ladder.xlsx b/data/2016_3_revised_sketch_ladder.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..2a389772eed0244889977ceedb82694e1a3e4945 GIT binary patch literal 6742 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 GfBy$Uu5!cx literal 0 HcmV?d00001 diff --git a/pdm.lock b/pdm.lock new file mode 100644 index 0000000..d6259ad --- /dev/null +++ b/pdm.lock @@ -0,0 +1,369 @@ +# This file is @generated by PDM. +# It is not intended for manual editing. + +[metadata] +groups = ["default"] +strategy = ["inherit_metadata"] +lock_version = "4.5.0" +content_hash = "sha256:807c6594612226bbdd8f9404d6192f7fa6a1f0d16b3b0daab32f02725bde6505" + +[[metadata.targets]] +requires_python = "==3.14.*" + +[[package]] +name = "contourpy" +version = "1.3.3" +requires_python = ">=3.11" +summary = "Python library for calculating contours of 2D quadrilateral grids" +groups = ["default"] +dependencies = [ + "numpy>=1.25", +] +files = [ + {file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"}, + {file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"}, + {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"}, + {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"}, + {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"}, + {file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"}, + {file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"}, + {file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"}, + {file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"}, + {file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"}, + {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"}, + {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"}, + {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"}, + {file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"}, + {file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"}, + {file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"}, + {file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"}, +] + +[[package]] +name = "cycler" +version = "0.12.1" +requires_python = ">=3.8" +summary = "Composable style cycles" +groups = ["default"] +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[[package]] +name = "et-xmlfile" +version = "2.0.0" +requires_python = ">=3.8" +summary = "An implementation of lxml.xmlfile for the standard library" +groups = ["default"] +files = [ + {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, + {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, +] + +[[package]] +name = "ezdxf" +version = "1.4.3" +requires_python = ">=3.10" +summary = "A Python package to create/manipulate DXF drawings." +groups = ["default"] +dependencies = [ + "fonttools", + "numpy", + "pyparsing>=2.0.1", + "typing-extensions>=4.6.0", +] +files = [ + {file = "ezdxf-1.4.3-py3-none-any.whl", hash = "sha256:19e464aa4525dca3f1dabce165308de7ac262f1122b3c3986320cbec9e8ca6be"}, + {file = "ezdxf-1.4.3.tar.gz", hash = "sha256:403adf7ce305877f6c9f3c007fe2e5c5df504dfb797032122abedd7170176764"}, +] + +[[package]] +name = "fonttools" +version = "4.62.1" +requires_python = ">=3.10" +summary = "Tools to manipulate font files" +groups = ["default"] +files = [ + {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fa1d16210b6b10a826d71bed68dd9ec24a9e218d5a5e2797f37c573e7ec215ca"}, + {file = "fonttools-4.62.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aa69d10ed420d8121118e628ad47d86e4caa79ba37f968597b958f6cceab7eca"}, + {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd13b7999d59c5eb1c2b442eb2d0c427cb517a0b7a1f5798fc5c9e003f5ff782"}, + {file = "fonttools-4.62.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8d337fdd49a79b0d51c4da87bc38169d21c3abbf0c1aa9367eff5c6656fb6dae"}, + {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d241cdc4a67b5431c6d7f115fdf63335222414995e3a1df1a41e1182acd4bcc7"}, + {file = "fonttools-4.62.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c05557a78f8fa514da0f869556eeda40887a8abc77c76ee3f74cf241778afd5a"}, + {file = "fonttools-4.62.1-cp314-cp314-win32.whl", hash = "sha256:49a445d2f544ce4a69338694cad575ba97b9a75fff02720da0882d1a73f12800"}, + {file = "fonttools-4.62.1-cp314-cp314-win_amd64.whl", hash = "sha256:1eecc128c86c552fb963fe846ca4e011b1be053728f798185a1687502f6d398e"}, + {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:1596aeaddf7f78e21e68293c011316a25267b3effdaccaf4d59bc9159d681b82"}, + {file = "fonttools-4.62.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8f8fca95d3bb3208f59626a4b0ea6e526ee51f5a8ad5d91821c165903e8d9260"}, + {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee91628c08e76f77b533d65feb3fbe6d9dad699f95be51cf0d022db94089cdc4"}, + {file = "fonttools-4.62.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f37df1cac61d906e7b836abe356bc2f34c99d4477467755c216b72aa3dc748b"}, + {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92bb00a947e666169c99b43753c4305fc95a890a60ef3aeb2a6963e07902cc87"}, + {file = "fonttools-4.62.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bdfe592802ef939a0e33106ea4a318eeb17822c7ee168c290273cbd5fabd746c"}, + {file = "fonttools-4.62.1-cp314-cp314t-win32.whl", hash = "sha256:b820fcb92d4655513d8402d5b219f94481c4443d825b4372c75a2072aa4b357a"}, + {file = "fonttools-4.62.1-cp314-cp314t-win_amd64.whl", hash = "sha256:59b372b4f0e113d3746b88985f1c796e7bf830dd54b28374cd85c2b8acd7583e"}, + {file = "fonttools-4.62.1-py3-none-any.whl", hash = "sha256:7487782e2113861f4ddcc07c3436450659e3caa5e470b27dc2177cade2d8e7fd"}, + {file = "fonttools-4.62.1.tar.gz", hash = "sha256:e54c75fd6041f1122476776880f7c3c3295ffa31962dc6ebe2543c00dca58b5d"}, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +requires_python = ">=3.10" +summary = "A fast implementation of the Cassowary constraint solver" +groups = ["default"] +files = [ + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02"}, + {file = "kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a"}, + {file = "kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79"}, + {file = "kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e"}, + {file = "kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681"}, + {file = "kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57"}, + {file = "kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a"}, +] + +[[package]] +name = "matplotlib" +version = "3.10.8" +requires_python = ">=3.10" +summary = "Python plotting package" +groups = ["default"] +dependencies = [ + "contourpy>=1.0.1", + "cycler>=0.10", + "fonttools>=4.22.0", + "kiwisolver>=1.3.1", + "numpy>=1.23", + "packaging>=20.0", + "pillow>=8", + "pyparsing>=3", + "python-dateutil>=2.7", +] +files = [ + {file = "matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f"}, + {file = "matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b"}, + {file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d"}, + {file = "matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008"}, + {file = "matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c"}, + {file = "matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11"}, + {file = "matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8"}, + {file = "matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50"}, + {file = "matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908"}, + {file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a"}, + {file = "matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1"}, + {file = "matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c"}, + {file = "matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b"}, + {file = "matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f"}, + {file = "matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3"}, +] + +[[package]] +name = "numpy" +version = "2.4.4" +requires_python = ">=3.11" +summary = "Fundamental package for array computing in Python" +groups = ["default"] +files = [ + {file = "numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a"}, + {file = "numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b"}, + {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a"}, + {file = "numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d"}, + {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252"}, + {file = "numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f"}, + {file = "numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc"}, + {file = "numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74"}, + {file = "numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113"}, + {file = "numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d"}, + {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d"}, + {file = "numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f"}, + {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0"}, + {file = "numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150"}, + {file = "numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871"}, + {file = "numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e"}, + {file = "numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7"}, + {file = "numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0"}, +] + +[[package]] +name = "openpyxl" +version = "3.1.5" +requires_python = ">=3.8" +summary = "A Python library to read/write Excel 2010 xlsx/xlsm files" +groups = ["default"] +dependencies = [ + "et-xmlfile", +] +files = [ + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, +] + +[[package]] +name = "packaging" +version = "26.1" +requires_python = ">=3.8" +summary = "Core utilities for Python packages" +groups = ["default"] +files = [ + {file = "packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f"}, + {file = "packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de"}, +] + +[[package]] +name = "pillow" +version = "12.2.0" +requires_python = ">=3.10" +summary = "Python Imaging Library (fork)" +groups = ["default"] +files = [ + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"}, + {file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"}, + {file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"}, + {file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"}, + {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"}, + {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"}, + {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"}, + {file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"}, + {file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"}, + {file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"}, + {file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"}, + {file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"}, + {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"}, + {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"}, + {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"}, + {file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"}, + {file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"}, + {file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"}, + {file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"}, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +requires_python = ">=3.9" +summary = "pyparsing - Classes and methods to define and execute parsing grammars" +groups = ["default"] +files = [ + {file = "pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d"}, + {file = "pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc"}, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Extensions to the standard Python datetime module" +groups = ["default"] +dependencies = [ + "six>=1.5", +] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[[package]] +name = "scipy" +version = "1.17.1" +requires_python = ">=3.11" +summary = "Fundamental algorithms for scientific computing in Python" +groups = ["default"] +dependencies = [ + "numpy<2.7,>=1.26.4", +] +files = [ + {file = "scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b"}, + {file = "scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866"}, + {file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350"}, + {file = "scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118"}, + {file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068"}, + {file = "scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118"}, + {file = "scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19"}, + {file = "scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39"}, + {file = "scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca"}, + {file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad"}, + {file = "scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a"}, + {file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4"}, + {file = "scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2"}, + {file = "scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484"}, + {file = "scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21"}, + {file = "scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0"}, +] + +[[package]] +name = "six" +version = "1.17.0" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +summary = "Python 2 and 3 compatibility utilities" +groups = ["default"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +requires_python = ">=3.9" +summary = "Backported and Experimental Type Hints for Python 3.9+" +groups = ["default"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "xlrd" +version = "2.0.2" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +summary = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" +groups = ["default"] +files = [ + {file = "xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9"}, + {file = "xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3b5f787 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "surveyladder" +version = "0.1.0" +description = "Default template for PDM package" +authors = [ + {name = "Alexander Rossmanith", email = ""}, +] +dependencies = ["ezdxf>=1.4.3", "matplotlib>=3.10.8", "numpy>=2.4.4", "xlrd>=2.0.2", "openpyxl>=3.1.5", "scipy>=1.17.1"] +requires-python = "==3.14.*" +readme = "README.md" +license = {text = "MIT"} + + +[tool.pdm] +distribution = false diff --git a/scripts/generate_dotenv.py b/scripts/generate_dotenv.py new file mode 100644 index 0000000..fb0badd --- /dev/null +++ b/scripts/generate_dotenv.py @@ -0,0 +1,10 @@ +import pathlib + +script_file = pathlib.Path(__file__) +script_dir = script_file.parent +project_root = script_dir.parent +dotenv_path = project_root / '.env' + +with open(dotenv_path, 'w') as f: + f.write(f"# This file is automatically generated by the setup script {script_dir.name}/{script_file.name}. Do not edit it manually.\n") + f.write(f"PYTHONPATH={project_root};{project_root / 'surveyladder'}\n") diff --git a/surveyladder/dxf.py b/surveyladder/dxf.py new file mode 100644 index 0000000..63894df --- /dev/null +++ b/surveyladder/dxf.py @@ -0,0 +1,170 @@ +import ezdxf +import numpy + +from geo import bounding_box, poly_area, poly_centroid + + +def to_dxf(sketch, layer_prefix=None): + doc = ezdxf.new('R2010') + + if layer_prefix is None: + layer_prefix = "" + + # --- Define layers upfront --- + # name, color, linetype, lineweight, visible + # Colors are given by AutoCAD color index ACI + # 1= red, 2 = yellow, 3 = green, 4 = cyan, 5 = blue, 6 = magenta, 7 = white + layers = [ + ("LINES", 2, "Continuous", 0.25, True), + ("SUBDIVISIONS", 7, "Continuous", 0.35, True), + ("SUBDIVISION_LABELS", 7, "Continuous", 0.00, True), + ("CORNER_POINTS", 1, "Continuous", 0.25, True), + ("CORNER_LABELS", 1, "Continuous", 0.00, True), + ("CORNER_DISTANCES", 1, "Continuous", 0.25, True), + ("TIE_TABLE_POINTS", 6, "Continuous", 0.25, True), + ("TIE_TABLE_LABELS", 6, "Continuous", 0.00, True), + ("CHAINAGE_POINTS", 4, "Continuous", 0.25, True), + ("CHAINAGE_LABELS", 4, "Continuous", 0.00, True), + ] + for name, color, linetype, lineweight, visible in layers: + if name not in doc.layers: + layer = doc.layers.new(name=layer_prefix+name, dxfattribs={ + "color": color, + "linetype": linetype, + "lineweight": lineweight, + }) + else: + layer = doc.layers[layer_prefix+name] + if visible: + layer.on() + else: + layer.off() + + msp = doc.modelspace() + + point_positions = sketch.get_point_positions() + + # --- Draw struts (lines) --- + for strut in sketch.struts: + endpoint_positions = [point_positions[label] for label in strut] + msp.add_line(*endpoint_positions, dxfattribs={'layer': layer_prefix+'LINES'}) + + # --- Draw subdivision polygons + labels --- + for subdivision_label, poly_labels in sketch.subdivisions.items(): + if len(poly_labels) > 0: + polygon = numpy.array([point_positions[label] for label in poly_labels]) + msp.add_lwpolyline(polygon, close=True, dxfattribs={'layer': layer_prefix+'SUBDIVISIONS'}) + area = abs(poly_area(polygon)) + centroid = poly_centroid(polygon) + mtext = msp.add_mtext( + f"{subdivision_label}\n{area:.0f} sq.m\n{area/40.47:.1f} cents", + dxfattribs={'layer': layer_prefix+'SUBDIVISION_LABELS', 'char_height': 2.5} + ) + mtext.set_location((centroid[0], centroid[1])) + mtext.dxf.attachment_point = 5 # MIDDLE_CENTER + + # --- Draw points + labels --- + for point in sketch.points: + if point.source: + points_layer = point.source.upper() + "_POINTS" + labels_layer = point.source.upper() + "_LABELS" + else: + points_layer = 'POINTS' + labels_layer = 'LABELS' + msp.add_point((point.pos[0], point.pos[1]), dxfattribs={'layer': layer_prefix+points_layer}) + msp.add_text( + point.label, + dxfattribs={ + 'insert': (point.pos[0], point.pos[1]), + 'height': 2.5, + 'layer': layer_prefix+labels_layer + } + ) + + # Draw dimension lines + + for dim in sketch.dimensions: + label1, label2, distance = dim + pos1 = point_positions[label1] + pos2 = point_positions[label2] + dim_entity = msp.add_aligned_dim( + p1=(pos1[0], pos1[1]), + p2=(pos2[0], pos2[1]), + distance=0, + text=f"{distance:.1f}", + dxfattribs={'layer': layer_prefix+'CORNER_DISTANCES'} + ) + dim_entity.set_text_align(halign="center", valign="below") + dim_entity.render() + + # --- Set up the viewport to fit the sketch --- + + xmin, xmax, ymin, ymax = bounding_box(point_positions.values()) + cx = (xmin + xmax) / 2 + cy = (ymin + ymax) / 2 + doc.set_modelspace_vport( + center=(cx, cy), # center of the view + height=(ymax - ymin), # vertical size of the view +) + return doc + + + +def merge_dxf(doc1: ezdxf.document.Drawing, doc2: ezdxf.document.Drawing, + prefix1: str = "", prefix2: str = "") -> ezdxf.document.Drawing: + """ + Merge two DXF documents into one. Entities from doc1 go to layers prefixed with prefix1, + entities from doc2 go to layers prefixed with prefix2. Layer attributes are preserved. + """ + merged_doc = ezdxf.new(doc1.dxfversion) + msp = merged_doc.modelspace() + + def ensure_layer(src_layer, prefix): + name = f"{prefix}{src_layer.dxf.name}" + if name in merged_doc.layers: + return name + # src_layer.dxfattribs() returns a dict; remove keys not allowed by layers.new + attribs = src_layer.dxfattribs().copy() + attribs.pop("name", None) + # create new layer with same attributes + merged_doc.layers.new(name=name, dxfattribs=attribs) + return name + + # copy layers from both docs (with prefix) + for layer in doc1.layers: + ensure_layer(layer, prefix1) + for layer in doc2.layers: + ensure_layer(layer, prefix2) + + def copy_entities(src_doc, prefix): + for e in src_doc.modelspace(): + # get source layer name (some entities may not have dxf.layer) + src_layer = getattr(e.dxf, "layer", None) + target_layer = f"{prefix}{src_layer}" if src_layer else None + + # make a deep copy of entity + new = e.copy() + + # assign to target layer if available + if target_layer: + # ensure the layer exists (defensive) + if target_layer not in merged_doc.layers: + merged_doc.layers.new(name=target_layer) + try: + new.dxf.layer = target_layer + except Exception: + # some entity types may not support layer assignment; skip setting layer + pass + + # add entity to merged modelspace via msp.add_entity or modelspace add functions + # prefer msp.add_entity for a direct copy object + if isinstance(new, ezdxf.entities.DXFEntity): + msp.add_entity(new) + else: + # fallback: use generic add if needed (should rarely happen) + msp.add_entity(new) + + copy_entities(doc1, prefix1) + copy_entities(doc2, prefix2) + + return merged_doc diff --git a/surveyladder/geo.py b/surveyladder/geo.py new file mode 100644 index 0000000..3a2341d --- /dev/null +++ b/surveyladder/geo.py @@ -0,0 +1,127 @@ +import math + +import numpy + + +def distance(p1, p2): + return numpy.linalg.norm(p2 - p1) + + +def poly_area(polygon): + """ + returns signed polygon area + positive if points are ordered counter-clockwise, + negative if points are ordered clockwise. + """ + assert polygon.shape[0] >= 3, "polygon should have at least 3 points" + assert polygon.shape[1] == 2, "polygon should be a list of 2D points" + x = polygon[:, 0] + y = polygon[:, 1] + return 0.5 * (numpy.dot(y, numpy.roll(x, 1)) - numpy.dot(x, numpy.roll(y, 1))) + + +def poly_centroid(polygon): + """ + returns polygon centroid + """ + assert polygon.shape[0] >= 3, "polygon should have at least 3 points" + assert polygon.shape[1] == 2, "polygon should be a list of 2D points" + x = polygon[:, 0] + y = polygon[:, 1] + A = poly_area(polygon) # signed area + factor = (y * numpy.roll(x, 1) - numpy.roll(y, 1) * x) + Cx = (1/(6*A)) * numpy.sum((x + numpy.roll(x, 1)) * factor) + Cy = (1/(6*A)) * numpy.sum((y + numpy.roll(y, 1)) * factor) + return numpy.array([Cx, Cy]) + + +def chainage_to_xy(A, B, chainage): + """ + Gets point on line from A to B + + Args: + A: Start point of line + B: End point of line + chainage: distance from A of point on line + """ + vec_ab = B-A + return A + vec_ab / numpy.linalg.norm(vec_ab) * chainage + + +def chainage_offset_to_xy(A, B, chainage, offset): + """ + Gets point with perpendicular offset on line from B to A. + + Args: + A: Start point of line + B: End point of line + chainage: distance from A of closest point on line + offset: perpendicular distance of point from line + """ + P = chainage_to_xy(A, B, chainage) + line_direction = B-A + perpendicular_direction = numpy.array([-line_direction[1], line_direction[0]]) + Q = chainage_to_xy(P, P + perpendicular_direction, offset) + return Q + + +def trilaterate_xy(A: str, a: float, B: str, b: float) -> numpy.ndarray: + """ + Calculates the coordinates of point C of a triangle given by two points + A and B and the distances a and b from these points to C. + + Args: + A: coordinates of point A as a numpy array [x, y] + a: distance from point A to point C + B: coordinates of point B as a numpy array [x, y] + b: distance from point B to point C + """ + vec_c = B-A + c = numpy.linalg.norm(vec_c) + orientation_c = math.atan2(vec_c[1], vec_c[0]) + cos_alpha = (a*a+c*c-b*b)/(2*a*c) # cosine rule + alpha = math.acos(cos_alpha) + cx = A[0] + a * math.cos(orientation_c + alpha) + cy = A[1] + a * math.sin(orientation_c + alpha) + C = numpy.array([cx, cy]) + return C + + +def intersect_lines_xy(p, q): + """ + Find intersection point of two lines. + Each point is a tuple (x, y). + Returns (x, y) if intersection exists, else None if lines are parallel. + """ + p1, p2 = p + q1, q2 = q + + # Direction vectors + d1 = p2 - p1 + d2 = q2 - q1 + + # Solve system: p1 + t*d1 = q1 + u*d2 + A = numpy.array([[d1[0], -d2[0]], [d1[1], -d2[1]]]) + b = q1 - p1 + + det = numpy.linalg.det(A) + if abs(det) < 1e-10: + # Lines are parallel (or coincident) + return None + + t, u = numpy.linalg.solve(A, b) + intersection = p1 + t * d1 + return intersection + + +def get_point_by_distance_at_angle(P, dist, angle_deg): + angle = numpy.deg2rad(angle_deg) + dir_vec = numpy.array([numpy.cos(angle), numpy.sin(angle)]) + Q = P + dist * dir_vec + return Q + + +def bounding_box(positions): + xs = [p[0] for p in positions] + ys = [p[1] for p in positions] + return min(xs), max(xs), min(ys), max(ys) diff --git a/surveyladder/ladder.py b/surveyladder/ladder.py new file mode 100644 index 0000000..a7a7f0d --- /dev/null +++ b/surveyladder/ladder.py @@ -0,0 +1,165 @@ +import csv +from dataclasses import dataclass + +from solver import DirectSolver +import openpyxl + +from util import is_corner_label + +from sketch import Point + +@dataclass +class OffsetPoint: + """ + A point on the ladder with a label, distance along the line, and offset from the line. + Together with the definition of the line (start and end points), this allows us to calculate the + position of the point in 2D space. + """ + + label: str + distance_on_line: float + offset: float + + +@dataclass +class LadderSegment: + + """ + A segment between two seed points, containing the distance between the seed points and + all the offset points in between. + """ + + start_label: str + end_label: str + distance: float + offset_points: list[OffsetPoint] + + +def extract_all_corner_labels(ladder_segments): + corner_labels = list() + for segment in ladder_segments: + if segment.start_label not in corner_labels: + corner_labels.append(segment.start_label) + if segment.end_label not in corner_labels: + corner_labels.append(segment.end_label) + return sorted(corner_labels) + + +def load_ladder_xls(filename): + """ + Loads survey ladder from xlsx file + """ + workbook = openpyxl.load_workbook(filename) + worksheet = workbook.active + rows = [] + def to_str(val): + if val is None: + return "" + return str(val) + for row_index in range(1, worksheet.max_row + 1): + row = [to_str(worksheet.cell(row=row_index, column=col_index).value) for col_index in range(1, 6)] + rows.append(row) + return create_ladder(rows) + + +def load_ladder_csv(filename, invert_segments=False): + """ + Loads survey ladder from csv file + """ + rows = load_csv_rows(filename) + return create_ladder(rows, invert_segments=invert_segments) + + + +def load_chainage_csv(filename): + """ + Loads chainage points from csv file + """ + return load_csv_rows(filename) + + +def load_csv_rows(filename): + with open(filename, newline="") as csvfile: + reader = csv.reader(csvfile) + return [row for row in reader] + + +def create_ladder(rows, invert_segments=False): + """ + Creates a survey ladder from a list of rows, where each row is a 5-tuple of (left_label, left_offset, distance_on_line, right_offset, right_label). + """ + # column 1 contains the labels of the ladder points on the left side. + # column 2 contains the offsets of the ladder points on the left side. + # column 3 contains the labels of the reference points, and the distances between them. + # column 4 contains the offsets of the ladder points on the right side. + # column 5 contains the distances of the ladder points on the right side. + segments = [] + for (start_row, end_row) in get_segment_borders(rows): + start_label = str(rows[start_row][2]) + end_label = str(rows[end_row][2]) + if invert_segments: + start_label, end_label = end_label, start_label + distance = parse_distance(rows[start_row+1][2]) + offset_points = [] + for i in range(start_row + 2, end_row): + offset_points.extend(parse_offset_points(rows[i])) + segments.append(LadderSegment(start_label, end_label, distance, offset_points)) + return segments + + +def parse_offset_points(row): + points = [] + distance_on_line = parse_distance(row[2]) + left_label = row[0] + right_label = row[4] + if left_label: + left_label = str(left_label) + left_offset = parse_distance(row[1]) + points.append(OffsetPoint(left_label, distance_on_line, left_offset)) + if right_label: + right_label = str(right_label) + right_offset = parse_distance(row[3]) + points.append(OffsetPoint(right_label, distance_on_line, -right_offset)) + return points + + +def get_segment_borders(rows): + segment_borders = [] + start_row = None + for i, row in enumerate(rows): + if is_corner_label(row[2]): + if start_row is None: + start_row = i + else: + end_row = i + segment_borders.append((start_row, end_row)) + start_row = None + return segment_borders + + +def parse_distance(val): + if isinstance(val, str): + val = val.strip().lower() + if val == "line": + return 0.0 + if (val.startswith("(") and val.endswith(")")): + val = val[1:-1] + return float(val) + else: + return float(val) + + +def get_corner_points(ladder_segments): + origin_label = ladder_segments[0].start_label + corner_labels = extract_all_corner_labels(ladder_segments) + solver = DirectSolver(corner_labels, origin_label=origin_label) + for segment in ladder_segments: + solver.add_distance(segment.start_label, segment.end_label, segment.distance) + for offset_point in segment.offset_points: + if is_corner_label(offset_point.label): + solver.add_offset(offset_point.label, segment.start_label, segment.end_label, offset_point.distance_on_line, offset_point.offset) + P = solver.solve() + points = list() + for label in corner_labels: + points.append(Point(label, P[label], "corner")) + return points diff --git a/surveyladder/main.py b/surveyladder/main.py new file mode 100644 index 0000000..4e79687 --- /dev/null +++ b/surveyladder/main.py @@ -0,0 +1,369 @@ +import json +import math +import pathlib + +import numpy as np + +from geo import intersect_lines_xy, chainage_to_xy, get_point_by_distance_at_angle, trilaterate_xy +from ladder import load_ladder_csv, load_chainage_csv, get_corner_points +from sketch import Sketch, Point +from dxf import to_dxf, merge_dxf +from plot import plot + +def main(): + + sketch_1987 = load_sketch( + R"C:\Data\Rat killer\Survey Sketches\Official\1987-12-01\sketch.json") + dxf_1987 = to_dxf(sketch_1987) + dxf_1987.saveas("survey_1987_sketch.dxf") + + sketch_2006 = load_sketch( + R"C:\Data\Rat killer\Survey Sketches\Official\2006-10-16 1016 subdivision 1,3,2,4,5,6\sketch.json") + dxf_2006 = to_dxf(sketch_2006) + dxf_2006.saveas("survey_2006_sketch.dxf") + + merge_dxf(dxf_1987, dxf_2006, "1987", "2006").saveas("survey_1987_2006_sketch.dxf") + +def load_sketch(sketchinfo_filepath): + sketchinfo_filepath = pathlib.Path(sketchinfo_filepath) + sketch_dir = sketchinfo_filepath.parent + sketchinfo = json.load(open(sketchinfo_filepath)) + sketch = Sketch() + invert_segments = sketchinfo["tie_table"]["invert_segments"] + ladder = load_ladder_csv(sketch_dir/"tie_table.csv", invert_segments=invert_segments) + P = get_corner_points(ladder) + sketch = Sketch(points=P, subdivisions=sketchinfo["subdivisions"]) + sketch.add_ladder_points(ladder) + point_positions = sketch.get_point_positions() + chainage_points = load_chainage_csv(sketch_dir / "chainage.csv") + for label, start_label, end_label, chainage in chainage_points: + start_point = point_positions[start_label] + end_point = point_positions[end_label] + chainage = float(chainage) + point_positions[label] = chainage_to_xy(start_point, end_point, chainage) + sketch.points.append(Point(label, chainage_to_xy(start_point, end_point, chainage), "chainage")) + sketch.reorient( + sketchinfo["origin"], + sketchinfo["northing"]["line"], + math.radians(sketchinfo["northing"]["orientation"])) + for seg in ladder: + sketch.add_dimension_line(seg.start_label, seg.end_label, seg.distance) + return sketch + + +def survey_shibu_ikka_sketch_march7(): + P = dict() + P["D"] = np.array([0, 0]) + triangulation = [ + ("C", "D", 130.5), + ("C", "F", 138.5), + ("C", "G", 133.2), + ("D", "F", 161.3), + ] + P["F"] = np.array(P["D"] + [0, -161.3]) + P["C"] = trilaterate_xy(P["F"], 138.5, P["D"], 130.5) + P["G"] = trilaterate_xy(P["F"], 157.6, P["C"], 133.2) + P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3) + P["A"] = trilaterate_xy(P["G"], 120.8, P["B"], 58.0) + sketch = Sketch(points=P) + + ladder = ([ + ("F", "D", [ + ("29", 2.7, 153.5), + ("28", 12.9, 123.2), + ("27", 13.2, 114.6), + ("E", -2.1, 161.2), + ("14", 11.2, 106.9), + ("26", 10.4, 104.2), + ("25", 24.6, 99.9), + ("24", 28.4, 99.0), + ("23", 29.4, 98.2), + ("22", 23.5, 97.2), + ("21", 28.7, 91.8), + ("21", 27.5, 81.8), + ("20", 24.2, 52.6), + ]), + ("G", "C", [ + ("B", 10.0, 126.0), + ]), + ("F", "C", [ + ("16", -34.0, 62.3), + ("13", -36.7, 91.0), + ]), + ("G", "A", [ + ("B", -56.9, 112.6), + ("12", 16.8, 97.2), + ("11", 11.1, 62.8), + ]), + ("F", "G", [ + ("10", -2.6, 130.2), + ]), + ("F", "E", [ + ("9", -0.2, 159.0), + ("8", 1.4, 157.8), + ]), + ("D", "C", [ + ("7", 4.4, 117.5), + ("6", -3.4, 110.2), + ("5", -2.0, 107.1), + ("4", -0.4, 98.6), + ("3", 3.8, 69.2), + ("18", 8.1, 50.8), + ("17", 7.4, 50.4), + ("16a", 5.7, 30.7), + ("15", 7.1, 30.0), + ("2", 0.0, 24.4), + ("15", 21.2, 49.0), + ]), + ("B", "A", [ + ("1", 2.1, 15.4),]) + ]) + sketch.add_ladder_points(ladder) + sketch.points["8_F_1"] = chainage_to_xy(sketch.points["8"], sketch.points["F"], 2.1) + sketch.points["F_10_1"] = chainage_to_xy(sketch.points["F"], sketch.points["10"], 13.0) + sketch.add_polyline_to_struts(("D", "2", "3", "4", "5", "6", "7", "C", "B", "1", "A", "12", "11", "G", "10", "F", "8", "9", "E", "D")) + sketch.add_polyline_to_struts(("7", "13", "14")) + sketch.add_polyline_to_struts(("F_10_1", "20", "21", "15")) + sketch.reorient(origin_label="G", direction_labels=("D", "F"), angle=math.radians(-100)) + return sketch + + +def survey_1016_3_revised_sketch(): + #lad = load_ladder_xls("data/2016_3_revised_sketch_ladder.xlsx") + #P = get_seed_points(lad) + # P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3) + P = dict() + P["D"] = np.array([0, 0]) + P["F"] = np.array(P["D"] + [0, -161.3]) + P["C"] = trilaterate_xy(P["F"], 138.5, P["D"], 130.5) + P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3) + P["G"] = trilaterate_xy(P["F"], 157.6, P["B"], 126.0) + ladder = ([ + ("F", "D", [ + ("30", 2.7, 153.5), + ("29", 12.9, 123.5), + ("28", 13.2, 114.6), + ("E", -2.1, 161.2), + ("14", 11.2, 106.9), + ("27", 10.4, 104.2), + ("26", 24.6, 99.9), + ("25", 28.4, 99.0), + ("24", 29.4, 98.2), + ("23", 23.5, 97.2), + ("22", 28.7, 91.8), + ("21", 27.5, 81.8), + ("20", 24.2, 52.6), + ]), + ("G", "C", [ + ("B0", 10.0, 126.0), + ]), + ("F", "C", [ + ("13", -36.7, 91.0), + ("16_old", -34.0, 62.3) + ]), + ("F", "G", [ + ("10", -2.6, 130.0), + ]), + ("F", "E", [ + ("9", -0.2, 159.0), + ("8", 1.4, 157.8), + ]), + ("D", "C", [ + ("7", 4.4, 117.5), + ("6", -3.4, 110.2), + ("5", -2.0, 107.1), + ("4", -0.4, 98.6), + ("3", 3.8, 69.2), + ("19", 8.1, 50.8), + ("18", 7.4, 50.4), + ("17", 5.7, 30.7), + ("16", 7.1, 30.0), + ("2", 0.0, 24.4), + ("15", 21.2, 49.0), + ]) + ]) + sketch = Sketch(points=P) + sketch.add_ladder_points(ladder) + + sketch.points["C1"] = chainage_to_xy(sketch.points["7"], sketch.points["C"], 2.8) + + sketch.points["F1"] = chainage_to_xy(sketch.points["F"], sketch.points["10"], 13.3) + sketch.points["F2"] = chainage_to_xy(sketch.points["F1"], sketch.points["10"], 29.5) + sketch.points["F3"] = chainage_to_xy(sketch.points["F2"], sketch.points["10"], 30.6) + sketch.points["F4"] = chainage_to_xy(sketch.points["F3"], sketch.points["10"], 26.4) + + sketch.points["101"] = chainage_to_xy(sketch.points["26"], sketch.points["18"], 12.4718) + sketch.points["27a"] = chainage_to_xy(sketch.points["14"], sketch.points["16"], 7.9762) + sketch.points["102"] = chainage_to_xy(sketch.points["27a"], sketch.points["16"], 4.5389) + sketch.points["3a"] = chainage_to_xy(sketch.points["3"], sketch.points["F2"], 7.20) + sketch.points["4a"] = chainage_to_xy(sketch.points["3"], sketch.points["4"], 21.9) + sketch.points["8a"] = chainage_to_xy(sketch.points["8"], sketch.points["F"], 4) + sketch.points["18a"] = chainage_to_xy(sketch.points["18"], sketch.points["3a"], 4.1) + sketch.points["18b"] = chainage_to_xy(sketch.points["18a"], sketch.points["18"], 3.65) + sketch.points["F8"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 110.7) + sketch.points["F9"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 110.7) + sketch.points["14F3"] = chainage_to_xy(sketch.points["F9"], sketch.points["8"], 16.3) + sketch.points["17line30"] = chainage_to_xy(sketch.points["17"], sketch.points["30"], 4.2) + + sketch.reorient(origin_label="G", direction_labels=("D", "F"), angle=math.radians(-100)) + + sketch.struts = ( + [("D", "E"), + ("25", "24"), ("24", "22"), ("22", "21"), ("21", "20"), ("20", "F1"), + ("7", "13"), ("13", "22"), + ("8", "F"), ("C1", "F4"), + ("3", "F2"), ("8a", "30"), ("F8", "14"), + ("101", "102"), ("18", "3a"), ("18a", "25"), + ("F3", "4a"), ("E", "9"), ("8", "9"), ("30", "17"), + ("29", "14F3"), ("17line30", "29")] + ) + sketch.add_polyline_to_struts(["C", "B", "G"]) + sketch.add_polyline_to_struts(["23", "27", "16", "17", "18", "18b", "23"]) + sketch.add_polyline_to_struts(["D", "2", "3", "4", "5", "6", "7", "C"]) + sketch.add_polyline_to_struts(["F", "F1", "F2", "F3", "F4", "10", "G"]) + return sketch + + +def survey_1016_3_sketch(): + """ + Sketch based on survey 2016-3 from 2006 + """ + P = dict() + P["D"] = np.array([0, 0]) + P["F"] = np.array(P["D"] + [0, -161.3]) + P["C"] = trilaterate_xy(P["F"], 138.5, P["D"], 130.5) + P["B"] = trilaterate_xy(P["F"], 145.5, P["C"], 12.3) + P["G"] = trilaterate_xy(P["F"], 157.6, P["B"], 126.0) + sketch = Sketch(points=P) + + ladder = ([ + ("F", "D", [ + ("E", -2.1, 161.2), + ("14", 12.0, 110.7), + ]), + ("G", "C", [ + ("B", 10.0, 126.0), + ]), + ("F", "C", [ + ("13", -36.7, 91.0), + ("16", -34.0, 62.3), + ("16new", -34.1, 62.2) + ]), + ("F", "G", [ + ("10", -2.6, 130.2), + ]), + ("F", "E", [ + ("9", -0.2, 159.0), + ("8", 1.4, 157.8), + ]), + ("D", "C", [ + ("7", 4.4, 117.5), + ("6", -3.4, 110.2), + ("5", -2.0, 107.1), + ("4", -0.4, 98.6), + ("3", 3.8, 69.2), + ("2", 0.0, 24.4), + ("15", 21.2, 49.0), + ]) + ]) + sketch = Sketch(points=P) + sketch.add_ladder_points(ladder) + + sketch.points["F-10-1"] = chainage_to_xy(sketch.points["F"], sketch.points["10"], 13.0) + sketch.points["F-10-2"] = chainage_to_xy(sketch.points["F-10-1"], sketch.points["10"], 29.8) + sketch.points["F-10-3"] = chainage_to_xy(sketch.points["F-10-2"], sketch.points["10"], 30.6) + sketch.points["F-10-4"] = chainage_to_xy(sketch.points["F-10-3"], sketch.points["10"], 26.4) + sketch.points["F-10-5"] = chainage_to_xy(sketch.points["F-10-4"], sketch.points["10"], 23.0) + + sketch.points["F-8-1"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 112.5) + sketch.points["F-8-2"] = chainage_to_xy(sketch.points["F"], sketch.points["8"], 123.5) + + sketch.points["7-C-1"] = chainage_to_xy(sketch.points["7"], sketch.points["C"], 2.8) + + #sketch.points["3-2-1"] = chainage_to_xy(sketch.points["3"], sketch.points["2"], 21.5) + #sketch.points["3-2-2"] = chainage_to_xy(sketch.points["3-2-1"], sketch.points["2"], 18.1) + + sketch.points["2-3-1"] = chainage_to_xy(sketch.points["2"], sketch.points["3"], 3.7) + sketch.points["2-3-2"] = chainage_to_xy(sketch.points["2-3-1"], sketch.points["3"], 18.4) + sketch.points["2-3-3"] = chainage_to_xy(sketch.points["2-3-2"], sketch.points["3"], 21.5) + + sketch.points["3-4-1"] = chainage_to_xy(sketch.points["3"], sketch.points["4"], 21.9) + + sketch.points["i-1"] = intersect_lines_xy( + (sketch.points["7"], sketch.points["13"]), + (sketch.points["3-4-1"], sketch.points["F-10-3"])) + + sketch.points["i-2"] = intersect_lines_xy( + (sketch.points["7"], sketch.points["13"]), + (sketch.points["2-3-3"], sketch.points["F-10-2"])) + + sketch.points["i-3"] = intersect_lines_xy( + (sketch.points["13"], sketch.points["F-8-2"]), + (sketch.points["15"], sketch.points["16"])) + + sketch.points["i-4"] = intersect_lines_xy( + (sketch.points["13"], sketch.points["F-8-2"]), + (sketch.points["2-3-1"], sketch.points["14"])) + + + sketch.add_polyline_to_struts(["D", "E", "9", "8", "F", "10", "G"]) + sketch.add_polyline_to_struts(["D", "2", "3", "4", "5", "6", "7", "C", "B"]) + sketch.add_polyline_to_struts(["F-10-1", "16", "15", "2-3-2"]) + sketch.add_polyline_to_struts(["7", "13", "F-8-2"]) + sketch.add_polyline_to_struts(["2-3-1", "14", "F-8-1"]) + + sketch.add_polyline_to_struts(["F-10-2", "2-3-3"]) + sketch.add_polyline_to_struts(["F-10-3", "3-4-1"]) + sketch.add_polyline_to_struts(["F-10-4", "7-C-1"]) + + sketch.subdivisions["1"] = ["7", "6", "5", "4", "3-4-1", "i-1"] + sketch.subdivisions["2"] = ["F-10-3", "F-10-4", "7-C-1", "7", "i-1"] + sketch.subdivisions["3"] = ["8", "9", "E", "D", "2", "2-3-1", "i-4", "F-8-2"] + sketch.subdivisions["4"] = ["i-4", "14", "F-8-1", "F-8-2"] + sketch.subdivisions["7"] = ["2-3-3", "3", "3-4-1", "i-1", "i-2"] + sketch.subdivisions["8"] = ["2-3-2", "2-3-3", "i-2", "13", "i-3", "15"] + sketch.subdivisions["9"] = ["i-3", "i-4", "2-3-1", "2-3-2", "15"] + sketch.subdivisions["10"] = ["i-2", "i-1", "F-10-3", "F-10-2"] + sketch.subdivisions["11"] = ["i-3", "16", "F-10-1", "F-10-2", "i-2", "13"] + sketch.subdivisions["12"] = ["F", "F-8-1", "14", "i-4", "i-3", "16", "F-10-1"] + sketch.subdivisions["?"] = ["10", "F-10-4", "7-C-1", "C", "B"] + + sketch.reorient(origin_label="G", direction_labels=("D", "F"), angle=math.radians(-93)) + + return sketch + +def shibu_ikka_sketch_march7(): + sketch = survey_1016_3_sketch() + sketch.points["BM"] = chainage_to_xy(sketch.points["2-3-1"], sketch.points["14"], 3.7) + sketch.points["8-F-1"] = chainage_to_xy(sketch.points["8"], sketch.points["F"], 2.1) + sketch.points["8-F-2"] = chainage_to_xy(sketch.points["8-F-1"], sketch.points["F"], 26.8) + return sketch + +def survey_2010(): + A = np.array([0, 0]) + B = np.array(A + [17.2, 0]) + C = np.array(B + [51.9, 0]) + D = np.array(C + [13.4, 0]) + E = np.array(D + [29.5, 0]) + H = trilaterate_xy(C, 28.5, D, 29.0) + G = trilaterate_xy(C, 27.4, H, 11.3) + F = trilaterate_xy(A, 13.6, G, 67.6) + I = trilaterate_xy(H, 24.2, D, 35.2) + K = trilaterate_xy(I, 1.65 + 3.65, D, 31.6) + J = chainage_to_xy(I, K, 1.65) + # L = trilaterate_xy(K, 15.35, E, 12.1) + print(np.linalg.norm(K-E)) + print(12.1 + 15.35) + M = get_point_by_distance_at_angle(J, 28.3, 21.5) + N = get_point_by_distance_at_angle(K, 29.9, 21.5) + print(J, M) + print(K, N) + + polygon = np.array([A, B, C, D, E, K, N, M, J, I, H, G, F]) + # polygon = np.array([A, B, C, D, E, L, K, N, M, J, I, H, G, F]) + struts = [(C, G), (C, H), (D, I), (D, K), (D, H), (J, K)] + return Sketch(points=None, subdivisions=[("Area", polygon)], struts=struts) + + +if __name__ == "__main__": + main() diff --git a/surveyladder/plot.py b/surveyladder/plot.py new file mode 100644 index 0000000..d26280d --- /dev/null +++ b/surveyladder/plot.py @@ -0,0 +1,39 @@ +import numpy + +from geo import poly_area, poly_centroid + + +def plot(ax, sketch): + """plots a property""" + for poly_labels in sketch.subdivisons: + polygon = numpy.array([sketch.subdivisons[label] for label in poly_labels]) + plot_polygon(ax, polygon) + area_in_sq_m = abs(poly_area(polygon)) + area_in_cents = area_in_sq_m / 40.47 + centroid = poly_centroid(polygon) + add_textbox(ax, centroid, f"area = {area_in_cents:.1f} cents") + for label, pos in sketch.points.items(): + add_textbox(ax, pos, label) + for strut in sketch.struts: + endpoint_positions = [sketch.points[label] for label in strut] + plot_line(ax, *endpoint_positions) + + +def plot_polygon(ax, polygon): + """plots polygon given by list of point coordinates""" + if numpy.array(polygon).size == 0: + return + for A, B in zip(polygon, numpy.roll(polygon, 1, 0)): + plot_line(ax, A, B) + + +def plot_line(ax, A, B): + """plots a line from A to B""" + ax.plot([A[0], B[0]], [A[1], B[1]], color='black', linewidth=1) + + +def add_textbox(ax, pos, text): + ax.text(pos[0], pos[1], text, + horizontalalignment='left', + # bbox=dict(facecolor='white', alpha=0.6), + fontsize=12.5) diff --git a/surveyladder/remove_empty_layers.py b/surveyladder/remove_empty_layers.py new file mode 100644 index 0000000..372bfcb --- /dev/null +++ b/surveyladder/remove_empty_layers.py @@ -0,0 +1,48 @@ +import ezdxf + +def remove_empty_layers(input_file: str, output_file: str): + # Load the DXF document + doc = ezdxf.readfile(input_file) + + # Collect all layers that have entities across ALL layouts + used_layers = set() + for layout in doc.layouts: + for entity in layout: + used_layers.add(entity.dxf.layer) + + # Also scan block definitions (important for symbols, inserts, etc.) + for block in doc.blocks: + for entity in block: + used_layers.add(entity.dxf.layer) + + # Identify layers to remove + layers_to_remove = [] + for layer in doc.layers: + if layer.dxf.name not in used_layers: + layers_to_remove.append(layer.dxf.name) + + # Remove empty layers + for layer_name in layers_to_remove: + print(f"Removing empty layer: {layer_name}") + doc.layers.remove(layer_name) + + # Save the cleaned DXF + doc.saveas(output_file) + + # Summary report + total_layers = len(doc.layers) + removed_count = len(layers_to_remove) + retained_count = total_layers + + print("\n--- Summary Report ---") + print(f"Total layers after cleanup: {total_layers}") + print(f"Layers removed: {removed_count}") + print(f"Layers retained: {retained_count}") + print(f"Saved cleaned DXF to {output_file}") + + +if __name__ == "__main__": + # Example usage + input_path = R"C:\Data\Rat killer\Resurvey docs\Shibu Ikka Survey\Kitten\2026-04-30 Overlay Feb19 with Taluk Survey V2.dxf" + output_path = R"C:\Data\Rat killer\Resurvey docs\Shibu Ikka Survey\Kitten\2026-04-30 Overlay Feb19 with Taluk Survey V2 cleaned.dxf" + remove_empty_layers(input_path, output_path) diff --git a/surveyladder/sketch.py b/surveyladder/sketch.py new file mode 100644 index 0000000..137b38b --- /dev/null +++ b/surveyladder/sketch.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass +import numpy + +from geo import chainage_offset_to_xy + + +@dataclass +class Point: + + label: str + pos: numpy.ndarray + source: str + + +class Sketch: + + def __init__(self, points=None, struts=None, subdivisions=None, dimensions=None): + self.points = points if points is not None else list() + self.subdivisions = subdivisions if subdivisions is not None else dict() + self.struts = struts if struts is not None else [] + self.dimensions = dimensions if dimensions is not None else [] + + def reorient(self, origin_label, direction_labels, angle): + """ + Reorients the sketch so that the origin point is at (0, 0) and the line from the + origin point to the direction point has the specified angle with the positive x-axis. + """ + point_positions = self.get_point_positions() + origin = point_positions[origin_label] + direction_point_0 = point_positions[direction_labels[0]] + direction_point_1 = point_positions[direction_labels[1]] + current_angle = numpy.arctan2(direction_point_1[1] - direction_point_0[1], direction_point_1[0] - direction_point_0[0]) + rotation_angle = angle - current_angle + rotation_matrix = numpy.array([[numpy.cos(rotation_angle), -numpy.sin(rotation_angle)], + [numpy.sin(rotation_angle), numpy.cos(rotation_angle)]]) + self.points = ([ + Point(label=p.label, pos=rotation_matrix @ (p.pos - origin), source=p.source) + for p in self.points + ]) + + def add_ladder_points(self, ladder): + point_positions = self.get_point_positions() + for seg in ladder: + for p in seg.offset_points: + self.points.append(Point( + label=p.label, + pos=chainage_offset_to_xy( + point_positions[seg.start_label], + point_positions[seg.end_label], + p.distance_on_line, + p.offset + ), + source="tie_table" + )) + + def add_polyline_to_struts(self, labels): + for label1, label2 in zip(labels, labels[1:]): + self.struts.append((label1, label2)) + + def add_dimension_line(self, label1, label2, distance): + self.dimensions.append((label1, label2, distance)) + + + def get_point_positions(self): + return {p.label: p.pos for p in self.points} diff --git a/surveyladder/solver.py b/surveyladder/solver.py new file mode 100644 index 0000000..013158f --- /dev/null +++ b/surveyladder/solver.py @@ -0,0 +1,217 @@ +import collections + +import numpy as np +from scipy.optimize import minimize + +from geo import poly_area, chainage_offset_to_xy, trilaterate_xy +from util import corner_labels_are_ascending + + +class DirectSolver: + + def __init__(self, point_labels, origin_label=None): + if not point_labels: + raise ValueError("No point labels given.") + self.point_labels = point_labels + self.label_to_index = {label: index for index, label in enumerate(point_labels)} + self.distances = collections.defaultdict(list) + self.offsets = collections.defaultdict(list) + self.origin_label = origin_label if origin_label is not None else self.point_labels[0] + + def add_distance(self, label1: str, label2: str, distance: float): + self.distances[label1].append((label2, distance)) + self.distances[label2].append((label1, distance)) + + def add_offset(self, label: str, start_label: str, end_label: str, chainage: float, offset: float): + self.offsets[label].append((start_label, end_label, chainage, offset)) + + + def solve(self): + self.sanity_check() + points = {} # label -> np.array([x, y]) + unprocessed_labels = set(self.point_labels) + + L0 = self.origin_label + D0 = self.distances[L0][0] + L1 = D0[0] + d0 = D0[1] + + points[L0] = np.array([0.0, 0.0]) + unprocessed_labels.remove(L0) + points[L1] = np.array([d0, 0.0]) + unprocessed_labels.remove(L1) + + def next_trilateration_candidate(): + for label in unprocessed_labels: + distances_to_processed_neighbors = [(neighbor_label, d) for (neighbor_label, d) in self.distances[label] if neighbor_label not in unprocessed_labels] + if len(distances_to_processed_neighbors) >= 2: + return label, distances_to_processed_neighbors + return None, None + + def next_offset_candidate(): + for label in unprocessed_labels: + if label in self.offsets: + for start_label, end_label, chainage, offset in self.offsets[label]: + if start_label in points and end_label in points: + return label, (start_label, end_label, chainage, offset) + return None, None + + while unprocessed_labels: + label, distances_to_processed_neighbors = next_trilateration_candidate() + while label is not None: + neighbor_1 = distances_to_processed_neighbors[0][0] + distance_1 = distances_to_processed_neighbors[0][1] + neighbor_2 = distances_to_processed_neighbors[1][0] + distance_2 = distances_to_processed_neighbors[1][1] + if corner_labels_are_ascending(label, neighbor_1, neighbor_2): + neighbor_1, neighbor_2 = neighbor_2, neighbor_1 + distance_1, distance_2 = distance_2, distance_1 + points[label] = trilaterate_xy(points[neighbor_1], distance_1, points[neighbor_2], distance_2) + unprocessed_labels.remove(label) + label, distances_to_processed_neighbors = next_trilateration_candidate() + if not unprocessed_labels: + break + label, chainage_offset = next_offset_candidate() + if label is None: + raise ValueError("Could not solve for all points, remaining unprocessed labels: " + str(unprocessed_labels)) + else: + (start_label, end_label, chainage, offset) = chainage_offset + points[label] = chainage_offset_to_xy(points[start_label], points[end_label], chainage, offset) + unprocessed_labels.remove(label) + + return points + + + def sanity_check(self): + if len(self.point_labels) < 2: + raise ValueError("At least two points are required to solve for coordinates") + if len(self.distances) < 1: + raise ValueError("Not enough distance constraints to determine coordinates") + for label, neighbors in self.distances.items(): + if len(neighbors) < 2 and label not in self.offsets: + raise ValueError(f"Label {label} has less than 2 neighbors, cannot determine position") + + + +class OptimizationSolver: + + def __init__(self, point_labels): + self.point_labels = point_labels + self.label_to_index = {label: index for index, label in enumerate(point_labels)} + self.distances = [] + self.offsets = [] + + def add_distance(self, label1, label2, distance): + self.distances.append((label1, label2, distance)) + + def add_offset(self, label, start_label, end_label, chainage, offset): + self.offsets.append((label, start_label, end_label, chainage, offset)) + + def solve(self): + self.sanity_check() + + n_points = len(self.point_labels) + points0 = np.random.rand(n_points * 2).reshape((n_points, 2)) + + constraints = build_orientation_constraints(self.point_labels) + + initial_points = points0.flatten() + # result = minimize( + # self.cost_function, initial_points, method='COBYLA', constraints=constraints, options={'tol': 1.0E-3, 'maxiter': 10000}) + result = minimize( + self.cost_function, initial_points, method='BFGS', options={'gtol': 1.0E-3, 'maxiter': 10000}) + if not result.success: + raise ValueError("Could not solve for seed points, %s", result.message) + points = result.x.reshape((n_points, 2)) + return {label: points[i] for label, i in self.label_to_index.items()} + + + def cost_function(self, flat_points): + return self.distances_cost(flat_points) + self.offsets_cost(flat_points) + self.orientation_cost(flat_points) + + + def distances_cost(self, pts): + pts = pts.reshape((-1, 2)) + total_cost = 0.0 + for label1, label2, expected_distance in self.distances: + index1, index2 = self.label_to_index[label1], self.label_to_index[label2] + calculated_distance = np.linalg.norm(pts[index2] - pts[index1]) + total_cost += (calculated_distance - expected_distance) ** 2 + return total_cost + + + def offsets_cost(self, pts): + pts = pts.reshape((-1, 2)) + total_cost = 0.0 + for label, start_label, end_label, chainage, offset in self.offsets: + start_point = pts[self.label_to_index[start_label]] + end_point = pts[self.label_to_index[end_label]] + actual_position = pts[self.label_to_index[label]] + if np.linalg.norm(end_point - start_point) == 0: + total_cost += 10000 # (2 * offset) ** 2 + else: + expected_position = chainage_offset_to_xy(start_point, end_point, chainage, offset) + total_cost += np.linalg.norm(actual_position - expected_position) ** 2 + return total_cost + + + def orientation_cost(self, pts): + pts = pts.reshape((-1, 2)) + total_cost = 0.0 + total_cost += np.sum(pts[0]**2) # penalize deviation from origin + total_cost += pts[1,1]**2 # penalize deviation from x-axis + return total_cost + + def sanity_check(self): + if len(self.point_labels) < 2: + raise ValueError("At least two points are required to solve for coordinates") + if len(self.distances) < 1: + raise ValueError("Not enough distance constraints to determine coordinates") + # for label, neighbors in extract_neighbors_from_distances(self.point_labels, self.distances).items(): + # if len(neighbors) < 2 and label not in self.offsets: + # raise ValueError(f"Label {label} has less than 2 neighbors, cannot determine position") + + +def orientation_constraint_factory(i, j, k, sign): + """ + Returns a constraint function enforcing orient(A,B,C) >= 0 + for points with indices i, j, k in the variable vector. + """ + def constraint(vars): + points = vars.reshape((-1, 2)) + return sign * poly_area(points[[i,j,k]]) # must be >= 0 + return constraint + + +def build_orientation_constraints(point_labels): + """ + Build orientation constraints for all alphabetically ordered triples + (A,B,C), (B,C,D), ... up to n_points. + """ + n_points = len(point_labels) + cons = [] + for i in range(n_points-2): + j, k = i+1, i+2 + a = point_labels[i] + b = point_labels[j] + c = point_labels[k] + if (a < b < c) or (b < c < a) or (c < a < b): + sign = -1 # clockwise order + else: + sign = 1 # counter-clockwise order + cons.append({ + 'type': 'ineq', + 'fun': orientation_constraint_factory(i, j, k, sign) + }) + return cons + + +def extract_neighbors_from_distances(point_labels, distances): + neighbors = {label: set() for label in point_labels} + for label1, label2, _ in distances: + neighbors[label1].add(label2) + neighbors[label2].add(label1) + return neighbors + + + diff --git a/surveyladder/util.py b/surveyladder/util.py new file mode 100644 index 0000000..bbbfd83 --- /dev/null +++ b/surveyladder/util.py @@ -0,0 +1,19 @@ +def is_corner_label(label): + return is_single_uppercase_letter(label) + + +def is_single_uppercase_letter(s: str) -> bool: + """ + Check if the string is exactly one uppercase letter (A-Z). + """ + if not isinstance(s, str): + return False + return len(s) == 1 and s.isupper() and s.isalpha() + + +def corner_labels_are_ascending(label1, label2, label3): + if not (is_corner_label(label1) and is_corner_label(label2) and is_corner_label(label3)): + raise ValueError("Labels must be single uppercase letters") + if label1 == label2 or label2 == label3 or label1 == label3: + raise ValueError("Labels must be distinct") + return (label1 < label2 < label3) or (label2 < label3 < label1) or (label3 < label1 < label2) diff --git a/tests/data/ladder.csv b/tests/data/ladder.csv new file mode 100644 index 0000000..92e3cea --- /dev/null +++ b/tests/data/ladder.csv @@ -0,0 +1,10 @@ +,,A,, +,,120,, +1,3,100,, +2,4,90,, +,,80,5,3 +,,B,, +,,C,, +,,30,, +4,6,20,, +,,D,, diff --git a/tests/data/ladder.xlsx b/tests/data/ladder.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..5280d699a58d892980b2d197e7e76c0bd27bfbdc GIT binary patch literal 6091 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 p{tQmQ- (6-3)^2 = 9 + # distance(B, C) = 8 instead of 4 -> (8-4)^2 = 16 + # distance(A, C) = 10 instead of 5 -> (10-5)^2 = 25 + # total cost = 9 + 16 + 25 = 50 + cost = solver.distances_cost(suboptimal_solution) + self.assertAlmostEqual(cost, 50.0, places=3) + + def test_offsets_cost_optimal_solution(self): + solver = OptimizationSolver(['A', 'B', 'C']) + solver.add_distance('A', 'B', 10) + solver.add_offset('C', 'A', 'B', 5, 2) + optimal_solution = numpy.array([[0, 0], [10, 0], [5, 2]]).flatten() + cost = solver.offsets_cost(optimal_solution) + self.assertAlmostEqual(cost, 0.0, places=3) + + def test_offsets_cost_suboptimal_solution(self): + solver = OptimizationSolver(['A', 'B', 'C']) + solver.add_distance('A', 'B', 10) + solver.add_offset('C', 'A', 'B', 5, 2) + suboptimal_solution = numpy.array([[0, 0], [10, 0], [5, 4]]).flatten() + cost = solver.offsets_cost(suboptimal_solution) + # The expected position of C is (5, 2), but the actual position is (5, 4), + # so the cost should be (distance((5, 4), (5, 2)))^2 = 2^2 = 4 + self.assertAlmostEqual(cost, 4.0, places=3) + + + def test_simple_triangle_only_distances_ccw(self): + solver = OptimizationSolver(['A', 'B', 'C']) + solver.add_distance('A', 'B', 3) + solver.add_distance('B', 'C', 4) + solver.add_distance('A', 'C', 5) + + points = solver.solve() + + numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 3, decimal=2) + numpy.testing.assert_almost_equal(distance(points['B'], points['C']), 4, decimal=2) + numpy.testing.assert_almost_equal(distance(points['A'], points['C']), 5, decimal=2) + + triangle = numpy.array([points['A'], points['B'], points['C']]) + self.assertAlmostEqual(poly_area(triangle), 6.0, places=3) + + + def test_simple_triangle_only_distances_cw(self): + solver = OptimizationSolver(['A', 'C', 'B']) + solver.add_distance('A', 'C', 3) + solver.add_distance('B', 'C', 4) + solver.add_distance('A', 'B', 5) + + points = solver.solve() + + numpy.testing.assert_almost_equal(distance(points['A'], points['C']), 3, decimal=2) + numpy.testing.assert_almost_equal(distance(points['B'], points['C']), 4, decimal=2) + numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 5, decimal=2) + + triangle = numpy.array([points['A'], points['C'], points['B']]) + self.assertAlmostEqual(poly_area(triangle), -6.0, places=3) + + + def test_simple_triangle_with_offset(self): + solver = OptimizationSolver(['A', 'B', 'C']) + solver.add_distance('A', 'B', 10) + solver.add_offset('C', 'A', 'B', 5, 2) + + points = solver.solve() + + numpy.testing.assert_almost_equal(distance(points['A'], points['B']), 10, decimal=2) + numpy.testing.assert_almost_equal(points['C'], chainage_offset_to_xy(points['A'], points['B'], 5, 2), decimal=2) + + +unittest.main()