├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── documentation_request.yml
│ ├── feature_request.yml
│ └── question.yml
├── pull_request_template.md
└── workflows
│ ├── conda.yml
│ ├── documentation.yml
│ ├── linting_and_testing.yml
│ ├── publish_conda.yml
│ └── pypi.yml
├── .gitignore
├── .release-please-manifest.json
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── conda
├── bld.bat
├── build.sh
└── meta.yaml
├── dependencies.txt
├── docs
├── Dockerfile
├── Makefile
├── build_docs.sh
├── docker-compose.yml
├── examples
│ ├── README.rst
│ ├── plot_data_checks_on_fault.py
│ ├── plot_data_checks_on_structure.py
│ ├── plot_hamersley.py
│ └── plot_m2l_data_checks_on_datatype_geology.py
├── make.bat
├── requirements.txt
└── source
│ ├── API.rst
│ ├── _static
│ ├── HJSON_TEMPLATE.hjson
│ ├── images
│ │ ├── csv.png
│ │ ├── fault_attributes_table.png
│ │ ├── litho_attributes_table.png
│ │ └── ori_attributes_table.png
│ └── m2l_code_template.ipynb
│ ├── _templates
│ ├── custom-class-template.rst
│ ├── custom-module-template.rst
│ ├── custom.css
│ └── page.html
│ ├── conf.py
│ ├── index.rst
│ └── user_guide
│ ├── changing_colours.rst
│ ├── config_file.rst
│ ├── explanation.rst
│ ├── exporting.rst
│ ├── fault_offset.rst
│ ├── geomodelling.rst
│ ├── getting_started.rst
│ ├── index.rst
│ ├── installation.rst
│ ├── m2l_code_template.rst
│ ├── setup_jupyter.rst
│ ├── stratigraphic_order.rst
│ └── stratigraphic_thickness.rst
├── map2loop
├── __init__.py
├── _datasets
│ ├── clut_files
│ │ ├── QLD_clut.csv
│ │ ├── SA_clut.csv
│ │ └── WA_clut.csv
│ ├── config_files
│ │ ├── NSW.json
│ │ ├── QLD.json
│ │ ├── SA.json
│ │ ├── TAS.json
│ │ ├── VIC.json
│ │ └── WA.json
│ └── geodata_files
│ │ ├── hamersley
│ │ ├── dtm_rp.tif
│ │ ├── faults.geojson
│ │ ├── geology.geojson
│ │ └── structures.geojson
│ │ └── load_map2loop_data.py
├── aus_state_urls.py
├── config.py
├── data_checks.py
├── deformation_history.py
├── fault_orientation.py
├── interpolators.py
├── logging.py
├── m2l_enums.py
├── map2model_wrapper.py
├── mapdata.py
├── project.py
├── sampler.py
├── sorter.py
├── stratigraphic_column.py
├── thickness_calculator.py
├── throw_calculator.py
├── utils.py
└── version.py
├── pyproject.toml
├── release-please-config.json
├── setup.py
└── tests
├── __init__.py
├── data_checks
├── test_config.py
├── test_input_data_faults.py
├── test_input_data_fold.py
├── test_input_data_geology.py
└── test_input_data_structure.py
├── mapdata
├── test_mapdata_assign_random_colours_to_units.py
├── test_mapdata_dipdir.py
├── test_minimum_fault_length.py
├── test_set_get_recreate_bounding_box.py
└── test_set_get_working_projection.py
├── project
├── test_config_arguments.py
├── test_ignore_codes_setters_getters.py
├── test_plot_hamersley.py
└── test_thickness_calculations.py
├── sampler
├── geo_test.csv
├── test_SamplerSpacing.py
└── test_SamplerSpacing_featureId.py
├── test_import.py
├── thickness
├── InterpolatedStructure
│ └── test_interpolated_structure.py
├── StructurePoint
│ └── test_ThicknessStructuralPoint.py
└── ThicknessCalculatorAlpha
│ └── test_ThicknessCalculatorAlpha.py
└── utils
└── test_rgb_and_hex_functions.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text eol=lf
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: "🐛 Bug Report"
2 | description: "Report a bug or an issue in map2loop"
3 | title: "[Bug] - "
4 | labels: ["bug"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | ## 🐛 Bug Report
10 | Thanks for submitting a bug report to map2loop!
11 | Please use this template to report a bug. Please provide as much detail as possible to help us reproduce and fix the issue efficiently.
12 |
13 | - type: textarea
14 | id: version
15 | attributes:
16 | label: Version
17 | description: What version of map2loop and LoopProjectFile are you running? You can find this information by running `import map2loop` and `map2loop.__version__` in your python terminal or jupyter notebook.
18 | placeholder: "Enter map2loop and LoopProjectFile versions"
19 | validations:
20 | required: true
21 |
22 | - type: textarea
23 | id: bug_description
24 | attributes:
25 | label: "Bug Description"
26 | description: "Describe the bug you encountered. Include details on what you expected to happen and what actually happened."
27 | placeholder: "Enter a detailed description of the bug"
28 | validations:
29 | required: true
30 |
31 | - type: textarea
32 | id: steps_to_reproduce
33 | attributes:
34 | label: "Minimal reproducible example"
35 | description: "Provide a minimal reproducible example with the code necessary to reproduce the bug. For more guidance, visit: [How to create a minimal complete reproducible example](https://forum.access-hive.org.au/t/how-to-create-a-minimal-complete-reproducible-example/843)"
36 | placeholder: "Enter the steps to reproduce the bug"
37 | validations:
38 | required: false
39 |
40 | - type: textarea
41 | id: additional_context
42 | attributes:
43 | label: "Additional Context"
44 | description: "Provide any other context or information that may be helpful in understanding and fixing the bug."
45 | placeholder: "Enter any additional context"
46 | validations:
47 | required: false
48 |
49 | - type: input
50 | id: environment
51 | attributes:
52 | label: "Environment"
53 | description: "Specify the environment in which the bug occurred (e.g., operating system, browser, application version)."
54 | placeholder: "Enter the environment details"
55 | validations:
56 | required: false
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation_request.yml:
--------------------------------------------------------------------------------
1 | name: "📓 Documentation Request"
2 | description: "Help us improve map2loop documentation!"
3 | title: "[Documentation] - "
4 | labels: ["documentation"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | ## 📓 Documentation Request
10 | Please use this template to suggest an improvement or addition to map2loop documentation.
11 | Provide as much detail as possible to help us understand and implement your request efficiently
12 |
13 | - type: textarea
14 | id: doc_details
15 | attributes:
16 | label: "Documentation Details"
17 | description: "Describe the documentation you would like to see. Include details on why it is needed and how it should be structured."
18 | placeholder: "Enter a detailed description of the documentation"
19 | validations:
20 | required: true
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: "🚀 Feature Request"
2 | description: "Suggest a new feature or enhancement for map2loop"
3 | title: "[Feature Request] - "
4 | labels: ["enhancement", "feature request"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | ## 🚀 Feature Request
10 | Please use this template to submit your feature request. Provide as much detail as possible to help us understand and implement your request efficiently.
11 |
12 | - type: checkboxes
13 | id: input1
14 | attributes:
15 | label: "💻"
16 | description: |
17 | Check this if you would like to try and implement your vision in a PR.
18 | The map2loop team will help you go through the process
19 | options:
20 | - label: Would you like to work on this feature?
21 |
22 | - type: textarea
23 | id: feature_description
24 | attributes:
25 | label: "Feature Description"
26 | description: "Describe the feature you would like to see. Include details on why it is needed and how it should work."
27 | placeholder: "Enter a detailed description of the feature"
28 | validations:
29 | required: true
30 |
31 | - type: textarea
32 | id: version
33 | attributes:
34 | label: Version
35 | description: What version of map2loop and LoopProjectFile are you running that doesn't have this feature? You can find this information by running `import map2loop` and `map2loop.__version__` in your python terminal or jupyter notebook.
36 | placeholder: "Enter map2loop and LoopProjectFile versions"
37 | validations:
38 | required: true
39 |
40 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question.yml:
--------------------------------------------------------------------------------
1 | name: "💬 Question"
2 | description: "Ask a question about map2loop!"
3 | title: "[Question] - "
4 | labels: ["question"]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | ## 💬 Question
10 | Please use this template to ask a question about applying map2loop to your data.
11 | Provide as much detail as possible to help us understand and answer your question efficiently.
12 |
13 | - type: textarea
14 | id: question_details
15 | attributes:
16 | label: "Question Details"
17 | description: "Describe your question in detail. Include any context or background information that might be helpful."
18 | placeholder: "Enter the details of your question"
19 | validations:
20 | required: true
21 |
22 | - type: textarea
23 | id: relevant_code_snippets
24 | attributes:
25 | label: "Relevant code or data"
26 | description: "If applicable, provide any relevant code snippets or examples related to your question."
27 | placeholder: "Enter any relevant code snippets"
28 | validations:
29 | required: false
30 |
31 | - type: textarea
32 | id: additional_context
33 | attributes:
34 | label: "Additional Context"
35 | description: "Provide any other context or information that may be helpful in answering your question."
36 | placeholder: "Enter any additional context"
37 | validations:
38 | required: false
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 | 📝 Thanks for contributing to map2loop!
4 | Please describe the issue that this pull request addresses and summarize the changes you are implementing.
5 | Include relevant motivation and context, if appropriate.
6 | List any new dependencies that are required for this change.
7 |
8 | Fixes #(issue)
9 |
10 | ## Type of change
11 |
12 | - [ ] Documentation update
13 | - [ ] Bug fix (non-breaking change which fixes an issue)
14 | - [ ] New feature (non-breaking change which adds functionality)
15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
16 | - [ ] Test improvement
17 |
18 | ## How Has This Been Tested?
19 |
20 | Please describe any tests that you ran to verify your changes.
21 | Provide branch name so we can reproduce.
22 |
23 | ## Checklist:
24 |
25 | - [ ] This branch is up-to-date with master
26 | - [ ] All gh-action checks are passing
27 | - [ ] I have performed a self-review of my own code
28 | - [ ] My code follows the style guidelines of this project
29 | - [ ] I have commented my code, particularly in hard-to-understand areas
30 | - [ ] I have made corresponding changes to the documentation
31 | - [ ] I have added tests that prove my fix is effective or that my feature works
32 | - [ ] My tests run with pytest from the map2loop folder
33 | - [ ] New and existing tests pass locally with my changes
34 |
35 | ## Checklist continued (if PR includes changes to documentation)
36 | - [ ] I have built the documentation locally with make.bat
37 | - [ ] I have built this documentation in docker, following the docker configuration in map2loop/docs
38 | - [ ] I have checked my spelling and grammar
--------------------------------------------------------------------------------
/.github/workflows/conda.yml:
--------------------------------------------------------------------------------
1 | name: Build conda packages
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | build_wheels:
8 | name: Build wheels on
9 | runs-on: ${{ matrix.os }}
10 | strategy:
11 | fail-fast: false
12 | matrix:
13 | os: ${{ fromJSON(vars.BUILD_OS)}}
14 | python-version: ${{ fromJSON(vars.PYTHON_VERSIONS) }}
15 | steps:
16 | - uses: conda-incubator/setup-miniconda@v3
17 | with:
18 | auto-update-conda: true
19 | python-version: ${{ matrix.python-version }}
20 |
21 | - uses: actions/checkout@v4
22 | - name: update submodules
23 | run: |
24 | git submodule update --init --recursive
25 | - name: Conda build
26 | env:
27 | ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }}
28 | shell: bash -l {0}
29 | run: |
30 | conda config --env --add channels conda-forge
31 | conda config --env --add channels loop3d
32 | conda config --env --set channel_priority strict
33 | conda install -c conda-forge conda-build scikit-build-core numpy anaconda-client conda-libmamba-solver -y
34 | conda config --set solver libmamba
35 | conda build --output-folder conda conda --python ${{matrix.python-version}}
36 | anaconda upload --label main conda/*/*.tar.bz2
37 |
38 | - name: upload artifacts
39 | uses: actions/upload-artifact@v4
40 | with:
41 | name: conda-build-${{matrix.os}}-${{ matrix.python-version }}
42 | path: conda
--------------------------------------------------------------------------------
/.github/workflows/documentation.yml:
--------------------------------------------------------------------------------
1 | name: Build and Deploy Documentation
2 |
3 | on:
4 | workflow_dispatch:
5 |
6 | jobs:
7 | documentation-test:
8 | runs-on: ubuntu-24.04
9 | steps:
10 | - uses: actions/checkout@v4
11 | with:
12 | ref: ${{ github.ref }}
13 |
14 | - run: |
15 | cp CHANGELOG.md docs/source/CHANGELOG.md
16 | docker build . -t=docs -f docs/Dockerfile
17 | docker run -v $(pwd):/map2loop docs bash map2loop/docs/build_docs.sh
18 | - name: upload artifacts
19 | uses: actions/upload-artifact@v4
20 | with:
21 | name: docs
22 | path: docs/build/html
23 |
24 | documentation-deploy:
25 | runs-on: ubuntu-24.04
26 | if: GitHub.ref == 'refs/heads/master'
27 | steps:
28 | - uses: actions/checkout@v4
29 | - uses: actions/download-artifact@v4
30 | with:
31 | name: docs
32 | path: docs
33 | - name: ls
34 | run: |
35 | ls -l docs
36 | - name: Deploy 🚀
37 | uses: JamesIves/github-pages-deploy-action@v4
38 | with:
39 | branch: gh-pages # The branch the action should deploy to.
40 | folder: docs # The folder the action should deploy.
41 |
--------------------------------------------------------------------------------
/.github/workflows/linting_and_testing.yml:
--------------------------------------------------------------------------------
1 | name: Linting and Testing
2 |
3 | on:
4 | [push]
5 |
6 | jobs:
7 | linting:
8 | name: Linting
9 | runs-on: ubuntu-24.04
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Install dependencies
13 | run: |
14 | python -m pip install --upgrade pip
15 | pip install black ruff
16 | ruff check . --fix
17 |
18 | - uses: stefanzweifel/git-auto-commit-action@v5
19 | with:
20 | commit_message: "style: style fixes by ruff and autoformatting by black"
21 |
22 |
23 | testing:
24 | name: Testing
25 | runs-on: ubuntu-24.04
26 | steps:
27 | - uses: actions/checkout@v4
28 | - name: Install GDAL
29 | run: |
30 | sudo add-apt-repository ppa:ubuntugis/ubuntugis-unstable
31 | sudo apt-get update
32 | sudo apt-get install -y libgdal-dev gdal-bin
33 |
34 | - name: Install dependencies
35 | run: |
36 | conda update -n base -c defaults conda -y
37 | conda install -n base conda-libmamba-solver -c conda-forge -y
38 | conda install -c conda-forge gdal -y
39 | conda install -c conda-forge -c loop3d --file dependencies.txt -y
40 | conda install pytest -y
41 |
42 | - name: Install map2loop
43 | run: |
44 | python -m pip install .
45 |
46 | - name: Run tests
47 | run: |
48 | pytest
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/publish_conda.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - master
5 | workflow_dispatch:
6 |
7 | jobs:
8 | release-please:
9 | runs-on: ubuntu-24.04
10 | steps:
11 | - uses: GoogleCloudPlatform/release-please-action@v4
12 | id: release
13 | with:
14 | release-type: python
15 | package-name: map2loop
16 | - name: Debug release_created output
17 | run: |
18 | echo "Release created: ${{ steps.release.outputs.release_created }}"
19 | outputs:
20 | release_created: ${{ steps.release.outputs.release_created }}
21 |
22 | pypi:
23 | runs-on: ubuntu-24.04
24 | needs: release-please
25 | if: ${{ needs.release-please.outputs.release_created }}
26 | steps:
27 | - name: Trigger build for pypi and upload
28 | run: |
29 | curl -X POST \
30 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
31 | -H "Accept: application/vnd.github.v3+json" \
32 | https://api.github.com/repos/Loop3d/map2loop/actions/workflows/pypi.yml/dispatches \
33 | -d '{"ref":"master"}'
34 | - name: Trigger build for conda and upload
35 | run: |
36 | curl -X POST \
37 | -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
38 | -H "Accept: application/vnd.github.v3+json" \
39 | https://api.github.com/repos/Loop3d/map2loop/actions/workflows/conda.yml/dispatches \
40 | -d '{"ref":"master"}'
41 |
--------------------------------------------------------------------------------
/.github/workflows/pypi.yml:
--------------------------------------------------------------------------------
1 | name: Deploy to PYPI
2 |
3 | on:
4 | workflow_dispatch:
5 | jobs:
6 |
7 | sdist:
8 | name: Build sdist
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - uses: actions/setup-python@v5
13 | - name: Install dependencies
14 | run: |
15 | python -m pip install --upgrade pip
16 | pip install build
17 | - name: Build sdist
18 | run: python -m build --sdist
19 | - uses: actions/upload-artifact@v4
20 | with:
21 | name: dist
22 | path: ./dist/*.tar.gz
23 |
24 | publish:
25 | name: Publish wheels to pypi
26 | runs-on: ubuntu-latest
27 | permissions:
28 | # IMPORTANT: this permission is mandatory for trusted publishing
29 | id-token: write
30 | needs: sdist
31 | steps:
32 | - uses: actions/download-artifact@v4
33 | with:
34 | name: dist
35 | path: dist
36 | - name: copy to wheelhouse
37 | run: |
38 | # cp -r wheelhouse/*/*.whl dist
39 | - uses: pypa/gh-action-pypi-publish@release/v1
40 | with:
41 | skip-existing: true
42 | verbose: true
43 | packages-dir: dist/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | ### M2L stuff ###
3 | /model-test*
4 | .ipynb_checkpoints
5 | notebooks/Testing.ipynb
6 | .vim/
7 | .vscode/
8 | _version.py
9 | 3/
10 |
11 | # Created by https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,c++,cmake
12 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,visualstudiocode,c++,cmake
13 |
14 | ### C++ ###
15 | # Prerequisites
16 | *.d
17 |
18 | # Compiled Object files
19 | *.slo
20 | *.lo
21 | *.o
22 | *.obj
23 |
24 | # Precompiled Headers
25 | *.gch
26 | *.pch
27 |
28 | # Compiled Dynamic libraries
29 | *.so
30 | *.dylib
31 | *.dll
32 |
33 | # Fortran module files
34 | *.mod
35 | *.smod
36 |
37 | # Compiled Static libraries
38 | *.lai
39 | *.la
40 | *.a
41 | *.lib
42 |
43 | # Executables
44 | *.exe
45 | *.out
46 | *.app
47 |
48 | ### CMake ###
49 | CMakeLists.txt.user
50 | CMakeCache.txt
51 | CMakeFiles
52 | CMakeScripts
53 | Testing
54 | cmake_install.cmake
55 | install_manifest.txt
56 | compile_commands.json
57 | CTestTestfile.cmake
58 | _deps
59 |
60 | ### CMake Patch ###
61 | # External projects
62 | *-prefix/
63 |
64 | ### Python ###
65 | # Byte-compiled / optimized / DLL files
66 | __pycache__/
67 | *.py[cod]
68 | *$py.class
69 |
70 | # C extensions
71 |
72 | # Distribution / packaging
73 | .Python
74 | build/
75 | develop-eggs/
76 | dist/
77 | downloads/
78 | eggs/
79 | .eggs/
80 | lib/
81 | lib64/
82 | parts/
83 | sdist/
84 | var/
85 | wheels/
86 | pip-wheel-metadata/
87 | share/python-wheels/
88 | *.egg-info/
89 | .installed.cfg
90 | *.egg
91 | MANIFEST
92 |
93 | # PyInstaller
94 | # Usually these files are written by a python script from a template
95 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
96 | *.manifest
97 | *.spec
98 |
99 | # Installer logs
100 | pip-log.txt
101 | pip-delete-this-directory.txt
102 |
103 | # Unit test / coverage reports
104 | htmlcov/
105 | .tox/
106 | .nox/
107 | .coverage
108 | .coverage.*
109 | .cache
110 | nosetests.xml
111 | coverage.xml
112 | *.cover
113 | *.py,cover
114 | .hypothesis/
115 | .pytest_cache/
116 |
117 | # Translations
118 | *.mo
119 | *.pot
120 |
121 | # Django stuff:
122 | *.log
123 | local_settings.py
124 | db.sqlite3
125 | db.sqlite3-journal
126 |
127 | # Flask stuff:
128 | instance/
129 | .webassets-cache
130 |
131 | # Scrapy stuff:
132 | .scrapy
133 |
134 | # Sphinx documentation
135 | docs/_build/
136 |
137 | # PyBuilder
138 | target/
139 |
140 | # Jupyter Notebook
141 | .ipynb_checkpoints
142 |
143 | # IPython
144 | profile_default/
145 | ipython_config.py
146 |
147 | # pyenv
148 | .python-version
149 |
150 | # pipenv
151 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
152 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
153 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
154 | # install all needed dependencies.
155 | #Pipfile.lock
156 |
157 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
158 | __pypackages__/
159 |
160 | # Celery stuff
161 | celerybeat-schedule
162 | celerybeat.pid
163 |
164 | # SageMath parsed files
165 | *.sage.py
166 |
167 | # Environments
168 | .env
169 | .venv
170 | env/
171 | venv/
172 | ENV/
173 | env.bak/
174 | venv.bak/
175 |
176 | # Spyder project settings
177 | .spyderproject
178 | .spyproject
179 |
180 | # Rope project settings
181 | .ropeproject
182 |
183 | # mkdocs documentation
184 | /site
185 |
186 | # mypy
187 | .mypy_cache/
188 | .dmypy.json
189 | dmypy.json
190 |
191 | # Pyre type checker
192 | .pyre/
193 |
194 | # pytype static type analyzer
195 | .pytype/
196 |
197 | ### VisualStudioCode ###
198 | .vscode/*
199 | !.vscode/settings.json
200 | !.vscode/tasks.json
201 | !.vscode/launch.json
202 | !.vscode/extensions.json
203 | *.code-workspace
204 |
205 | ### VisualStudioCode Patch ###
206 | # Ignore all local history of files
207 | .history
208 | .vscode/
209 |
210 | docs/build
211 | docs/source/_autosummary/*
212 | # End of https://www.toptal.com/developers/gitignore/api/python,visualstudiocode,c++,cmake
213 | #ignore automatically generated doc files
214 | docs/source/_autosummary
215 | docs/source/_auto_examples/*
216 | docs/source/_auto_examples
217 | examples/*_tmp
218 | *.loop3d
219 | m2l_data_tmp/*
220 | docs/examples/m2l_data_tmp/*
221 | docs/source/sg_execution_times.rst
222 | docs/source/auto_examples/*
223 | tests/m2l_data_tmp/*
224 |
225 | tests/project/m2l_data_tmp/*
226 | tests/project/m2l_data_tmp/*
227 | tests/thickness/m2l_data_tmp/*
228 | docs/m2l_data_tmp/*
229 | tests/thickness/StructurePoint/m2l_data_tmp/*
230 |
231 |
--------------------------------------------------------------------------------
/.release-please-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | ".": "3.2.2"
3 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020-2021 The Loop Project
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include map2loop/_datasets/clut_files/*.csv
2 | include map2loop/_datasets/config_files/*.json
3 | include map2loop/_datasets/geodata_files/hamersley/*
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://doi.org/10.5194/gmd-14-5063-2021)
3 | 
4 | 
5 | 
6 | [](https://github.com/Loop3D/map2loop/actions/workflows/linting_and_testing.yml)
7 | [](https://github.com/Loop3D/map2loop/actions/workflows/documentation.yml)
8 |
9 |
10 | # Map2Loop 3.2
11 |
12 | Generate 3D geological model inputs from geological maps — a high-level implementation and extension of the original map2loop code developed by Prof. Mark Jessell at UWA. To see an example interactive model built with map2loop and LoopStructural, follow this link:
13 |
14 | 3D Model from the Hamersley region, Western Australia
15 |
16 | ## Install
17 |
18 | #### Option 1: Install with Anaconda
19 |
20 | This is the simplest and recommended installation process, with:
21 |
22 | ```bash
23 | conda install -c loop3d -c conda-forge map2loop
24 | ```
25 |
26 | #### Option 2: Install with pip
27 | Installation with pip will require that GDAL is installed on your system prior to map2loop installation.
28 | This is because GDAL cannot be installed via pip (at least not with one line of code), and the GDAL installation process will vary depending on your OS.
29 |
30 | For more information on installing gdal, see GDAL's Pypi page.
31 |
32 | Once GDAL is available on your system, map2loop can be installed with:
33 | ```bash
34 | pip install map2loop
35 | ```
36 |
37 | #### Option 3: From source
38 |
39 | ```bash
40 | git clone https://github.com/Loop3D/map2loop.git
41 |
42 | cd map2loop
43 |
44 | conda install gdal
45 |
46 | conda install -c loop3d -c conda-forge --file dependencies.txt
47 |
48 | pip install .
49 | ```
50 |
51 | #### Option 4: From source & developer mode:
52 | ```bash
53 | git clone https://github.com/Loop3D/map2loop.git
54 |
55 | cd map2loop
56 |
57 | conda install gdal
58 |
59 | conda install -c loop3d -c conda-forge --file dependencies.txt
60 |
61 | pip install -e .
62 | ```
63 |
64 | ### Documentation
65 |
66 | Map2loop's documentation is available here
67 |
68 |
69 | ## Usage
70 |
71 | Our notebooks cover use cases in more detail, but here is an example of processing Loop's South Australia remote geospatial data in just 20 lines of Python.
72 |
73 | First, let's import map2loop and define a bounding box. You can use GIS software to find one or use [Loop's Graphical User Interface](https://loop3d.github.io/downloads.html) for the best experience and complete toolset. Remember what projection your coordinates are in!
74 |
75 | ```python
76 | from map2loop.project import Project
77 | from map2loop.m2l_enums import VerboseLevel
78 |
79 | # Note that this region is defined in the EPSG 28354 projection and
80 | # x and y represent easting and northing respectively
81 | bbox_3d = {
82 | 'minx': 250805.1529856466,
83 | 'miny': 6405084.328058686,
84 | 'maxx': 336682.921539395,
85 | 'maxy': 6458336.085975628,
86 | 'base': -3200,
87 | 'top': 1200
88 | }
89 | ```
90 |
91 | Then, specify: the state, directory for the output, the bounding box and projection from above - and hit go! That's it.
92 |
93 | ```python
94 | proj = Project(use_australian_state_data = "SA",
95 | working_projection = 'EPSG:28354',
96 | bounding_box = bbox_3d,
97 | loop_project_filename = "output.loop3d"
98 | )
99 |
100 | proj.run_all()
101 | ```
102 |
103 | This is a minimal example and a small part of Loop.
104 |
105 | Our _documentation and other resources outline how to extend map2loop and port to the LoopStructural modelling engine. We are working to incorporate geophysical tools and best provide visualisation and workflow consolidation in the GUI._
106 |
107 | _Loop is led by Laurent Ailleres (Monash University) with a team of Work Package leaders from:_
108 |
109 | - _Monash University: Roy Thomson, Lachlan Grose and Robin Armit_
110 | - _University of Western Australia: Mark Jessell, Jeremie Giraud, Mark Lindsay and Guillaume Pirot_
111 | - _Geological Survey of Canada: Boyan Brodaric and Eric de Kemp_
112 |
113 | ---
114 |
115 | ### Known Issues and FAQs
116 |
117 | - Developing with docker on Windows means you won't have GPU passthrough and can’t use a discrete graphics card in the container even if you have one.
118 | - If Jupyter links require a token or password, it may mean port 8888 is already in use. To fix, either make docker map to another port on the host ie -p 8889:8888 or stop any other instances on 8888.
119 |
120 | ### Links
121 |
122 | [https://loop3d.github.io/](https://loop3d.github.io/)
123 |
124 | [https://github.com/Loop3D/LoopStructural](https://github.com/Loop3D/LoopStructural)
125 |
--------------------------------------------------------------------------------
/conda/bld.bat:
--------------------------------------------------------------------------------
1 | mkdir %SP_DIR%\map2loop
2 | copy %RECIPE_DIR%\..\LICENSE %SP_DIR%\map2loop\
3 | copy %RECIPE_DIR%\..\README.md %SP_DIR%\map2loop\
4 | copy %RECIPE_DIR%\..\dependencies.txt %SP_DIR%\map2loop\
5 | %PYTHON% -m pip install .
--------------------------------------------------------------------------------
/conda/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | mkdir -p $SP_DIR/map2loop
3 | cp $RECIPE_DIR/../dependencies.txt $SP_DIR/map2loop/
4 | cp $RECIPE_DIR/../LICENSE $SP_DIR/map2loop/
5 | cp $RECIPE_DIR/../README.md $SP_DIR/map2loop/
6 | $PYTHON -m pip install .
--------------------------------------------------------------------------------
/conda/meta.yaml:
--------------------------------------------------------------------------------
1 | {% set name = "map2loop" %}
2 |
3 | package:
4 | name: "{{ name|lower }}"
5 | version: "{{ environ.get('GIT_DESCRIBE_TAG', '') }}"
6 |
7 | source:
8 | git_url: https://github.com/Loop3D/map2loop
9 |
10 | build:
11 | number: 0
12 |
13 | requirements:
14 | host:
15 | - pip
16 | - python
17 | - setuptools
18 | run:
19 | - loopprojectfile ==0.2.2
20 | - gdal
21 | - python
22 | - numpy
23 | - scipy
24 | - geopandas
25 | - shapely
26 | - networkx
27 | - owslib
28 | - map2model
29 | - beartype
30 | - pytest
31 | - scikit-learn
32 |
33 | about:
34 | home: "https://github.com/Loop3D/map2loop"
35 | license: MIT
36 | license_family: MIT
37 | license_file: ../LICENSE
38 | summary: "Generate 3D model data using 2D maps."
39 |
40 | extra:
41 | recipe-maintainers:
42 | - lachlangrose
43 |
44 | channels:
45 | - loop3d
46 | - conda-forge
--------------------------------------------------------------------------------
/dependencies.txt:
--------------------------------------------------------------------------------
1 | numpy
2 | scipy
3 | geopandas
4 | shapely
5 | networkx
6 | owslib
7 | map2model
8 | loopprojectfile==0.2.2
9 | beartype
10 | pytest
11 | scikit-learn
--------------------------------------------------------------------------------
/docs/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM continuumio/miniconda3
2 | LABEL maintainer="lachlan.grose@monash.edu"
3 | #This docker image has been adapted from the lavavu dockerfile
4 | # install things
5 |
6 | RUN apt-get update -qq && \
7 | DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
8 | gcc \
9 | g++ \
10 | libc-dev \
11 | make\
12 | libgl1\
13 | libtinfo5\
14 | libtiff6\
15 | libgl1-mesa-glx\
16 | libarchive13
17 |
18 | COPY . /map2loop
19 |
20 | WORKDIR /map2loop
21 |
22 | COPY dependencies.txt dependencies.txt
23 | COPY dependencies.txt dependenciesdocs.txt
24 |
25 | RUN cat ./docs/requirements.txt >> dependenciesdocs.txt
26 | RUN conda install -c conda-forge -c loop3d --file dependenciesdocs.txt -y
27 | RUN conda install gdal -y
28 |
29 | RUN pip install .
30 |
31 | WORKDIR /
32 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = source
9 | BUILDDIR = build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/docs/build_docs.sh:
--------------------------------------------------------------------------------
1 | pip install ./map2loop
2 | export DOCUMENTATION_TEST=True
3 | make -C ./map2loop/docs html
--------------------------------------------------------------------------------
/docs/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | map2loop:
3 | build:
4 | dockerfile: docs/Dockerfile
5 | context: ../
6 | volumes:
7 | - ../:/map2loop
8 | - ../../LoopStructural:/LoopStructural
9 | tty: true
10 |
11 | # command: sh map2loop/docs/build_docs.sh
--------------------------------------------------------------------------------
/docs/examples/README.rst:
--------------------------------------------------------------------------------
1 | Examples
2 | ========
--------------------------------------------------------------------------------
/docs/examples/plot_data_checks_on_structure.py:
--------------------------------------------------------------------------------
1 | # %%
2 | import geopandas as gpd
3 | from map2loop.mapdata import MapData
4 | from map2loop import data_checks
5 | import shapely.geometry
6 |
7 |
8 | # Mock Datatype Enum
9 | class Datatype:
10 | GEOLOGY = 0
11 | STRUCTURE = 1
12 |
13 |
14 | # Mock Config class
15 | class MockConfig:
16 | def __init__(self):
17 | self.structure_config = {
18 | "dipdir_column": "DIPDIR",
19 | "dip_column": "DIP",
20 | "description_column": "DESCRIPTION",
21 | "overturned_column": "OVERTURNED",
22 | "objectid_column": "ID",
23 | }
24 |
25 |
26 | # Mock data for the structure dataset
27 | valid_structure_data = {
28 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
29 | "DIPDIR": [45.0, 135.0],
30 | "DIP": [30.0, 45.0],
31 | "DESCRIPTION": ["Description1", "Description2"],
32 | "OVERTURNED": ["Yes", "No"],
33 | "ID": [1, 2],
34 | }
35 |
36 | # Create a GeoDataFrame for valid structure data
37 | valid_structure_gdf = gpd.GeoDataFrame(valid_structure_data, crs="EPSG:4326")
38 |
39 |
40 | # Instantiate the MapData class with the mock config and data
41 | map_data = MapData()
42 | map_data.config = MockConfig()
43 |
44 | # Test with valid structure data
45 | map_data.raw_data = [None] * len(Datatype.__dict__)
46 | map_data.raw_data[Datatype.STRUCTURE] = valid_structure_gdf
47 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
48 | print("Test 1 - Valid Data:")
49 | print(f"Validity Check: {validity_check}, Message: {message}")
50 |
51 | # %%
52 | # Mock data with invalid geometry
53 | invalid_geometry_structure_data = valid_structure_data.copy()
54 | invalid_geometry_structure_data["geometry"] = [
55 | shapely.geometry.Point(0, 0),
56 | shapely.geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 1), (0, 0)]), # Invalid geometry
57 | ]
58 | invalid_geometry_structure_gdf = gpd.GeoDataFrame(invalid_geometry_structure_data, crs="EPSG:4326")
59 |
60 |
61 | # Test with invalid geometry
62 | map_data.raw_data[Datatype.STRUCTURE] = invalid_geometry_structure_gdf
63 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
64 | print("\nTest 3 - Invalid Geometry:")
65 | print(f"Validity Check: {validity_check}, Message: {message}")
66 |
67 | # %%
68 | # Mock data with missing required columns
69 | missing_column_structure_data = valid_structure_data.copy()
70 | del missing_column_structure_data["DIPDIR"]
71 | missing_column_structure_gdf = gpd.GeoDataFrame(missing_column_structure_data, crs="EPSG:4326")
72 |
73 | # Test with missing required column
74 | map_data.raw_data[Datatype.STRUCTURE] = missing_column_structure_gdf
75 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
76 | print("\nTest 2 - Missing Required Column:")
77 | print(f"Validity Check: {validity_check}, Message: {message}")
78 |
79 | # %%
80 | # Mock data for the structure dataset
81 | invalid_structure_data = {
82 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
83 | "DIPDIR": ["A", "B"],
84 | "DIP": [30.0, 45.0],
85 | "DESCRIPTION": ["Description1", "Description2"],
86 | "OVERTURNED": ["Yes", "No"],
87 | "ID": [1, 2],
88 | }
89 |
90 | map_data.raw_data[Datatype.STRUCTURE] = invalid_structure_data
91 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
92 | print(f"Validity Check: {validity_check}, Message: {message}")
93 |
94 | # %%
95 | # Mock data for the structure dataset
96 | invalid_structure_data = gpd.GeoDataFrame(
97 | {
98 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
99 | "DIPDIR": ["A", "B"],
100 | "DIP": [30.0, 45.0],
101 | "DESCRIPTION": ["Description1", "Description2"],
102 | "OVERTURNED": ["Yes", "No"],
103 | "ID": [1, 2],
104 | }
105 | )
106 |
107 | map_data.raw_data[Datatype.STRUCTURE] = invalid_structure_data
108 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
109 | print(f"Validity Check: {validity_check}, Message: {message}")
110 |
111 | # %%
112 | # Mock data for the structure dataset
113 | invalid_structure_data = gpd.GeoDataFrame(
114 | {
115 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
116 | "DIPDIR": [None, 3],
117 | "DIP": [30.0, 45.0],
118 | "DESCRIPTION": ["Description1", "Description2"],
119 | "OVERTURNED": ["Yes", "No"],
120 | "ID": [1, 2],
121 | }
122 | )
123 |
124 | map_data.raw_data[Datatype.STRUCTURE] = invalid_structure_data
125 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
126 | print(f"Validity Check: {validity_check}, Message: {message}")
127 |
128 | # %%
129 | # Mock data for the structure dataset
130 | invalid_structure_data = gpd.GeoDataFrame(
131 | {
132 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
133 | "DIPDIR": [5, 3],
134 | "DIP": [120.0, 45.0],
135 | "DESCRIPTION": ["Description1", "Description2"],
136 | "OVERTURNED": ["Yes", "No"],
137 | "ID": [1, 2],
138 | }
139 | )
140 |
141 | map_data.raw_data[Datatype.STRUCTURE] = invalid_structure_data
142 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
143 | print(f"Validity Check: {validity_check}, Message: {message}")
144 |
145 | # %%
146 | # Mock data for the structure dataset
147 | invalid_structure_data = gpd.GeoDataFrame(
148 | {
149 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
150 | "DIPDIR": [5, 3],
151 | "DIP": [90, 45.0],
152 | "DESCRIPTION": [None, "Description2"],
153 | "OVERTURNED": ["Yes", "No"],
154 | "ID": [1, 2],
155 | }
156 | )
157 |
158 | map_data.raw_data[Datatype.STRUCTURE] = invalid_structure_data
159 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
160 | print(f"Validity Check: {validity_check}, Message: {message}")
161 |
162 | # %%
163 | # Mock data for the structure dataset
164 | invalid_structure_data = gpd.GeoDataFrame(
165 | {
166 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
167 | "DIPDIR": [5, 3],
168 | "DIP": [90, 45.0],
169 | "DESCRIPTION": [None, "Description2"],
170 | "OVERTURNED": ["Yes", "No"],
171 | "ID": [1, 1],
172 | }
173 | )
174 |
175 | map_data.raw_data[Datatype.STRUCTURE] = invalid_structure_data
176 | validity_check, message = data_checks.check_structure_fields_validity(map_data)
177 | print(f"Validity Check: {validity_check}, Message: {message}")
178 |
179 | # %%
180 |
--------------------------------------------------------------------------------
/docs/examples/plot_hamersley.py:
--------------------------------------------------------------------------------
1 | """
2 | ============================
3 | Hamersley, Western Australia
4 | ============================
5 | """
6 |
7 | from map2loop.project import Project
8 | from map2loop.m2l_enums import VerboseLevel, Datatype
9 | from map2loop.sorter import SorterAlpha
10 | from map2loop.sampler import SamplerSpacing
11 |
12 | ####################################################################
13 | # Set the region of interest for the project
14 | # -------------------------------------------
15 | # Define the bounding box for the ROI
16 |
17 | bbox_3d = {
18 | "minx": 515687.31005864,
19 | "miny": 7493446.76593407,
20 | "maxx": 562666.860106543,
21 | "maxy": 7521273.57407786,
22 | "base": -3200,
23 | "top": 3000,
24 | }
25 |
26 | # Specify minimum details (which Australian state, projection and bounding box
27 | # and output file)
28 | loop_project_filename = "wa_output.loop3d"
29 | proj = Project(
30 | use_australian_state_data="WA",
31 | working_projection="EPSG:28350",
32 | bounding_box=bbox_3d,
33 | verbose_level=VerboseLevel.NONE,
34 | loop_project_filename=loop_project_filename,
35 | overwrite_loopprojectfile=True,
36 | )
37 |
38 | # Set the distance between sample points for arial and linestring geometry
39 | proj.set_sampler(Datatype.GEOLOGY, SamplerSpacing(200.0))
40 | proj.set_sampler(Datatype.FAULT, SamplerSpacing(200.0))
41 |
42 | # Choose which stratigraphic sorter to use or run_all with "take_best" flag to run them all
43 | proj.set_sorter(SorterAlpha())
44 | # proj.set_sorter(SorterAgeBased())
45 | # proj.set_sorter(SorterUseHint())
46 | # proj.set_sorter(SorterUseNetworkx())
47 | # proj.set_sorter(SorterMaximiseContacts())
48 | # proj.set_sorter(SorterObservationProjections())
49 | proj.run_all(take_best=True)
50 |
51 | ####################################################################
52 | # Visualise the map2loop results
53 | # -------------------------------------------
54 |
55 | proj.map_data.basal_contacts.plot()
56 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=source
11 | set BUILDDIR=build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/requirements.txt:
--------------------------------------------------------------------------------
1 | sphinx
2 | sphinx-gallery
3 | pydata-sphinx-theme
4 | myst-parser
5 | sphinxcontrib-bibtex
6 | poppler
7 | LoopStructural
--------------------------------------------------------------------------------
/docs/source/API.rst:
--------------------------------------------------------------------------------
1 | API
2 | ---
3 |
4 | .. autosummary::
5 | :caption: API
6 | :toctree: _autosummary
7 | :template: custom-module-template.rst
8 | :recursive:
9 |
10 | map2loop
11 |
--------------------------------------------------------------------------------
/docs/source/_static/HJSON_TEMPLATE.hjson:
--------------------------------------------------------------------------------
1 | #This is a template for a map2loop HJSON file, using the LEGACY code variable names (the left most 'term').
2 | #The information after the hash on the right is a description of the required field. You can delete this from your own file if you'd like.
3 | #You'll need to change the attribute name in the second quotation to match your shapefile attribute names
4 | # (e.g. change "INSERT_DIP" to your attribute name). Ensure that the attribute name is between '' or "".
5 |
6 | #Any lines with a *opt in the description string, means that they are optional. You can just leave the attribute field blank
7 | # (e.g. "g": '', )
8 |
9 | #For more information on the variables and map2loop requirements please see the documentation.
10 |
11 | {
12 | #ORIENTATION SHAPEFILE ATTRIBUTES
13 | "d": "INSERT_DIP", #attribute containing dip information
14 | "dd": "INSERT_DIP_DIRECTION", #attribute containing dip direction information
15 | "otype": 'dip direction', #Set the measurement convention used (either 'strike' or 'dip direction')
16 | "sf": 'INSERT_STRUCTURE_DESCRIPTION', #*opt attribute containing type of structure (eg. S0, S1)
17 | "bedding": 'INSERT_BEDDING_TEXT', #*opt text defining bedding measurements in the "sf" field (eg "Bedding" or "S0")
18 | "bo": 'INSERT_FOLIATION_DESCRIPTION', #*opt attribute containing type of foliation
19 | "btype": 'OVERTURNED_BEDDING_TEXT', #*opt text defining overturned bedding measurements in the "bo" field (eg. 'overturned')
20 |
21 | #LITHOLOGY SHAPEFILE ATTRIBUTES
22 | "c": 'INSERT_UNIT_NAME', #attribute containing stratigraphic unit name (most specific)
23 | "u": 'INSERT_ALT_UNIT_NAME', #attribute containing alternative stratigraphic unit name (eg unit code). Can be the same as 'c'
24 | "g": 'INSERT_GROUP', #*opt attribute containing stratigraphic group
25 | "g2": 'INSERT_SUPERGROUP', #*opt attribute containing stratigraphic supergroup (most coarse classification)
26 | "ds": 'INSERT_DESCRIPTION', #*opt general description field
27 | "r1": 'INSERT_ROCKTYPE', #*opt attribute containing extra lithology information (can indicate intrusions)
28 | "r2": 'INSERT_ROCKTYPE2', #*opt attribute containing secondary rocktype information
29 | "sill": 'INSERT_SILL_TEXT', #*opt text defining a sill in the "ds" field (eg 'sill')
30 | "intrusive": 'INSERT_INTRUSIVE_TEXT', #*opt text defining an intrusion in the "r1" field (eg 'intrusion')
31 | "volcanic": 'INSERT_VOLCANIC_TEXT', #*opt text defining volcanics in the "ds" field (eg 'volcanic')
32 | "min": 'INSERT_MIN_AGE', #*opt attribute containing minimum unit age
33 | "max": 'INSERT_MAX_AGE', #*opt attribute containing maximum unit age
34 |
35 | #LINEAR FEATURES SHAPEFILE ATTRIBUTES
36 | "f": 'INSERT_STRUCT_TYPE', #attribute containing linear structure type (e.g. fault)
37 | "fault": 'fault', #text defining faults in the "f" field (eg. 'fault')
38 | "fdip": 'INSERT_FAULT_DIP', #*opt attribute containing numeric fault dip value (defaults to fdipnull)
39 | "fdipnull": '0', #Default fault dip value, if 'fdip' field is empty
40 | "fdipdir": 'INSERT_FAULT_DIP_DIRECTION', #*opt attribute containing the fault dip direction (defaults to -999)
41 | "fdipdir_flag": 'num', #*opt specifies whether fdipdir is "num":numeric or other ("alpha")
42 | "fdipest": 'INSERT_DIP_EST_TEXT', #*opt field for text fault dip estimate value (defaults to none)
43 | "fdipest_vals": 'INSERT_DIP_EST_TERMS', #*opt text used to estimate dip in increasing steepness, in "fdipest" field
44 | "n": 'INSERT_FAULT_NAME', #*opt attribute containing the fault name
45 |
46 | #GENERAL IDS
47 | "o": 'INSERT_OBJ_ID', #*opt attribute containing unique object id (used in polygon and lineString shapefiles
48 | "gi": 'INSERT_ORI_ID', #*opt attribute containing unique id of structural points
49 | }
50 |
--------------------------------------------------------------------------------
/docs/source/_static/images/csv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loop3D/map2loop/1af43e4621a837db02252c05ff8cee0d5d8b3dd0/docs/source/_static/images/csv.png
--------------------------------------------------------------------------------
/docs/source/_static/images/fault_attributes_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loop3D/map2loop/1af43e4621a837db02252c05ff8cee0d5d8b3dd0/docs/source/_static/images/fault_attributes_table.png
--------------------------------------------------------------------------------
/docs/source/_static/images/litho_attributes_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loop3D/map2loop/1af43e4621a837db02252c05ff8cee0d5d8b3dd0/docs/source/_static/images/litho_attributes_table.png
--------------------------------------------------------------------------------
/docs/source/_static/images/ori_attributes_table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loop3D/map2loop/1af43e4621a837db02252c05ff8cee0d5d8b3dd0/docs/source/_static/images/ori_attributes_table.png
--------------------------------------------------------------------------------
/docs/source/_templates/custom-class-template.rst:
--------------------------------------------------------------------------------
1 | {{ fullname | escape | underline}}
2 |
3 | .. currentmodule:: {{ module }}
4 |
5 | .. autoclass:: {{ objname }}
6 | :members:
7 | :show-inheritance:
8 | :inherited-members:
9 |
10 | {% block methods %}
11 | .. automethod:: __init__
12 |
13 | {% if methods %}
14 | .. rubric:: {{ _('Methods') }}
15 |
16 | .. autosummary::
17 | {% for item in methods %}
18 | ~{{ name }}.{{ item }}
19 | {%- endfor %}
20 | {% endif %}
21 | {% endblock %}
22 |
23 | {% block attributes %}
24 | {% if attributes %}
25 | .. rubric:: {{ _('Attributes') }}
26 |
27 | .. autosummary::
28 | {% for item in attributes %}
29 | ~{{ name }}.{{ item }}
30 | {%- endfor %}
31 | {% endif %}
32 | {% endblock %}
--------------------------------------------------------------------------------
/docs/source/_templates/custom-module-template.rst:
--------------------------------------------------------------------------------
1 | {{ fullname | escape | underline}}
2 |
3 | .. automodule:: {{ fullname }}
4 |
5 | {% block attributes %}
6 | {% if attributes %}
7 | .. rubric:: Module Attributes
8 |
9 | .. autosummary::
10 | :toctree:
11 | {% for item in attributes %}
12 | {{ item }}
13 | {%- endfor %}
14 | {% endif %}
15 | {% endblock %}
16 |
17 | {% block functions %}
18 | {% if functions %}
19 | .. rubric:: {{ _('Functions') }}
20 |
21 | .. autosummary::
22 | :toctree:
23 | :recursive:
24 | {% for item in functions %}
25 | {{ item }}
26 | {%- endfor %}
27 | {% endif %}
28 | {% endblock %}
29 |
30 | {% block classes %}
31 | {% if classes %}
32 | .. rubric:: {{ _('Classes') }}
33 |
34 | .. autosummary::
35 | :toctree:
36 | :template: custom-class-template.rst
37 | :recursive:
38 | {% for item in classes %}
39 | {{ item }}
40 | {%- endfor %}
41 | {% endif %}
42 | {% endblock %}
43 |
44 | {% block exceptions %}
45 | {% if exceptions %}
46 | .. rubric:: {{ _('Exceptions') }}
47 |
48 | .. autosummary::
49 | :toctree:
50 | {% for item in exceptions %}
51 | {{ item }}
52 | {%- endfor %}
53 | {% endif %}
54 | {% endblock %}
55 |
56 | {% block modules %}
57 | {% if modules %}
58 | .. rubric:: Modules
59 |
60 | .. autosummary::
61 | :toctree:
62 | :template: custom-module-template.rst
63 | :recursive:
64 | {% for item in modules %}
65 | {{ item }}
66 | {%- endfor %}
67 | {% endif %}
68 | {% endblock %}
69 |
--------------------------------------------------------------------------------
/docs/source/_templates/custom.css:
--------------------------------------------------------------------------------
1 | .toggle .header {
2 | display: block;
3 | clear: both;
4 | }
5 |
6 | .toggle .header:after {
7 | content: " ▶";
8 | }
9 |
10 | .toggle .header.open:after {
11 | content: " ▼";
12 | }
--------------------------------------------------------------------------------
/docs/source/_templates/page.html:
--------------------------------------------------------------------------------
1 | {% extends "!page.html" %} {% block footer %}
2 |
12 | {% endblock %}
13 |
--------------------------------------------------------------------------------
/docs/source/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | import os
14 | import sys
15 |
16 | sys.path.insert(0, os.path.abspath("../../"))
17 |
18 |
19 | # -- Project information -----------------------------------------------------
20 | import map2loop
21 |
22 | project = "map2loop"
23 | copyright = "2024, Loop development team"
24 | author = "Roy Thomson, Mark Jessell, Lachlan Grose, and others"
25 |
26 | # The full version, including alpha/beta/rc tags
27 | release = map2loop.__version__
28 |
29 |
30 | # -- General configuration ---------------------------------------------------
31 |
32 | # Add any Sphinx extension module names here, as strings. They can be
33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
34 | # ones.
35 | autoclass_content = "both" # include both class docstring and __init__
36 | autodoc_default_flags = [
37 | # Make sure that any autodoc declarations show the right members
38 | "members",
39 | "inherited-members",
40 | "private-members",
41 | "show-inheritance",
42 | ]
43 | autosummary_generate = True # Make _autosummary files and include them
44 | napoleon_numpy_docstring = True # False # Force consistency, leave only Google
45 | napoleon_use_rtype = False # More legible
46 | autosummary_imported_members = True
47 | autosummary_ignore_module_all = False
48 | # Add any Sphinx extension module names here, as strings. They can be
49 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
50 | # ones.
51 | extensions = [
52 | # Need the autodoc and autosummary packages to generate our docs.
53 | "sphinx.ext.autodoc",
54 | "sphinx.ext.autosummary",
55 | # The Napoleon extension allows for nicer argument formatting.
56 | "sphinx.ext.napoleon",
57 | # add sphinx gallery
58 | "sphinx_gallery.gen_gallery",
59 | # citations
60 | "myst_parser",
61 | ]
62 |
63 | source_suffix = {".rst": "restructuredtext", ".txt": "markdown", ".md": "markdown"}
64 |
65 | # Add any paths that contain templates here, relative to this directory.
66 | templates_path = ["_templates"]
67 |
68 |
69 | # List of patterns, relative to source directory, that match files and
70 | # directories to ignore when looking for source files.
71 | # This pattern also affects html_static_path and html_extra_path.
72 | exclude_patterns = []
73 |
74 |
75 | # -- Options for HTML output -------------------------------------------------
76 | html_theme_options = {
77 | "icon_links": [
78 | {
79 | "name": "GitHub",
80 | "url": "https://github.com/loop3d/map2loop",
81 | "icon": "fab fa-github-square",
82 | },
83 | {"name": "Twitter", "url": "https://twitter.com/loop3d", "icon": "fab fa-twitter-square"},
84 | ],
85 | # "navbar_start": ["navbar-logo", "navbar-version"],
86 | # "use_edit_page_button": True,
87 | "collapse_navigation": True,
88 | "external_links": [{"name": "Loop3d", "url": "https://www.loop3d.org"}],
89 | }
90 | # The theme to use for HTML and HTML Help pages. See the documentation for
91 | # a list of builtin themes.
92 | #
93 | html_theme = "pydata_sphinx_theme"
94 |
95 | # Add any paths that contain custom static files (such as style sheets) here,
96 | # relative to this directory. They are copied after the builtin static files,
97 | # so a file named "default.css" will overwrite the builtin "default.css".
98 | # Add any paths that contain custom static files (such as style sheets) here,
99 | # relative to this directory. They are copied after the builtin static files,
100 | # so a file named "default.css" will overwrite the builtin "default.css".
101 | html_static_path = ["_static"]
102 | autosummary_generate = True
103 |
104 | autosummary_mock_imports = ["LoopStructural.interpolators._cython"]
105 | # Sphinx gallery examples
106 |
107 |
108 | # from LoopStructural.visualisation.sphinx_scraper import Scraper as LoopScraper
109 | from sphinx_gallery.sorting import ExampleTitleSortKey
110 |
111 | sphinx_gallery_conf = {
112 | "examples_dirs": ["../examples/"],
113 | "gallery_dirs": ["_auto_examples/"], # path to where to save gallery generated output
114 | "image_scrapers": ("matplotlib"),
115 | "within_subsection_order": ExampleTitleSortKey,
116 | "reference_url": {"LoopStructural": None},
117 | }
118 |
119 | # def setup(app):
120 | # app.add_stylesheet('custom.css')
121 |
--------------------------------------------------------------------------------
/docs/source/index.rst:
--------------------------------------------------------------------------------
1 | .. map2loop documentation master file, created by
2 | sphinx-quickstart on Wed Jan 17 15:48:56 2024.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Map2loop |release|
7 | ====================================
8 |
9 | Generate 3D geological model inputs from geolocial maps — a high-level implementation and extension of the original map2loop code developed by Prof. Mark Jessell at UWA. To see an example interactive model built with map2loop and LoopStructural, follow this link:
10 |
11 | `Brockman Syncline model, WA `_
12 |
13 |
14 |
15 | Usage
16 | ------
17 |
18 | Our notebooks cover use cases in more detail, but here is an example of processing Loop's South Australia remote geospatial data in just 20 lines of Python.
19 |
20 | First, lets import map2loop and define a bounding box. You can use GIS software to find one. Remember what projection your coordinates are in!
21 |
22 | .. code-block::
23 |
24 | from map2loop.project import Project
25 | from map2loop.m2l_enums import VerboseLevel
26 |
27 | # Note that this region is defined in the EPSG 28354 projection and
28 | # x and y represent easting and northing respectively
29 | bbox_3d = {
30 | 'minx': 250805.1529856466,
31 | 'miny': 6405084.328058686,
32 | 'maxx': 336682.921539395,
33 | 'maxy': 6458336.085975628,
34 | 'base': -3200,
35 | 'top': 1200
36 | }
37 |
38 |
39 |
40 | Then, specify: the state, directory for the output, the bounding box and projection from above - and hit go! That's it.
41 |
42 | .. code-block::
43 |
44 | proj = Project(use_australian_state_data = "SA",
45 | working_projection = 'EPSG:28354',
46 | bounding_box = bbox_3d,
47 | loop_project_filename = "output.loop3d"
48 | )
49 |
50 | proj.run_all()
51 |
52 |
53 | This is a minimal example and a small part of Loop.
54 |
55 | Our *documentation and other resources outline how to extend map2loop and port to the LoopStructural modelling engine. We are working to incorporate geophysical tools and best provide visualisation and workflow consolidation in the GUI.*
56 |
57 | *Loop is led by Laurent Ailleres (Monash University) with a team of Work Package leaders from:*
58 |
59 | - *Monash University: Roy Thomson, Lachlan Grose and Robin Armit*
60 | - *University of Western Australia: Mark Jessell, Jeremie Giraud, Mark Lindsay and Guillaume Pirot*
61 | - *Geological Survey of Canada: Boyan Brodaric and Eric de Kemp*
62 |
63 |
64 |
65 | Known Issues and FAQs
66 | ~~~~~~~~~~~~~~~~~~~~~~~
67 | - Developing with docker on Windows means you won't have GPU passthrough and can’t use a discrete graphics card in the container even if you have one.
68 | - If Jupyter links require a token or password, it may mean port 8888 is already in use. To fix, either make docker map to another port on the host ie -p 8889:8888 or stop any other instances on 8888.
69 |
70 | Links
71 | ~~~~~~
72 |
73 | `https://loop3d.github.io/ `_
74 |
75 | `https://github.com/Loop3D/LoopStructural `_
76 |
77 |
78 |
79 | .. toctree::
80 | :hidden:
81 |
82 | user_guide/index
83 | _auto_examples/index
84 | CHANGLOG.md
85 |
86 | .. autosummary::
87 | :caption: API
88 | :toctree: _autosummary
89 | :template: custom-module-template.rst
90 | :recursive:
91 |
92 | map2loop
93 |
--------------------------------------------------------------------------------
/docs/source/user_guide/changing_colours.rst:
--------------------------------------------------------------------------------
1 | Changing Colours
2 | ================
3 | Using a CSV file
4 | ----------------
5 | The easiest way to set the colour of different units in your model is to create a csv file that contains the lithological unit names in one column and the hex colour code in another, as shown below:
6 |
7 | .. image:: _static/images/csv.png
8 | :width: 500
9 |
10 | Changing colours via your Jupyter notebook
11 | ------------------------------------------
12 | You can also change unit and fault colours manually in your Jupyter notebook. There are examples of the code you can use to achieve this below. Importantly, if you decide to use this method, you’ll need to call this code after the creation of ‘model’ in your Jupyter notebook.
13 | The stratigraphy data is stored in a python dictionary, so to change the colour of specific elements you need to use the associated key. In the below cases you need to navigate through the nested dictionaries to find the colour value. To view the contents of the stratigraphic column dictionary in your own notebook you can just use the command:
14 |
15 | .. code-block:: python
16 |
17 | model. stratigraphic_column
18 |
19 | To view a dictionary nested within the stratigraphic_column dictionary you will need to specify the key, for example:
20 |
21 | .. code-block:: python
22 |
23 | model.stratigraphic_column['sg']
24 |
25 | If you run this command, you’ll get an output showing the sg dictionary (which contains all of the rock units) as well as any dictionaries nested within it.
26 |
27 |
28 | Unit Colours
29 | .............
30 | Following on from the above explanation, to access the unit colour you will need to navigate from the stratigraphic_column dictionary through the dictionaries nested within it. In the example below you’ll navigate to ‘sg’ then ‘unit_name’ then ‘colour’ where you can finally edit the hex colour value.
31 |
32 | Make sure to replace ‘unit_name’ with the name of the unit you want to change and the ‘#f71945’ with the hex colour code you desire. Remember you can check these values by running and inspecting the output of: model. stratigraphic_column
33 |
34 | .. code-block:: python
35 |
36 | model.stratigraphic_column['sg']['unit_name']['colour'] = '#f71945'
37 |
38 | Fault Colours
39 | ..............
40 | The code to change the colour of faults is very similar, where ‘fault_name’ is the name of the fault you’re editing.
41 |
42 | .. code-block:: python
43 |
44 | model.stratigraphic_column['faults']['fault_name’]['colour'] = '#f25d27'
45 |
46 | Please see the examples for further clarification.
47 |
48 |
49 |
--------------------------------------------------------------------------------
/docs/source/user_guide/config_file.rst:
--------------------------------------------------------------------------------
1 | Mapping attributes to variables using a JSON file
2 | ===================================================
3 | Once you’ve completed your map in QGIS, you’ll need to map the attributes (with whatever names you’ve given them) to the variable names used in map2loop. You can map the attributes to the variable names used in version 2 or 3 of map2loop, as specified in the tables in the Setting up your Shapefiles section.
4 | An example json file is shown below, using map2loop-2 variable names (also known as Legacy code. If you decide to use map2loop-2 variable names, you will have to set the legacy flag to true in the map2loop calling code later on. If you use map2loop-3 variable names you'll need to set the legacy flag to false.
5 |
6 | Feel free to copy the attached template and fill in the required variables with the attribute names specific to your project.
7 |
8 | Config File Template
9 | ---------------------
10 | The templates below demonstrate how to setup a config file for map2loop.
11 | Explanations of the JSON file elements:
12 |
13 | * The left most 'term' is the map2loop variable name
14 | * The information after the hash on the right is a description of the required field. You can delete this from your own file if you'd like.
15 | * You'll need to change the attribute name in the second quotation to match your shapefile attribute names (e.g. change "INSERT_DIP" to your attribute name). Ensure that the attribute name is between '' or "".
16 | * Any lines with a *opt in the description string, means that they are optional. If you don't want to include them, just leave the attribute field blank (e.g. "g": '', )
17 |
18 | For more information on the variables and map2loop requirements please see the documentation.
19 |
20 | Variable Names (map2loop-3) Template
21 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22 | Note, there is an inbuilt converter within map2loop so you can use either of the config files with the most recent version of map2loop.
23 | `This <../_static/HJSON_TEMPLATE.hjson>`_ is an example of the legacy variables.
24 |
25 | .. code-block:: python
26 |
27 | {
28 | #ORIENTATION SHAPEFILE ATTRIBUTES
29 | "orientation_type": "dip direction", #attribute containing dip information
30 | "dipdir_column": "INSERT_DIP_DIRECTION", #attribute containing dip direction information
31 | "dip_column": "INSERT_DIP", #attribute containing dip information
32 | "description_column": "INSERT_STRUCTURE_DESCRIPTION", #*opt attribute containing type of structure (eg. S0, S1)
33 | "bedding_text": "INSERT_BEDDING_TEXT", #*opt text defining bedding measurements in the "sf" field (eg "Bedding" or "S0")
34 | "overturned_column": "INSERT_OVERTURNED", #*opt attribute containing type of foliation
35 | "overturned_text": "INSERT_OVERTURNED_DESCRIPTION", #*opt text defining overturned bedding measurements (eg. 'overturned')
36 | "objectid_column": "INSERT_ID", #*opt attribute containing unique id of structural points
37 | #LITHOLOGY SHAPEFILE ATTRIBUTES
38 | "unitname_column": "INSERT_UNITNAME", #attribute containing stratigraphic unit name (most specific)
39 | "alt_unitname_column": "INSERT_ALTERNATIVE_UNITNAME_CODE", #attribute containing alternative stratigraphic unit name (eg unit code). Can be the same as "unitname_column"
40 | "group_column": "INSERT_GROUP", #*opt attribute containing stratigraphic group
41 | "supergroup_column": "INSERT_SUPERGROUP", #*opt attribute containing stratigraphic supergroup (most coarse classification)
42 | "description_column": "INSERT_DESCRIPTION", #*opt general description field
43 | "minage_column": "INSERT_MIN_AGE", #*opt attribute containing minimum unit age
44 | "maxage_column": "INSERT_MAX_AGE", #*opt attribute containing maximum unit age
45 | "rocktype_column": "INSERT_ROCKTYPE1", #*opt attribute containing extra lithology information (can indicate intrusions)
46 | "alt_rocktype_column": "INSERT_ALTERNATIVE_ROCKTYPE", #*opt attribute containing secondary rocktype information
47 | "sill_text": "INSERT_SILL_TEXT", #*opt text defining a sill in the "ds" field (eg 'sill')
48 | "intrusive_text": "INSERT_INTRUSIVE_TEXT", #*opt text defining an intrusion in the "r1" field (eg 'intrusion')
49 | "volcanic_text": "INSERT_VOLCANIC_TEXT", #*opt text defining volcanics in the "ds" field (eg 'volcanic')
50 | "objectid_column": "INSERT_ID", #*opt attribute containing unique object id (used in polygon and lineString shapefiles)
51 | "ignore_codes": ["INSERT_COVER_UNIT_CODES_TO_IGNORE"], #*opt attribute containing codes to ignore
52 | #LINEAR FEATURES SHAPEFILE ATTRIBUTES
53 | "structtype_column": "INSERT_FEATURE_TYPE", #attribute containing linear structure type (e.g. fault)
54 | "fault_text": "INSERT_FAULT_TEXT", #text defining faults in the "f" field (eg. 'fault')
55 | "dip_null_value": "-999", #Default fault dip value, if 'fdip' field is empty
56 | "dipdir_flag": "num", #*opt specifies whether fdipdir is "num":numeric or other ("alpha")
57 | "dipdir_column": "INSERT_DIP_DIRECTION", #*opt attribute containing the fault dip direction (defaults to -999)
58 | "dip_column": "INSERT_DIP", #*opt attribute containing numeric fault dip value (defaults to fdipnull)
59 | "orientation_type": "dip direction", #Set the measurement convention used (either 'strike' or 'dip direction')
60 | "dipestimate_column": "INSERT_DIP_ESTIMATE", #*opt field for text fault dip estimate value (defaults to none)
61 | "dipestimate_text": "'NORTH_EAST','NORTH',,'NOT ACCESSED'", #*opt text used to estimate dip in increasing steepness, in "fdipest" field
62 | "name_column": "INSERT_FAULT_NAME", #*opt attribute containing the fault name
63 | "objectid_column": "INSERT_ID", #*opt attribute containing unique object id (used in polygon and lineString shapefiles)
64 | #FOLD FEATURES SHAPEFILE ATTRIBUTES
65 | "structtype_column": "INSERT_FEATURE_TYPE", #attribute containing linear structure type (e.g. fault)
66 | "fold_text": "INSERT_FOLD_TEXT", #text defining folds in the "f" field (eg. 'fold')
67 | "description_column": "INSERT_FOLD_DESCRIPTION", #*opt attribute containing type of fold
68 | "synform_text": "INSERT_SYNCLINE_TEXT", #*opt text defining synclines in the "f" field (eg. 'syncline')
69 | "foldname_column": "INSERT_FOLD_NAME", #*opt attribute containing the fold name
70 | "objectid_column": "INSERT_ID", #*opt attribute containing unique object id (used in polygon and lineString shapefiles)
71 | }
72 |
73 | The following is an example filled out of a JSON config file:
74 |
75 | .. code-block:: python
76 |
77 | {
78 | "structure" : {
79 | "orientation_type": "strike",
80 | "dipdir_column": "strike",
81 | "dip_column": "dip",
82 | "description_column": "feature",
83 | "bedding_text": "Bed",
84 | "overturned_column": "structypei",
85 | "overturned_text": "BEOI",
86 | "objectid_column": "geopnt_id",
87 | },
88 | "geology" : {
89 | "unitname_column": "unitname",
90 | "alt_unitname_column": "code",
91 | "group_column": "group_",
92 | "supergroup_column": "supersuite",
93 | "description_column": "descriptn",
94 | "minage_column": "min_age_ma",
95 | "maxage_column": "max_age_ma",
96 | "rocktype_column": "rocktype1",
97 | "alt_rocktype_column": "rocktype2",
98 | "sill_text": "is a sill",
99 | "intrusive_text": "intrusive",
100 | "volcanic_text": "volcanic",
101 | "objectid_column": "objectid",
102 | "ignore_codes": ["cover"],
103 | },
104 | "fault" : {
105 | "structtype_column": "feature",
106 | "fault_text": "Fault",
107 | "dip_null_value": "0",
108 | "dipdir_flag": "num",
109 | "dipdir_column": "dip_dir",
110 | "dip_column": "dip",
111 | "orientation_type": "dip direction",
112 | "dipestimate_column": "dip_est",
113 | "dipestimate_text": "gentle,moderate,steep",
114 | "name_column": "name",
115 | "objectid_column": "objectid",
116 | },
117 | "fold" : {
118 | "structtype_column": "feature",
119 | "fold_text": "Fold axial trace",
120 | "description_column": "type",
121 | "synform_text": "syncline",
122 | "foldname_column": "name",
123 | "objectid_column": "objectid",
124 | }
125 | }
126 |
127 | map2loop-3 variable names JSON File Template
128 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
129 | This is a template with the most up-to date variable names.
--------------------------------------------------------------------------------
/docs/source/user_guide/explanation.rst:
--------------------------------------------------------------------------------
1 | What map2loop does
2 | ==================
3 |
--------------------------------------------------------------------------------
/docs/source/user_guide/exporting.rst:
--------------------------------------------------------------------------------
1 | Exporting
2 | ==========
3 |
4 | Geological map
5 | --------------
--------------------------------------------------------------------------------
/docs/source/user_guide/fault_offset.rst:
--------------------------------------------------------------------------------
1 | Fault offset calculation
2 | ------------------------
3 |
4 |
--------------------------------------------------------------------------------
/docs/source/user_guide/getting_started.rst:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 | map2loop is a python library that improves the ease and accessibility of 3D modelling with Loop. It enables you to create 3D geological models from digital maps produced in programs such as GIS. map2loop's role within the Loop3D ecosystem is to automatically convert digital map data into a useable format. The generated ‘.loop3d’ output file can then be transformed into a 3D geological model using the LoopStructural library.
4 |
5 | map2loop Requirements
6 | ......................
7 | In order to run map2loop you will need the following input files:
8 |
9 | #. Polygon shapefile containing your lithologies
10 | #. LineString shapefile containing your linear features (e.g. faults)
11 | #. Point data shapefile containing orientation data
12 | #. hjson config file, used to map the attribute names used in your shapefiles to map2loop's variables
13 |
14 | Additional useful inputs:
15 |
16 | * DEM or DTM file for your map region
17 | * Optional CSV file specifying unit colours
18 |
19 | If you need help setting up these files please refer to the relevant map2loop user pages or to the examples which provide a step-by-step guide.
20 |
--------------------------------------------------------------------------------
/docs/source/user_guide/index.rst:
--------------------------------------------------------------------------------
1 | User Guide
2 | ===========
3 |
4 | .. toctree::
5 | :caption: User Guide
6 |
7 | getting_started
8 | installation
9 | geomodelling
10 | config_file
11 | setup_jupyter
12 | changing_colours
13 | stratigraphic_thickness
14 | stratigraphic_order
15 | fault_offset
16 | m2l_code_template
17 | exporting
18 |
--------------------------------------------------------------------------------
/docs/source/user_guide/installation.rst:
--------------------------------------------------------------------------------
1 | Installing map2loop
2 | ===================
3 |
4 | Install
5 | -------------
6 |
7 | You will need some flavour of conda (a python package manager, `see here `_), as well as Python ≥ 3.8
8 |
9 | In addition, map2loop installation may run smoother if ```conda-forge``` is added to the channels.
10 | To check for that, run:
11 |
12 | .. code-block::
13 |
14 | conda config --show channels
15 |
16 |
17 | if conda-forge is not in the output, the channel can be added with:
18 |
19 | .. code-block::
20 |
21 | conda config --add channels conda-forge
22 |
23 |
24 |
25 | Express install:
26 | ~~~~~~~~~~~~~~~~
27 |
28 | To just install map2loop run the following command
29 |
30 | .. code-block::
31 |
32 | conda install -c conda-forge -c loop3d map2loop -y
33 |
34 |
35 | Development
36 | ~~~~~~~~~~~~
37 |
38 | If you want to tinker yourself/contribute, clone the source code with
39 |
40 | .. code-block::
41 |
42 | git clone https://github.com/Loop3D/map2loop.git
43 |
44 | Or get the source + example notebooks with
45 |
46 | .. code-block::
47 |
48 | git clone https://github.com/Loop3D/map2loop.git
49 | git clone https://github.com/Loop3D/map2loop-3-notebooks
50 |
51 |
52 | Navigate into the map2loop folder, and issue the following to install map2loop and its dependencies.
53 |
54 | .. code-block::
55 |
56 | conda install -c conda-forge -c loop3d --file dependencies.txt
57 |
58 | To install map2loop in an editable environment run the following command:
59 |
60 | .. code-block::
61 |
62 | pip install -e .
63 |
64 |
65 | Building with Docker
66 | ---------------------
67 |
68 | Fair warning, we recommend conda to almost everyone. With great software development power comes great environment setup inconvenience.
69 | You'll need to download and install the `docker containerisation software `_, and the docker and docker-compose CLI.
70 |
71 | Development with docker
72 | ~~~~~~~~~~~~~~~~~~~~~~~
73 |
74 | 1. Clone this repo and navigate inside as per above
75 | 2. Run the following and click on the Jupyter server forwarded link to access and edit the notebooks
76 |
77 | .. code-block::
78 |
79 | docker-compose up --build
80 |
81 |
82 | 3. To hop into a bash shell in a running container, open a terminal and issue
83 |
84 | .. code-block::
85 |
86 | docker ps
87 |
88 |
89 | Find the container name or ID and then run
90 |
91 | .. code-block::
92 |
93 | docker exec -it bash
94 | # Probably -> docker exec -it map2loop_dev_1 bash
--------------------------------------------------------------------------------
/docs/source/user_guide/setup_jupyter.rst:
--------------------------------------------------------------------------------
1 | Setting up your Jupyter Notebook
2 | ================================
3 | Now that you’ve created all of the required inputs for map2loop, you can set up a Jupyter notebook to run the code. In order to do this, you’ll need to create an Anaconda (or similar) environment with all the required dependencies. If you need help setting up your virtual environment, please refer to the Installation page.
4 |
5 | Map2loop calling code:
6 | ---------------------
7 | In order to ‘run’ map2loop, you need to pass the program several pieces of information, including:
8 | * Shape file paths
9 | * Csv colour file path (optional)
10 | * DEM / DTM path
11 | * hjson config file that maps the attributes from your QGIS project onto the map2loop variables.
12 |
13 | An example code template for processing your QGIS map data using map2loop is provided under the Examples section on the map2loop website. The output from this code is a .loop3d file which can then be passed to LoopStructural to create your 3d model.
14 |
15 | Depending on the sorter algorithm you choose to use with map2loop (as described in the template), you’ll receive different outputs. If you use the take best sorter algorithm, you can expect an output similar to:
16 |
17 | **Best sorter SorterAgeBased calculated contact length of 29632.67023438857**
18 |
19 |
20 | Inspecting the .loop3d output file
21 | ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,
22 | You can inspect your map2loop output file using the ncdump command via the terminal (e.g. Anaconda Prompt). In order to use this tool, you’ll need to ensure that you have netcdf4 installed in your environment. You can check this by using the command: ''**conda list**'' within your Anaconda environment. This will list all of the installed applications and dependencies. You’ll need to scroll through these to see if netcdf4 is installed.
23 | If you are able to use this tool, you can run it with the following command, where local_source.loop3d is the name of the output file that you named in the map2loop calling code (see the explanation in the Map2Loop template notebook).
24 | **ncdump local_source.loop3d**
25 |
26 | Note: this command will only work if you are in the correct working directory (i.e wherever you saved the loop3d file). You can change directories using the **cd** command, followed by the directory path that you’d like to change to.
27 | The expected output of the ncdump command, is a series of text representing the tabulated data generated by map2loop. It will be output directly onto the terminal console.
28 |
29 | LoopStructural calling code
30 | ---------------------------
31 | Once you have run map2loop successfully, you can pass the produced .loop3d file to apt LoopStructural calling code, to produce a 3D model.
32 | There is an example of this code on the map2loop documentation page. It is also explained in the Loop Structural Calling Code Template.
33 |
34 |
35 |
--------------------------------------------------------------------------------
/docs/source/user_guide/stratigraphic_order.rst:
--------------------------------------------------------------------------------
1 | Identifying stratigraphic order
2 | ===============================
3 |
4 | Calculating stratigraphic order in map2loop can be difficult depending on the
5 | stratigraphy of your region of interest and how much data has been collected in
6 | the field. Because of this map2loop has been written so that a python-capable user
7 | can create their own stratigraphic column sorter and use it within the map2loop process.
8 |
9 | Below is the structure of a stratigraphic column sorter and an example of how to implement
10 | your own and use it within map2loop.
11 |
12 | In order to make a plugin sorter map2loop has a template that is extendable. The whole
13 | template is shown below:
14 |
15 | .. code-block::
16 |
17 | class Sorter(ABC):
18 | """
19 | Base Class of Sorter used to force structure of Sorter
20 |
21 | Args:
22 | ABC (ABC): Derived from Abstract Base Class
23 | """
24 | def __init__(self):
25 | """
26 | Initialiser of for Sorter
27 | """
28 | self.sorter_label = "SorterBaseClass"
29 |
30 | def type(self):
31 | """
32 | Getter for subclass type label
33 |
34 | Returns:
35 | str: Name of subclass
36 | """
37 | return self.sorter_label
38 |
39 | @beartype.beartype
40 | @abstractmethod
41 | def sort(self, units: pandas.DataFrame, unit_relationships: pandas.DataFrame, stratigraphic_order_hint: list, contacts: pandas.DataFrame, map_data: MapData) -> list:
42 | """
43 | Execute sorter method (abstract method)
44 |
45 | Args:
46 | units (pandas.DataFrame): the data frame to sort (columns must contain ["layerId", "name", "minAge", "maxAge", "group"])
47 | units_relationships (pandas.DataFrame): the relationships between units (columns must contain ["Index1", "Unitname1", "Index2", "Unitname2"])
48 | stratigraphic_order_hint (list): a list of unit names to be used as a hint to sorting the units
49 | contacts (geopandas.GeoDataFrame): unit contacts with length of the contacts in metres
50 | map_data (map2loop.MapData): a catchall so that access to all map data is available
51 |
52 | Returns:
53 | list: sorted list of unit names
54 | """
55 | pass
56 |
57 | Using this abstract base class a new class can be created by taking that base class and
58 | replacing the __init__ and sort functions, the simplest example is shown below:
59 |
60 | .. code-block::
61 | from map2loop.sorter import Sorter
62 | from map2loop.mapdata import MapData
63 | import pandas
64 | import geopandas
65 |
66 | class mySorter(Sorter):
67 | def __init__(self):
68 | self.sorter_label = "mySorter"
69 |
70 | def sort(self,
71 | units: pandas.DataFrame,
72 | unit_relationships: pandas.DataFrame,
73 | stratigraphic_order_hint: list,
74 | contacts: geopandas.GeoDataFrame,
75 | map_data: MapData
76 | ) -> list:
77 | unitnames = sorted(units['name'])
78 | return unitnames
79 |
80 | This example will sort the units into alphabetical order based on name and return the
81 | stratigraphic column order in a list of unit names.
82 |
83 | To use this new sorter in the map2loop project one final line needs to
84 | be added after the Project has been initialised:
85 |
86 | .. code-block::
87 |
88 | proj = map2loop.Project( ... )
89 |
90 | proj.set_sorter(mySorter())
91 |
92 | Notes
93 | -----
94 | You need to set the sorter as an instance of mySorter (with the ()s) rather than the definition.
95 |
96 | The sorter takes the existing units dataframe and must return a list containing all the
97 | unitnames present in that dataframe. If some are added or missing map2loop with raise an
98 | exception. Also while you have control of this dataframe you have the power to add or
99 | remove units, and change features of any unit but if you do this there is no longer any
100 | guarantee that map2loop will still process your maps or even finish.
101 |
102 | As the base class Sorter contains abstract methods and is parsed through beartype the
103 | structure of the sort function must remain the same. If there is reason to access more
104 | map2loop data that isn't in the map_data structure raise an issue with in the map2loop
105 | github repo and we can address that.
106 |
107 | Parameters
108 | ----------
109 | As seen in the template and the sort abstract method you have access to other data
110 | from within the map2loop process. Below is a brief description of each and a potential
111 | use for them in your thickness calculator:
112 |
113 | units - this is the data frame that contains the units and fields such as group, supergroup and
114 | min/max ages. If the age data is present it can be useful in sorting the units. Also
115 | group and supergroup information could be used to ensure that all units within the
116 | same group/supergroup are contiguous.
117 |
118 | unit_relationships - this data frame contains a list of adjacent units within the shapefile.
119 | The format is ['Index1', 'Unitname1', 'Index2', 'Unitname2'] and each row is a single
120 | adjacency that was found. Note that some of these contacts might have been across a fault
121 | so take that into account when using this data.
122 |
123 | stratigraphic_order_hint - this is a first pass attempt at the stratigraphic column
124 | calculated by map2model which looks at unit adjacency in the shapefile.
125 |
126 | contacts - this geometric data frame contains linear data of where adjacent
127 | units are and the length of that contact. Using this data you might prioritise
128 | longer contacts as more likely to be adjacent in the stratigraphic column.
129 |
130 | map_data - this catch-all gives you complete access to the shapefiles used in map2loop.
131 | If you need access to the structural orientation data you can use
132 | map_data.get_map_data(Datatype.STRUCTURE) or if you want the geology map
133 | map_data.get_map_data(Datatype.GEOLOGY) and you have access to those shapefiles. Note
134 | that changing information or using setter function from map_data is likely to cause
135 | problems within the map2loop workflow.
136 |
137 |
--------------------------------------------------------------------------------
/docs/source/user_guide/stratigraphic_thickness.rst:
--------------------------------------------------------------------------------
1 | Calculating stratigraphic thicknesses
2 | =====================================
3 |
4 | Calculating unit thicknesses in map2loop is both an important and a difficult
5 | task. There is no 'best' way to determine thicknesses because it depends on the
6 | stratigraphy of your region of interest and how much data has been collected in
7 | the field. Because of this map2loop has been written so that a python-capable user
8 | can create their own thickness calculator and use it within the map2loop process.
9 |
10 | Below is the structure of a thickness calculator and an example of how to implement
11 | your own and use it within map2loop.
12 |
13 | In order to make a plugin thickness calculator map2loop has a template that is
14 | extendable. The whole template is shown below:
15 |
16 | .. code-block::
17 |
18 | class ThicknessCalculator(ABC):
19 | """
20 | Base Class of Thickness Calculator used to force structure of ThicknessCalculator
21 |
22 | Args:
23 | ABC (ABC): Derived from Abstract Base Class
24 | """
25 |
26 | def __init__(self):
27 | """
28 | Initialiser of for ThicknessCalculator
29 | """
30 | self.thickness_calculator_label = "ThicknessCalculatorBaseClass"
31 |
32 | def type(self):
33 | """
34 | Getter for subclass type label
35 |
36 | Returns:
37 | str: Name of subclass
38 | """
39 | return self.thickness_calculator_label
40 |
41 | @beartype.beartype
42 | @abstractmethod
43 | def compute(
44 | self,
45 | units: pandas.DataFrame,
46 | stratigraphic_order: list,
47 | basal_contacts: geopandas.GeoDataFrame,
48 | map_data: MapData,
49 | ) -> pandas.DataFrame:
50 | """
51 | Execute thickness calculator method (abstract method)
52 |
53 | Args:
54 | units (pandas.DataFrame): the data frame of units to add thicknesses to
55 | stratigraphic_order (list): a list of unit names sorted from youngest to oldest
56 | basal_contacts (geopandas.GeoDataFrame): basal contact geo data with locations and unit names of the contacts (columns must contain ["ID","basal_unit","type","geometry"])
57 | map_data (map2loop.MapData): a catchall so that access to all map data is available
58 |
59 | Returns:
60 | pandas.DataFrame: units dataframe with added thickness column for calculated thickness values
61 | """
62 | pass
63 |
64 | Using this abstract base class a new class can be created by taking that base class and
65 | replacing the __init__ and compute functions, the simplest example is shown below:
66 |
67 | .. code-block::
68 | from map2loop.thickness_calculator import ThicknessCalculator
69 | from map2loop.mapdata import MapData
70 | import pandas
71 | import geopandas
72 |
73 | class myThicknessCalculator(ThicknessCalculator):
74 | def __init__(self, thickness=100):
75 | self.thickness_calculator_label = "myThicknessCalculator"
76 | self.default_thickness = thickness
77 |
78 | def compute(
79 | self,
80 | units: pandas.DataFrame,
81 | stratigraphic_order: list,
82 | basal_contacts: pandas.DataFrame,
83 | map_data: MapData,
84 | ) -> pandas.DataFrame:
85 | output_units = units.copy()
86 | output_units['thickness'] = default_thickness
87 | return output_units
88 |
89 | This example will set all unit thicknesses to 100m.
90 |
91 | To use this new thickness calculator in the map2loop project one final line needs to
92 | be added after the Project has been initialised:
93 |
94 | .. code-block::
95 |
96 | proj = map2loop.Project( ... )
97 |
98 | proj.set_thickness_calculator(myThicknessCalculator(50))
99 |
100 | Notes
101 | -----
102 | You need to set the thickness calculator as an instance of myThicknessCalculator
103 | (with the ()s) rather than the definition. If you want to set the default thickness using
104 | this class you can create the class with the thickness parameter as above
105 | (myThicknessCalculator(50)).
106 |
107 | The thickness calculator takes the existing units dataframe, changes the values in the
108 | thickness column and then returns the modified dataframe. While you have control of
109 | this dataframe you have the power to add or remove units, and change features
110 | of any unit but if you do this there is no longer any guarantee that map2loop will still
111 | process your maps or even finish.
112 |
113 | Parameters
114 | ----------
115 | As seen in the template and the compute abstract method you have access to other data
116 | from within the map2loop process. Below is a brief description of each and a potential
117 | use for them in your thickness calculator:
118 |
119 | units - while this is the data frame that you need to return it also contains fields
120 | such as group, supergroup and min/max ages. If you have coarser information about the
121 | thickness of a group this information could be used to ensure that the sum of the unit
122 | thicknesses in your region that are within the same group matches your information.
123 |
124 | stratigraphic_order - this is likely the most useful parameter as it tells you which
125 | units are adjacent. In combination with the basal_contacts parameter apparent thicknesses
126 | can be calculated.
127 |
128 | basal_contacts - this geometric data frame contains linear data of where adjacent
129 | contacts are. By comparing the contacts on both sides of a unit you can calculated the
130 | apparent thickness of a unit
131 |
132 | map_data - this catch-all gives you complete access to the shapefiles used in map2loop.
133 | If you need access to the structural orientation data you can use
134 | map_data.get_map_data(Datatype.STRUCTURE) and you have access to the shapefile. Note
135 | that changing information or using setter function from map_data is likely to cause
136 | problems within the map2loop workflow.
137 |
--------------------------------------------------------------------------------
/map2loop/__init__.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | loggers = {}
4 | ch = logging.StreamHandler()
5 | formatter = logging.Formatter("%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d -- %(message)s")
6 | ch.setFormatter(formatter)
7 | ch.setLevel(logging.WARNING)
8 | from .project import Project
9 | from .version import __version__
10 |
11 | import warnings # TODO: convert warnings to logging
12 | from packaging import (
13 | version as pkg_version,
14 | ) # used instead of importlib.version because adheres to PEP 440 using pkg_version.parse
15 | import pathlib
16 | import re
17 | from importlib.metadata import version as get_installed_version, PackageNotFoundError
18 |
19 |
20 | class DependencyChecker:
21 | '''
22 | A class to check installation and version compatibility of each package in dependencies.txt
23 |
24 | Attributes:
25 | package_name (str): Name of the package
26 | dependency_file (str): path to dependencies.txt
27 | required_version (str or None): required version of the package as in dependencies.txt
28 | installed_version (str or None): installed version of the package in the current environment
29 | '''
30 |
31 | def __init__(self, package_name, dependency_file="dependencies.txt"):
32 | self.package_name = package_name
33 | self.dependency_file = pathlib.Path(__file__).parent / dependency_file
34 | self.required_version = self.get_required_version()
35 | self.installed_version = self.get_installed_version()
36 |
37 | def get_required_version(self):
38 | '''
39 | Get the required package version for each package from dependencies.txt;
40 |
41 | Returns:
42 | str or None: The required version of the package (if specified), otherwise None.
43 | '''
44 | try:
45 | with self.dependency_file.open("r") as file:
46 | for line in file:
47 | if line.startswith(f"{self.package_name}=="):
48 | match = re.match(rf"^{self.package_name}==([\d\.]+)", line.strip())
49 | if match:
50 | return match.group(1)
51 | elif line.strip() == self.package_name:
52 | return None
53 | print(f"{self.package_name} version not found in {self.dependency_file}.")
54 | except FileNotFoundError:
55 | warnings.warn(
56 | f"{self.dependency_file} not found. Unable to check {self.package_name} version compatibility.",
57 | UserWarning,
58 | )
59 | return None
60 |
61 | def get_installed_version(self):
62 | '''
63 | Get the installed version of the package.
64 |
65 | Returns:
66 | str: The installed version of the package.
67 | '''
68 | try:
69 | # Use importlib.metadata to get the installed version of the package
70 | return get_installed_version(self.package_name)
71 | except PackageNotFoundError:
72 | raise ImportError(
73 | f"{self.package_name} is not installed. Please install {self.package_name}."
74 | )
75 |
76 | def check_version(self):
77 | '''
78 | Checks if the installed version of the package matches the required version in the dependencies.txt file.
79 | '''
80 | if self.required_version is None:
81 | if self.installed_version is None:
82 | raise ImportError(
83 | f"{self.package_name} is not installed. Please install {self.package_name} before using map2loop."
84 | )
85 | else:
86 | if self.installed_version is None or pkg_version.parse(
87 | self.installed_version
88 | ) != pkg_version.parse(self.required_version):
89 | raise ImportError(
90 | f"Installed version of {self.package_name}=={self.installed_version} does not match required version=={self.required_version}. "
91 | f"Please install the correct version of {self.package_name}."
92 | )
93 |
94 |
95 | def check_all_dependencies(dependency_file="dependencies.txt"):
96 | dependencies_path = pathlib.Path(__file__).parent / dependency_file
97 | try:
98 | with dependencies_path.open("r") as file:
99 | for line in file:
100 | line = line.strip()
101 | if line.startswith("gdal"):
102 | continue
103 | if line:
104 | if "==" in line:
105 | package_name, _ = line.split("==")
106 | else:
107 | package_name = line
108 |
109 | checker = DependencyChecker(package_name, dependency_file=dependency_file)
110 | checker.check_version()
111 |
112 | except FileNotFoundError:
113 | warnings.warn(
114 | f"{dependency_file} not found. No dependencies checked for map2loop.",
115 | UserWarning
116 | )
117 |
118 |
119 | # Run check for all dependencies listed in dependencies.txt
120 | check_all_dependencies()
121 |
--------------------------------------------------------------------------------
/map2loop/_datasets/clut_files/SA_clut.csv:
--------------------------------------------------------------------------------
1 | UNITNAME,code,colour
2 | test_all,PolyStyle00,#e6ffcc
3 | Unnamed_GIS_Unit___see_description,PolyStyle00,#e6ffcc
4 | Birksgate_Complex,PolyStyle01,#66b3b3
5 | Pitjantjatjara_Supersuite,PolyStyle07,#ff4d4d
6 | Manuka_Subgroup,PolyStyle022,#b3ffe6
7 | Eyre_Formation,PolyStyle043,#ffe600
8 | Bulldog_Shale,PolyStyle051,#ccff99
9 | Alcurra_Dolerite,PolyStyle054,#00ff00
10 | Amata_Dolerite,PolyStyle054,#00ff00
11 | Oodnadatta_Formation,PolyStyle059,#b3ff4d
12 | Coorikiana_Sandstone,PolyStyle059,#b3ff4d
13 | Algebuckina_Sandstone,PolyStyle076,#b3ffb3
14 | Polda_Formation,PolyStyle076,#b3ffb3
15 | Giles_Complex,PolyStyle095,#9900ff
16 | Waitoona_beds,PolyStyle0764,#b3ffff
17 | Radium_Ridge_Breccias,PolyStyle0764,#b3ffff
18 | Wataru_Gneiss,PolyStyle0767,#cc0000
19 | Dutton_Suite,PolyStyle0767,#cc0000
20 | Doonbara_Formation,PolyStyle060,#ffe6e6
21 | Yerelina_Subgroup,PolyStyle060,#ffe6e6
22 | Koolunga_Gravel,PolyStyle060,#ffe6e6
23 | Punkerri_Sandstone,PolyStyle0774,#ffe6cc
24 | Garford_Formation,PolyStyle0774,#ffe6cc
25 | Pound_Subgroup,PolyStyle0774,#ffe6cc
26 | Trainor_Hill_Sandstone,PolyStyle0875,#ff4dff
27 | Pindyin_Sandstone,PolyStyle0878,#b34d66
28 | Callanna_Group,PolyStyle0878,#b34d66
29 | Mimili_Formation,PolyStyle0916,#4d9999
30 | Munda_Group,PolyStyle0919,#990080
31 | Chambers_Bluff_Tillite,PolyStyle0952,#cc9980
32 | Murnaroo_Formation,PolyStyle0963,#ffe699
33 | Simmens_Quartzite_Member,PolyStyle0963,#ffe699
34 | ABC_Range_Quartzite,PolyStyle0963,#ffe699
35 | Table_Hill_Volcanics,PolyStyle01005,#ff99e6
36 | Bollaparudda_Subgroup,PolyStyle01005,#ff99e6
37 | Wright_Hill_beds,PolyStyle01007,#cc9999
38 | Wallaroo_Group,PolyStyle01007,#cc9999
39 | Wantapella_Volcanics,PolyStyle01328,#66ff00
40 | Varavaranha_Formation,PolyStyle01328,#66ff00
41 | Wirrildar_beds,PolyStyle0874,#ff6699
42 | Wilpena_Group,PolyStyle01690,#ffe6b3
43 | Rodda_beds,PolyStyle01690,#ffe6b3
44 | Peake_Metamorphics,PolyStyle01866,#ccccb3
45 | Wirriecurrie_Granite,PolyStyle02046,#ff6600
46 | Burra_Group,PolyStyle02352,#b39966
47 | Emeroo_Subgroup,PolyStyle02361,#994d4d
48 | Yudnamutana_Subgroup,PolyStyle03325,#b39999
49 | Nullarbor_Limestone,PolyStyle03642,#ff994d
50 | Rowland_Flat_Sand,PolyStyle03642,#ff994d
51 | Umberatana_Group,PolyStyle03661,#e6ccb3
52 | Mount_Woods_Complex,PolyStyle03685,#999900
53 | Warrow_Quartzite,PolyStyle03685,#999900
54 | Hutchison_Group,PolyStyle03685,#999900
55 | Barossa_Complex,PolyStyle03685,#999900
56 | Mulgathing_Complex,PolyStyle03807,#804d99
57 | Glenloth_Granite,PolyStyle03807,#804d99
58 | Hiltaba_Suite,PolyStyle02394,#ff0000
59 | Bimbowrie_Suite,PolyStyle02394,#ff0000
60 | Crocker_Well_Suite,PolyStyle02394,#ff0000
61 | Cultana_Subsuite,PolyStyle02394,#ff0000
62 | Billeroo_Intrusive_Complex,PolyStyle02394,#ff0000
63 | Eba_Formation,PolyStyle03869,#e6994d
64 | Ooldea_Sand,PolyStyle0556,#ffe680
65 | Barton_Sand,PolyStyle0556,#ffe680
66 | Loxton_Sand,PolyStyle0556,#ffe680
67 | Norwest_Bend_Formation,PolyStyle0556,#ffe680
68 | Hallett_Cove_Sandstone,PolyStyle0556,#ffe680
69 | Wilgena_Hill_Jaspilite,PolyStyle03876,#00ffff
70 | Coulthard_Suite,PolyStyle04592,#ff8000
71 | Peter_Pan_Supersuite,PolyStyle04592,#ff8000
72 | Willawortina_Formation,PolyStyle0188,#ffff00
73 | Tarcoola_Formation,PolyStyle04771,#cccccc
74 | Mobella_Tonalite,PolyStyle04924,#ff4d80
75 | Labyrinth_Formation,PolyStyle04991,#4d0000
76 | Mentor_Formation,PolyStyle05149,#e6b380
77 | Corunna_Conglomerate,PolyStyle05149,#e6b380
78 | Blue_Range_beds,PolyStyle05149,#e6b380
79 | Leigh_Creek_Coal_Measures,PolyStyle05293,#4dffcc
80 | Pepegoona_Porphyry,PolyStyle05355,#9966ff
81 | Wisanger_Basalt,PolyStyle05355,#9966ff
82 | Moolawatana_Suite,PolyStyle05414,#e64dff
83 | Petermorra_Volcanics,PolyStyle05440,#ccffff
84 | Saint_Kilda_Formation,PolyStyle05531,#cccc99
85 | Pandurra_Formation,PolyStyle05583,#ccb300
86 | Gairdner_Dolerite,PolyStyle06020,#00cc66
87 | Engenina_Monzogranite,PolyStyle06079,#ff9999
88 | St_Peter_Suite,PolyStyle06079,#ff9999
89 | Tunkillia_Suite,PolyStyle06198,#ff8099
90 | Basso_Suite,PolyStyle06225,#ff4d00
91 | Donington_Suite,PolyStyle06225,#ff4d00
92 | Nancatee_Granite,PolyStyle06225,#ff4d00
93 | Munjeela_Suite,PolyStyle06226,#800000
94 | Sleaford_Complex,PolyStyle06463,#664d99
95 | Muckanippie_Suite,PolyStyle06478,#00ffb3
96 | Bumbumbie_Suite,PolyStyle06538,#0099b3
97 | Bridgewater_Formation,PolyStyle038,#ffff4d
98 | Glanville_Formation,PolyStyle038,#ffff4d
99 | Lady_Louise_Suite,PolyStyle06703,#00804d
100 | Coolanie_Gneiss,PolyStyle06871,#0000b3
101 | St_Francis_Suite,PolyStyle06924,#ff6666
102 | Cooyerdoo_Granite,PolyStyle07130,#4d0066
103 | Price_Metasediments,PolyStyle07363,#80ff00
104 | Normanville_Group,PolyStyle07365,#b3ccff
105 | Willyama_Supergroup,PolyStyle07417,#cccc00
106 | Silver_City_Suite,PolyStyle07430,#ff8066
107 | Moola_Suite,PolyStyle07430,#ff8066
108 | Eucarro_Rhyolite,PolyStyle07643,#4de6e6
109 | Pinbong_Suite,PolyStyle07914,#ffb300
110 | _,PolyStyle07983,#e6ffff
111 | Spilsby_Suite,PolyStyle08147,#ff00cc
112 | Wertigo_Granite,PolyStyle08197,#e66666
113 | Boucaut_Volcanics,PolyStyle08245,#66b3ff
114 | Miltalie_Gneiss,PolyStyle08248,#ff4db3
115 | Corny_Point_Paragneiss,PolyStyle08248,#ff4db3
116 | Gawler_Range_Volcanics,PolyStyle08251,#6666e6
117 | Tip_Top_Granite,PolyStyle08379,#cc4d80
118 | Coomandook_Formation,PolyStyle05936,#ffffe6
119 | Kangaroo_Island_Group,PolyStyle08947,#b399cc
120 | Moody_Suite,PolyStyle05511,#e69999
121 | Nuyts_Volcanics,PolyStyle09386,#b34db3
122 | Keynes_Subgroup,PolyStyle09419,#ffcce6
123 | Kanmantoo_Group,PolyStyle09419,#ffcce6
124 | Padthaway_Formation,PolyStyle09439,#e6ffb3
125 | Bosanquet_Formation,PolyStyle09625,#ff99ff
126 | Truro_Volcanics,PolyStyle09905,#b399ff
127 | Broken_Hill_Group,PolyStyle010060,#00ff80
128 | Fulham_Sand,PolyStyle05,#ffffb3
129 | Mount_Gee_Sinter,PolyStyle010830,#e6ff00
130 | Poodla_Granodiorite,PolyStyle011275,#4d00ff
131 | Coonta_Gabbro,PolyStyle011336,#00e666
132 | Mount_Crawford_Granite_Gneiss,PolyStyle04272,#e69900
--------------------------------------------------------------------------------
/map2loop/_datasets/config_files/NSW.json:
--------------------------------------------------------------------------------
1 | {
2 | "structure": {
3 | "orientation_type": "dip direction",
4 | "dipdir_column": "azimuth",
5 | "dip_column": "dip",
6 | "description_column": "DESCRIPTION",
7 | "bedding_text": "bedding",
8 | "overturned_column": "deform",
9 | "overturned_text": "BEOI",
10 | "objectid_column": "unique_id",
11 | "desciption_column": "codedescpt"
12 | },
13 | "geology": {
14 | "unitname_column": "unit_name",
15 | "alt_unitname_column": "nsw_code",
16 | "group_column": "sub_provin",
17 | "supergroup_column": "province",
18 | "description_column": "deposition",
19 | "minage_column": "top_end_ag",
20 | "maxage_column": "base_start",
21 | "rocktype_column": "class",
22 | "alt_rocktype_column": "igneous_ty",
23 | "sill_text": "sill",
24 | "intrusive_text": "intrusive",
25 | "volcanic_text": "volcanic",
26 | "objectid_column": "ID",
27 | "ignore_lithology_codes": ["cover"]},
28 | "fault": {
29 | "structtype_column": "boundaryty",
30 | "fault_text": "Fault",
31 | "dip_null_value": "0",
32 | "dipdir_flag": "num",
33 | "dipdir_column": "faultdipdi",
34 | "dip_column": "dip",
35 | "orientation_type": "dip direction",
36 | "dipestimate_column": "faultdipan",
37 | "dipestimate_text": "Moderate,Listric,Steep,Vertical",
38 | "name_column": "name",
39 | "objectid_column": "unique_id",
40 | },
41 | "fold": {
42 | "structtype_column": "codedescpt",
43 | "fold_text": "cline",
44 | "description_column": "codedescpt",
45 | "synform_text": "Syncline",
46 | "foldname_column": "NAME",
47 | "objectid_column": "unique_id"
48 | }
49 | }
--------------------------------------------------------------------------------
/map2loop/_datasets/config_files/QLD.json:
--------------------------------------------------------------------------------
1 | {
2 | "structure": {
3 | "orientation_type": "dip direction",
4 | "dipdir_column": "azimuth",
5 | "dip_column": "dip_plunge",
6 | "description_column": "DESCRIPTION",
7 | "bedding_text": "BEDDING",
8 | "overturned_column": "facing",
9 | "overturned_text": "DOWN",
10 | "objectid_column": "Feature ID",
11 | "desciption_column": "structure"
12 | },
13 | "geology": {
14 | "unitname_column": "stratigrap",
15 | "alt_unitname_column": "ru_name",
16 | "group_column": "parent_na",
17 | "supergroup_column": "parent_na",
18 | "description_column": "rock_type",
19 | "minage_column": "top_minim",
20 | "maxage_column": "base_maxi",
21 | "rocktype_column": "intrusive",
22 | "alt_rocktype_column": "lith_summ",
23 | "sill_text": "sill",
24 | "intrusive_text": "Y",
25 | "volcanic_text": "VOLCANIC",
26 | "objectid_column": "ID",
27 | "ignore_lithology_codes": ["cover"]
28 | },
29 | "fault": {
30 | "structtype_column": "type",
31 | "fault_text": "Fault",
32 | "dip_null_value": "0",
33 | "dipdir_flag": "num",
34 | "dipdir_column": "dip_dir",
35 | "dip_column": "dip",
36 | "orientation_type": "dip direction",
37 | "dipestimate_column": "dip_est",
38 | "dipestimate_text": "gentle,moderate,steep",
39 | "name_column": "name",
40 | "objectid_column": "Feature ID"
41 | },
42 | "fold": {
43 | "structtype_column": "type",
44 | "fold_text": "Fold",
45 | "description_column": "id_desc",
46 | "synform_text": "Syncline",
47 | "foldname_column": "NAME",
48 | "objectid_column": "Feature ID"
49 | }
50 | }
--------------------------------------------------------------------------------
/map2loop/_datasets/config_files/SA.json:
--------------------------------------------------------------------------------
1 | {
2 | "structure": {
3 | "orientation_type": "dip direction",
4 | "dipdir_column": "azimuth_tr",
5 | "dip_column": "inclinatio",
6 | "description_column": "DESCRIPTION",
7 | "bedding_text": "bedding",
8 | "overturned_column": "younging",
9 | "overturned_text": "-",
10 | "objectid_column": "ID",
11 | "desciption_column": "structure_"
12 | },
13 | "geology": {
14 | "unitname_column": "stratname",
15 | "alt_unitname_column": "mainunit",
16 | "group_column": "parentname",
17 | "supergroup_column": "province",
18 | "description_column": "stratdesc",
19 | "minage_column": "min",
20 | "maxage_column": "max",
21 | "rocktype_column": "no_col",
22 | "alt_rocktype_column": "no_col",
23 | "sill_text": "sill",
24 | "intrusive_text": "intrusive",
25 | "volcanic_text": "volc",
26 | "objectid_column": "ID",
27 | "ignore_lithology_codes": ["cover"]
28 | },
29 | "fault": {
30 | "structtype_column": "descriptio",
31 | "fault_text": "fault",
32 | "dip_null_value": "0",
33 | "dipdir_flag": "num",
34 | "dipdir_column": "no_col",
35 | "dip_column": "no_col",
36 | "orientation_type": "dip direction",
37 | "dipestimate_column": "no_col",
38 | "dipestimate_text": "no_col",
39 | "name_column": "no_col",
40 | "objectid_column": "ID"
41 | },
42 | "fold": {
43 | "structtype_column": "descriptio",
44 | "fold_text": "fold",
45 | "description_column": "no_col",
46 | "synform_text": "syn",
47 | "foldname_column": "NAME",
48 | "objectid_column": "ID"
49 | }
50 | }
--------------------------------------------------------------------------------
/map2loop/_datasets/config_files/TAS.json:
--------------------------------------------------------------------------------
1 | {
2 | "structure": {
3 | "orientation_type": "dip direction",
4 | "dipdir_column": "AZIMUTH",
5 | "dip_column": "DIP",
6 | "description_column": "DESCRIPTION",
7 | "bedding_text": "bedding",
8 | "overturned_column": "DESCRIPT",
9 | "overturned_text": "overturned",
10 | "objectid_column": "No_col",
11 | "desciption_column": "DESCRIPT"
12 | },
13 | "geology": {
14 | "unitname_column": "SYMBOL",
15 | "alt_unitname_column": "SYMBOL",
16 | "group_column": "SUPERGROUP",
17 | "supergroup_column": "GRP",
18 | "description_column": "DESCRIPT",
19 | "minage_column": "MINAGE",
20 | "maxage_column": "MAXAGE",
21 | "rocktype_column": "No_col",
22 | "alt_rocktype_column": "No_col",
23 | "sill_text": "sill",
24 | "intrusive_text": "granit",
25 | "volcanic_text": "volc",
26 | "objectid_column": "ID",
27 | "ignore_lithology_codes": ["cover"]
28 | },
29 | "fault": {
30 | "structtype_column": "TYPE",
31 | "fault_text": "ault",
32 | "dip_null_value": "0",
33 | "dipdir_flag": "num",
34 | "dipdir_column": "No_col",
35 | "dip_column": "No_col",
36 | "orientation_type": "dip direction",
37 | "dipestimate_column": "No_col",
38 | "dipestimate_text": "No_col",
39 | "name_column": "No_col",
40 | "objectid_column": "No_col"
41 | },
42 | "fold": {
43 | "structtype_column": "TYPE",
44 | "fold_text": "form",
45 | "description_column": "TYPE",
46 | "synform_text": "syn",
47 | "foldname_column": "NAME",
48 | "objectid_column": "No_col"
49 | }
50 | }
--------------------------------------------------------------------------------
/map2loop/_datasets/config_files/VIC.json:
--------------------------------------------------------------------------------
1 | {
2 | "structure": {
3 | "orientation_type": "dip direction",
4 | "dipdir_column": "azimuth",
5 | "dip_column": "inclinatn",
6 | "description_column": "DESCRIPTION",
7 | "bedding_text": "bed",
8 | "overturned_column": "no_col",
9 | "overturned_text": "blah",
10 | "objectid_column": "geographic",
11 | "desciption_column": "sub_type"
12 | },
13 | "geology": {
14 | "unitname_column": "formatted_",
15 | "alt_unitname_column": "abbreviate",
16 | "group_column": "no_col",
17 | "supergroup_column": "interpreta",
18 | "description_column": "text_descr",
19 | "minage_column": "no_col",
20 | "maxage_column": "no_col",
21 | "rocktype_column": "rank",
22 | "alt_rocktype_column": "type",
23 | "sill_text": "sill",
24 | "intrusive_text": "intrusion",
25 | "volcanic_text": "volc",
26 | "objectid_column": "ID",
27 | "ignore_lithology_codes": ["cover"]
28 | },
29 | "fault": {
30 | "structtype_column": "featuretyp",
31 | "fault_text": "s",
32 | "dip_null_value": "0",
33 | "dipdir_flag": "num",
34 | "dipdir_column": "no_col",
35 | "dip_column": "no_col",
36 | "orientation_type": "dip direction",
37 | "dipestimate_column": "no_col",
38 | "dipestimate_text": "no_col",
39 | "name_column": "no_col",
40 | "objectid_column": "geographic"
41 | },
42 | "fold": {
43 | "structtype_column": "featuretyp",
44 | "fold_text": "fold",
45 | "description_column": "no_col",
46 | "synform_text": "syn",
47 | "foldname_column": "NAME",
48 | "objectid_column": "geographic"
49 | }
50 | }
--------------------------------------------------------------------------------
/map2loop/_datasets/config_files/WA.json:
--------------------------------------------------------------------------------
1 | {
2 | "structure": {
3 | "orientation_type": "strike",
4 | "dipdir_column": "strike",
5 | "dip_column": "dip",
6 | "description_column": "feature",
7 | "bedding_text": "Bed",
8 | "overturned_column": "structypei",
9 | "overturned_text": "BEOI",
10 | "objectid_column": "objectid"
11 | },
12 | "geology": {
13 | "unitname_column": "unitname",
14 | "alt_unitname_column": "code",
15 | "group_column": "group_",
16 | "supergroup_column": "supersuite",
17 | "description_column": "descriptn",
18 | "minage_column": "min_age_ma",
19 | "maxage_column": "max_age_ma",
20 | "rocktype_column": "rocktype1",
21 | "alt_rocktype_column": "rocktype2",
22 | "sill_text": "is a sill",
23 | "intrusive_text": "intrusive",
24 | "volcanic_text": "volcanic",
25 | "objectid_column": "ID",
26 | "ignore_lithology_codes": ["cover"]
27 | },
28 | "fault": {
29 | "structtype_column": "feature",
30 | "fault_text": "Fault",
31 | "dip_null_value": "0",
32 | "dipdir_flag": "num",
33 | "dipdir_column": "dip_dir",
34 | "dip_column": "dip",
35 | "orientation_type": "dip direction",
36 | "dipestimate_column": "dip_est",
37 | "dipestimate_text": "gentle,moderate,steep",
38 | "name_column": "name",
39 | "objectid_column": "objectid"
40 | },
41 | "fold": {
42 | "structtype_column": "feature",
43 | "fold_text": "Fold axial trace",
44 | "description_column": "type",
45 | "synform_text": "syncline",
46 | "foldname_column": "NAME",
47 | "objectid_column": "objectid"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/map2loop/_datasets/geodata_files/hamersley/dtm_rp.tif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loop3D/map2loop/1af43e4621a837db02252c05ff8cee0d5d8b3dd0/map2loop/_datasets/geodata_files/hamersley/dtm_rp.tif
--------------------------------------------------------------------------------
/map2loop/_datasets/geodata_files/load_map2loop_data.py:
--------------------------------------------------------------------------------
1 | import geopandas
2 | from importlib.resources import files
3 | from osgeo import gdal
4 | gdal.UseExceptions()
5 |
6 | def load_hamersley_geology():
7 | """
8 | Loads Hamersley geology data from a shapefile
9 |
10 | Args:
11 | path (str):
12 | The path to the shapefile
13 |
14 | Returns:
15 | geopandas.GeoDataFrame: The geology data
16 | """
17 | stream = files("map2loop._datasets.geodata_files.hamersley").joinpath("geology.geojson")
18 | return geopandas.read_file(stream)
19 |
20 |
21 | def load_hamersley_structure():
22 | """
23 | Loads Hamersley structure data from a shapefile
24 |
25 | Args:
26 | path (str):
27 | The path to the shapefile
28 |
29 | Returns:
30 | geopandas.GeoDataFrame: The structure data
31 | """
32 |
33 | path = files("map2loop._datasets.geodata_files.hamersley").joinpath("structure.geojson")
34 | return geopandas.read_file(path)
35 |
36 |
37 | def load_hamersley_dtm():
38 | """
39 | Load DTM data from a raster file
40 |
41 | Returns:
42 | gdal.Dataset: The DTM data
43 | """
44 | path = files("map2loop._datasets.geodata_files.hamersley").joinpath("dtm_rp.tif")
45 | return gdal.Open(path)
46 |
--------------------------------------------------------------------------------
/map2loop/aus_state_urls.py:
--------------------------------------------------------------------------------
1 | import os
2 | import pathlib
3 |
4 | module_path = os.path.dirname(__file__)
5 |
6 |
7 | def load_clut(state):
8 | stream = (
9 | pathlib.Path(module_path)
10 | / pathlib.Path('_datasets')
11 | / pathlib.Path('clut_files')
12 | / pathlib.Path(f'{state}_clut.csv')
13 | )
14 | return stream
15 |
16 |
17 | def load_config(state):
18 | stream = (
19 | pathlib.Path(module_path)
20 | / pathlib.Path('_datasets')
21 | / pathlib.Path('config_files')
22 | / pathlib.Path(f'{state}.json')
23 | )
24 | return stream
25 |
26 |
27 | class AustraliaStateUrls:
28 | aus_geology_urls = {
29 | "WA": "http://13.211.217.129:8080/geoserver/loop/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=loop:500k_geol_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
30 | "QLD": "http://13.211.217.129:8080/geoserver/QLD/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=QLD:qld_geol_asud&bbox={BBOX_STR}&srsName=EPSG:28355&outputFormat=shape-zip",
31 | "SA": "http://13.211.217.129:8080/geoserver/SA/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=SA:2m_surface_geology_28354_relage&bbox={BBOX_STR}&srs=EPSG:28354&outputFormat=shape-zip",
32 | "VIC": "http://13.211.217.129:8080/geoserver/VIC/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=VIC:geolunit_250k_py_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
33 | "NSW": "http://13.211.217.129:8080/geoserver/NSW/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=NSW:ge_rockunit_lao2&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
34 | "ACT": "",
35 | "TAS": "http://13.211.217.129:8080/geoserver/TAS/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=TAS:geol_poly_250_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
36 | "NT": "",
37 | }
38 | aus_structure_urls = {
39 | "WA": "http://13.211.217.129:8080/geoserver/loop/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=loop:waroxi_wa_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
40 | "QLD": "http://13.211.217.129:8080/geoserver/QLD/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=QLD:outcrops_28355&bbox={BBOX_STR}&srsName=EPSG:28355&outputFormat=shape-zip",
41 | "SA": "http://13.211.217.129:8080/geoserver/SA/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=SA:sth_flinders_28354&bbox={BBOX_STR}&srs=EPSG:28354&outputFormat=shape-zip",
42 | "VIC": "http://13.211.217.129:8080/geoserver/VIC/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=VIC:struc_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
43 | "NSW": "http://13.211.217.129:8080/geoserver/NSW/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=NSW:lao_struct_pt&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
44 | "ACT": "",
45 | "TAS": "http://13.211.217.129:8080/geoserver/TAS/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=TAS:geol_struc_250_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
46 | "NT": "",
47 | }
48 | aus_fault_urls = {
49 | "WA": "http://13.211.217.129:8080/geoserver/loop/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=loop:500k_faults_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
50 | "QLD": "http://13.211.217.129:8080/geoserver/QLD/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=QLD:qld_faults_folds_28355&bbox={BBOX_STR}&srsName=EPSG:28355&outputFormat=shape-zip",
51 | "SA": "http://13.211.217.129:8080/geoserver/SA/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=SA:2m_linear_structures&bbox={BBOX_STR}&srs=EPSG:28354&outputFormat=shape-zip",
52 | "VIC": "http://13.211.217.129:8080/geoserver/VIC/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=VIC:geolstructure_250k_ln_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
53 | "NSW": "http://13.211.217.129:8080/geoserver/NSW/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=NSW:faults_joined_left_contains_drop_dups&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
54 | "ACT": "",
55 | "TAS": "http://13.211.217.129:8080/geoserver/TAS/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=TAS:geol_line_250_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
56 | "NT": "",
57 | }
58 | aus_fold_urls = {
59 | "WA": "http://13.211.217.129:8080/geoserver/loop/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=loop:500k_faults_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
60 | "QLD": "http://13.211.217.129:8080/geoserver/QLD/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=QLD:qld_faults_folds_28355&bbox={BBOX_STR}&srsName=EPSG:28355&outputFormat=shape-zip",
61 | "SA": "http://13.211.217.129:8080/geoserver/SA/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=SA:2m_linear_structures&bbox={BBOX_STR}&srs=EPSG:28354&outputFormat=shape-zip",
62 | "VIC": "http://13.211.217.129:8080/geoserver/VIC/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=VIC:geolstructure_250k_ln_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
63 | "NSW": "http://13.211.217.129:8080/geoserver/NSW/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=NSW:folds_lao&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
64 | "ACT": "",
65 | "TAS": "http://13.211.217.129:8080/geoserver/TAS/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=TAS:geol_line_250_28355&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
66 | "NT": "",
67 | }
68 | aus_mindep_loopdata = {
69 | "WA": "http://13.211.217.129:8080/geoserver/loop/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=loop:mindeps_2018_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
70 | "QLD": "http://13.211.217.129:8080/geoserver/QLD/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=qld_mindeps_28355&bbox={BBOX_STR}&srsName=EPSG:28355&outputFormat=shape-zip",
71 | "SA": "",
72 | "VIC": "http://13.211.217.129:8080/geoserver/VIC/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=VIC:mindeps_2018_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
73 | "NSW": "http://13.211.217.129:8080/geoserver/NSW/wfs?service=WFS&version=1.1.0&request=GetFeature&typeName=NSW:nsw_mindeps&bbox={BBOX_STR}&srs=EPSG:28355&outputFormat=shape-zip",
74 | "ACT": "",
75 | "TAS": "http://13.211.217.129:8080/geoserver/VIC/wfs?service=WFS&version=1.0.0&request=GetFeature&typeName=VIC:mindeps_2018_28350&bbox={BBOX_STR}&srs=EPSG:28350&outputFormat=shape-zip",
76 | "NT": "",
77 | }
78 | aus_config_urls = {
79 | "WA": load_config('WA'),
80 | "QLD": load_config('QLD'),
81 | "SA": load_config('SA'),
82 | "VIC": load_config('VIC'),
83 | "NSW": load_config('NSW'),
84 | "ACT": "",
85 | "TAS": load_config('TAS'),
86 | "NT": "",
87 | }
88 | aus_clut_urls = {
89 | "WA": load_clut('WA'),
90 | "QLD": load_clut('QLD'),
91 | "SA": load_clut('SA'),
92 | "VIC": "",
93 | "NSW": "",
94 | "ACT": "",
95 | "TAS": "",
96 | "NT": "",
97 | }
98 |
--------------------------------------------------------------------------------
/map2loop/config.py:
--------------------------------------------------------------------------------
1 | import urllib.error
2 | import beartype
3 | import json
4 | import urllib
5 | import time
6 | import pathlib
7 | from typing import Union
8 |
9 | from .logging import getLogger
10 |
11 | logger = getLogger(__name__)
12 |
13 |
14 | class Config:
15 | """
16 | A data structure containing column name mappings for files and keywords
17 |
18 | Attributes
19 | ----------
20 | structure_config: dict
21 | column names and keywords for structure mappings
22 | geology_config: dict
23 | column names and keywords for geology mappings
24 | fault_config: dict
25 | column names and keywords for fault mappings
26 | fold_config: dict
27 | column names and keywords for fold mappings
28 | """
29 |
30 | def __init__(self):
31 | self.structure_config = {
32 | "orientation_type": "dip direction",
33 | "dipdir_column": "DIPDIR",
34 | "dip_column": "DIP",
35 | "description_column": "DESCRIPTION",
36 | "bedding_text": "bedding",
37 | "overturned_column": "DESCRIPTION",
38 | "overturned_text": "overturned",
39 | "objectid_column": "ID",
40 | }
41 | self.geology_config = {
42 | "unitname_column": "UNITNAME",
43 | "alt_unitname_column": "CODE",
44 | "group_column": "GROUP",
45 | "supergroup_column": "SUPERGROUP",
46 | "description_column": "DESCRIPTION",
47 | "minage_column": "MIN_AGE",
48 | "maxage_column": "MAX_AGE",
49 | "rocktype_column": "ROCKTYPE1",
50 | "alt_rocktype_column": "ROCKTYPE2",
51 | "sill_text": "sill",
52 | "intrusive_text": "intrusive",
53 | "volcanic_text": "volcanic",
54 | "objectid_column": "ID",
55 | "ignore_lithology_codes": ["cover"],
56 | }
57 | self.fault_config = {
58 | "structtype_column": "FEATURE",
59 | "fault_text": "fault",
60 | "dip_null_value": "-999",
61 | "dipdir_flag": "num",
62 | "dipdir_column": "DIPDIR",
63 | "dip_column": "DIP",
64 | "orientation_type": "dip direction",
65 | "dipestimate_column": "DIP_ESTIMATE",
66 | "dipestimate_text": "'NORTH_EAST','NORTH',,'NOT ACCESSED'",
67 | "name_column": "NAME",
68 | "objectid_column": "ID",
69 | "minimum_fault_length": -1.0, # negative -1 means not set
70 | "ignore_fault_codes": [None],
71 | }
72 | self.fold_config = {
73 | "structtype_column": "FEATURE",
74 | "fold_text": "fold",
75 | "description_column": "DESCRIPTION",
76 | "synform_text": "syncline",
77 | "foldname_column": "NAME",
78 | "objectid_column": "ID",
79 | }
80 |
81 | def to_dict(self):
82 | """
83 | Convert the config dictionary to a dictionary
84 |
85 | Returns:
86 | dict: The dictionary representation of the config
87 | """
88 | return {
89 | "structure": self.structure_config,
90 | "geology": self.geology_config,
91 | "fault": self.fault_config,
92 | "fold": self.fold_config,
93 | }
94 |
95 | @beartype.beartype
96 | def update_from_dictionary(self, dictionary: dict, lower: bool = True):
97 | """
98 | Update the config dictionary from a provided dict
99 |
100 | Args:
101 | dictionary (dict): The dictionary to update from
102 | """
103 |
104 | if "structure" in dictionary:
105 | self.structure_config.update(dictionary["structure"])
106 | for key in dictionary["structure"].keys():
107 | if key not in self.structure_config:
108 | logger.warning(
109 | f"Config dictionary structure segment contained {key} which is not used"
110 | )
111 | dictionary.pop("structure")
112 |
113 | if "geology" in dictionary:
114 | self.geology_config.update(dictionary["geology"])
115 | for key in dictionary["geology"].keys():
116 | if key not in self.geology_config:
117 | logger.warning(
118 | f"Config dictionary geology segment contained {key} which is not used"
119 | )
120 | dictionary.pop("geology")
121 | if "fault" in dictionary:
122 | self.fault_config.update(dictionary["fault"])
123 | for key in dictionary["fault"].keys():
124 | if key not in self.fault_config:
125 | logger.warning(
126 | f"Config dictionary fault segment contained {key} which is not used"
127 | )
128 | dictionary.pop("fault")
129 | if "fold" in dictionary:
130 | self.fold_config.update(dictionary["fold"])
131 | for key in dictionary["fold"].keys():
132 | if key not in self.fold_config:
133 | logger.warning(
134 | f"Config dictionary fold segment contained {key} which is not used"
135 | )
136 | dictionary.pop("fold")
137 | if len(dictionary):
138 | logger.warning(f"Unused keys from config format {list(dictionary.keys())}")
139 |
140 |
141 | @beartype.beartype
142 | def update_from_file(
143 | self, filename: Union[pathlib.Path, str], lower: bool = False
144 | ):
145 | """
146 | Update the config dictionary from the provided json filename or url
147 |
148 | Args:
149 | filename (Union[pathlib.Path, str]): Filename or URL of the JSON config file
150 | lower (bool, optional): convert keys to lowercase. Defaults to False.
151 | """
152 | func = self.update_from_dictionary
153 |
154 | try:
155 | filename = str(filename)
156 |
157 | # if url, open the url
158 | if filename.startswith("http") or filename.startswith("ftp"):
159 | try_count = 5
160 | success = False
161 | # try 5 times to access the URL
162 | while try_count > 0 and not success:
163 | try:
164 | with urllib.request.urlopen(filename) as url_data:
165 | data = json.load(url_data)
166 | func(data, lower)
167 | success = True
168 |
169 | # case 1. handle url error
170 | except urllib.error.URLError as e:
171 | # wait 0.25 seconds before trying again
172 | time.sleep(0.25)
173 | # decrease the number of tries by 1
174 | try_count -= 1
175 | # if no more tries left, raise the error
176 | if try_count <= 0:
177 | raise urllib.error.URLError(
178 | f"Failed to access URL after multiple attempts: {filename}"
179 | ) from e
180 |
181 | # case 2. handle json error
182 | except json.JSONDecodeError as e:
183 | raise json.JSONDecodeError(
184 | f"Error decoding JSON data from URL: {filename}"
185 | ) from e
186 | else:
187 | try:
188 | with open(filename) as file_data:
189 | data = json.load(file_data)
190 | func(data, lower)
191 | except FileNotFoundError as e:
192 | err_string = f"The specified config file does not exist ({filename}).\n"
193 | err_string += (
194 | "Please check the file exists and is accessible, then try again.\n"
195 | )
196 | raise FileNotFoundError(err_string) from e
197 | except json.JSONDecodeError as e:
198 | raise json.JSONDecodeError(
199 | f"Error decoding JSON data from file: {filename}"
200 | ) from e
201 |
202 | except FileNotFoundError:
203 | raise
204 |
205 | except Exception:
206 | err_string = f"There is a problem parsing the config file ({filename}).\n"
207 | if filename.startswith("http"):
208 | err_string += "Please check the file is accessible online and then\n"
209 | else:
210 | err_string += "Please check the file exists and is accessible then\n"
211 | err_string += "Check the contents for mismatched quotes or brackets!"
212 | raise Exception(err_string)
--------------------------------------------------------------------------------
/map2loop/fault_orientation.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | import beartype
3 | import pandas
4 | import geopandas
5 | from .mapdata import MapData
6 | import numpy as np
7 |
8 | from .logging import getLogger
9 |
10 | logger = getLogger(__name__)
11 |
12 |
13 | class FaultOrientation(ABC):
14 | """
15 | Base Class of Fault Orientation assigner to force structure of FaultOrientation
16 |
17 | Args:
18 | ABC (ABC): Derived from Abstract Base Class
19 | """
20 |
21 | def __init__(self):
22 | """
23 | Initialiser of for orientation assigner
24 | """
25 | self.label = "FaultOrientationBaseClass"
26 |
27 | def type(self):
28 | """
29 | Getter for subclass type label
30 |
31 | Returns:
32 | str: Name of subclass
33 | """
34 | return self.label
35 |
36 | @beartype.beartype
37 | @abstractmethod
38 | def calculate(
39 | self,
40 | fault_trace: geopandas.GeoDataFrame,
41 | fault_orientations: pandas.DataFrame,
42 | map_data: MapData,
43 | ) -> pandas.DataFrame:
44 | """
45 | Execute fault orientation assigned method (abstract method)
46 |
47 | Args:
48 | faults (pandas.DataFrame): the data frame of the faults to add throw values to
49 | fault_orientation (pandas.DataFrame): data frame with fault orientations to assign to faults
50 | map_data (map2loop.MapData): a catchall so that access to all map data is available
51 |
52 | Returns:
53 | pandas.DataFrame: fault orientations assigned to a fault label
54 | """
55 | pass
56 |
57 |
58 | class FaultOrientationNearest(FaultOrientation):
59 | """
60 | FaultOrientation class which estimates fault orientation based on nearest orientation
61 | """
62 |
63 | def __init__(self):
64 | """
65 | Initialiser for nearest version of the fault orientation assigner
66 | """
67 | self.label = "FaultOrientationNearest"
68 |
69 | @beartype.beartype
70 | def calculate(
71 | self,
72 | fault_trace: geopandas.GeoDataFrame,
73 | fault_orientations: pandas.DataFrame,
74 | map_data: MapData,
75 | ) -> pandas.DataFrame:
76 | """
77 | Assigns the nearest fault orientation to a fault
78 |
79 | Args:
80 | fault_trace (geopandas.GeoDataFrame): the data frame of the fault traces
81 | fault_orientation (pandas.DataFrame): data frame with fault orientations to assign to faults
82 | map_data (map2loop.MapData): a catchall so that access to all map data is available
83 |
84 | Returns:
85 | pandas.DataFrame: fault orientations assigned to a fault label
86 | """
87 | logger.info("Assigning fault orientations to fault traces from nearest orientation")
88 | orientations = fault_orientations.copy()
89 | logger.info(f'There are {len(orientations)} fault orientations to assign')
90 |
91 | orientations["ID"] = -1
92 |
93 | for i in orientations.index:
94 | p = orientations.loc[i, :].geometry
95 | orientations.loc[i, "ID"] = fault_trace.loc[
96 | fault_trace.index[np.argmin(fault_trace.distance(p))], "ID"
97 | ]
98 | orientations.loc[i, "X"] = p.x
99 | orientations.loc[i, "Y"] = p.y
100 |
101 | return orientations.drop(columns="geometry")
102 |
--------------------------------------------------------------------------------
/map2loop/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import map2loop
3 |
4 |
5 | def get_levels():
6 | """dict for converting to logger levels from string
7 |
8 |
9 | Returns
10 | -------
11 | dict
12 | contains all strings with corresponding logging levels.
13 | """
14 | return {
15 | "info": logging.INFO,
16 | "warning": logging.WARNING,
17 | "error": logging.ERROR,
18 | "debug": logging.DEBUG,
19 | }
20 |
21 |
22 | def getLogger(name: str):
23 | """Get a logger object with a specific name
24 |
25 |
26 | Parameters
27 | ----------
28 | name : str
29 | name of the logger object
30 |
31 | Returns
32 | -------
33 | logging.Logger
34 | logger object
35 | """
36 | if name in map2loop.loggers:
37 | return map2loop.loggers[name]
38 | logger = logging.getLogger(name)
39 | logger.addHandler(map2loop.ch)
40 | logger.propagate = False
41 | map2loop.loggers[name] = logger
42 | return logger
43 |
44 |
45 | logger = getLogger(__name__)
46 |
47 |
48 | def set_level(level: str):
49 | """Set the level of the logging object
50 |
51 |
52 | Parameters
53 | ----------
54 | level : str
55 | level of the logging object
56 | """
57 | levels = get_levels()
58 | level = levels.get(level, logging.WARNING)
59 | map2loop.ch.setLevel(level)
60 |
61 | for name in map2loop.loggers:
62 | logger = logging.getLogger(name)
63 | logger.setLevel(level)
64 | logger.info(f"Logging level set to {level}")
65 |
--------------------------------------------------------------------------------
/map2loop/m2l_enums.py:
--------------------------------------------------------------------------------
1 | from enum import IntEnum
2 |
3 |
4 | class Datatype(IntEnum):
5 | GEOLOGY = 0
6 | STRUCTURE = 1
7 | FAULT = 2
8 | FOLD = 3
9 | DTM = 4
10 | FAULT_ORIENTATION = 5
11 |
12 |
13 | class Datastate(IntEnum):
14 | UNNAMED = 0
15 | UNLOADED = 1
16 | LOADED = 2
17 | REPROJECTED = 3
18 | CLIPPED = 4
19 | COMPLETE = 5
20 | ERRORED = 9
21 |
22 |
23 | class ErrorState(IntEnum):
24 | NONE = 0
25 | URLERROR = 1
26 | CONFIGERROR = 2
27 |
28 |
29 | class VerboseLevel(IntEnum):
30 | NONE = 0
31 | TEXTONLY = 1
32 | ALL = 2
33 |
--------------------------------------------------------------------------------
/map2loop/sampler.py:
--------------------------------------------------------------------------------
1 | # internal imports
2 | from .m2l_enums import Datatype
3 | from .mapdata import MapData
4 |
5 | # external imports
6 | from abc import ABC, abstractmethod
7 | import beartype
8 | import geopandas
9 | import pandas
10 | import shapely
11 | import numpy
12 | from typing import Optional
13 |
14 |
15 | class Sampler(ABC):
16 | """
17 | Base Class of Sampler used to force structure of Sampler
18 |
19 | Args:
20 | ABC (ABC): Derived from Abstract Base Class
21 | """
22 |
23 | def __init__(self):
24 | """
25 | Initialiser of for Sampler
26 | """
27 | self.sampler_label = "SamplerBaseClass"
28 |
29 | def type(self):
30 | """
31 | Getter for subclass type label
32 |
33 | Returns:
34 | str: Name of subclass
35 | """
36 | return self.sampler_label
37 |
38 | @beartype.beartype
39 | @abstractmethod
40 | def sample(
41 | self, spatial_data: geopandas.GeoDataFrame, map_data: Optional[MapData] = None
42 | ) -> pandas.DataFrame:
43 | """
44 | Execute sampling method (abstract method)
45 |
46 | Args:
47 | spatial_data (geopandas.GeoDataFrame): data frame to sample
48 |
49 | Returns:
50 | pandas.DataFrame: data frame containing samples
51 | """
52 | pass
53 |
54 |
55 | class SamplerDecimator(Sampler):
56 | """
57 | Decimator sampler class which decimates the geo data frame based on the decimation value
58 | ie. decimation = 10 means take every tenth point
59 | Note: This only works on data frames with lists of points with columns "X" and "Y"
60 | """
61 |
62 | @beartype.beartype
63 | def __init__(self, decimation: int = 1):
64 | """
65 | Initialiser for decimator sampler
66 |
67 | Args:
68 | decimation (int, optional): stride of the points to sample. Defaults to 1.
69 | """
70 | self.sampler_label = "SamplerDecimator"
71 | decimation = max(decimation, 1)
72 | self.decimation = decimation
73 |
74 | @beartype.beartype
75 | def sample(
76 | self, spatial_data: geopandas.GeoDataFrame, map_data: Optional[MapData] = None
77 | ) -> pandas.DataFrame:
78 | """
79 | Execute sample method takes full point data, samples the data and returns the decimated points
80 |
81 | Args:
82 | spatial_data (geopandas.GeoDataFrame): the data frame to sample
83 |
84 | Returns:
85 | pandas.DataFrame: the sampled data points
86 | """
87 | data = spatial_data.copy()
88 | data["X"] = data.geometry.x
89 | data["Y"] = data.geometry.y
90 | data["Z"] = map_data.get_value_from_raster_df(Datatype.DTM, data)["Z"]
91 | data["layerID"] = geopandas.sjoin(
92 | data, map_data.get_map_data(Datatype.GEOLOGY), how='left'
93 | )['index_right']
94 | data.reset_index(drop=True, inplace=True)
95 |
96 | return pandas.DataFrame(data[:: self.decimation].drop(columns="geometry"))
97 |
98 |
99 | class SamplerSpacing(Sampler):
100 | """
101 | Spacing based sampler which decimates the geo data frame based on the distance between points along a line or
102 | in the case of a polygon along the boundary of that polygon
103 | ie. spacing = 500 means take a sample every 500 metres
104 | Note: This only works on data frames that contain MultiPolgon, Polygon, MultiLineString and LineString geometry
105 | """
106 |
107 | @beartype.beartype
108 | def __init__(self, spacing: float = 50.0):
109 | """
110 | Initialiser for spacing sampler
111 |
112 | Args:
113 | spacing (float, optional): The distance between samples. Defaults to 50.0.
114 | """
115 | self.sampler_label = "SamplerSpacing"
116 | spacing = max(spacing, 1.0)
117 | self.spacing = spacing
118 |
119 | @beartype.beartype
120 | def sample(
121 | self, spatial_data: geopandas.GeoDataFrame, map_data: Optional[MapData] = None
122 | ) -> pandas.DataFrame:
123 | """
124 | Execute sample method takes full point data, samples the data and returns the sampled points
125 |
126 | Args:
127 | spatial_data (geopandas.GeoDataFrame): the data frame to sample (must contain column ["ID"])
128 |
129 | Returns:
130 | pandas.DataFrame: the sampled data points
131 | """
132 | schema = {"ID": str, "X": float, "Y": float, "featureId": str}
133 | df = pandas.DataFrame(columns=schema.keys()).astype(schema)
134 | for _, row in spatial_data.iterrows():
135 | if type(row.geometry) is shapely.geometry.multipolygon.MultiPolygon:
136 | targets = row.geometry.boundary.geoms
137 | elif type(row.geometry) is shapely.geometry.polygon.Polygon:
138 | targets = [row.geometry.boundary]
139 | elif type(row.geometry) is shapely.geometry.multilinestring.MultiLineString:
140 | targets = row.geometry.geoms
141 | elif type(row.geometry) is shapely.geometry.linestring.LineString:
142 | targets = [row.geometry]
143 | else:
144 | targets = []
145 |
146 | # For the main cases Polygon and LineString the list 'targets' has one element
147 | for a, target in enumerate(targets):
148 | df2 = pandas.DataFrame(columns=schema.keys()).astype(schema)
149 | distances = numpy.arange(0, target.length, self.spacing)[:-1]
150 | points = [target.interpolate(distance) for distance in distances]
151 | df2["X"] = [point.x for point in points]
152 | df2["Y"] = [point.y for point in points]
153 |
154 | # # account for holes//rings in polygons
155 | df2["featureId"] = str(a)
156 | # 1. check if line is "closed"
157 | if target.is_ring:
158 | target_polygon = shapely.geometry.Polygon(target)
159 | if target_polygon.exterior.is_ccw: # if counterclockwise --> hole
160 | for j, target2 in enumerate(targets):
161 | # skip if line or point
162 | if len(target2.coords) >= 2:
163 | continue
164 | # which poly is the hole in? assign featureId of the same poly
165 | t2_polygon = shapely.geometry.Polygon(target2)
166 | if target.within(t2_polygon): #
167 | df2['featureId'] = str(j)
168 |
169 | df2["ID"] = row["ID"] if "ID" in spatial_data.columns else 0
170 | df = df2 if len(df) == 0 else pandas.concat([df, df2])
171 |
172 | df.reset_index(drop=True, inplace=True)
173 | return df
174 |
--------------------------------------------------------------------------------
/map2loop/stratigraphic_column.py:
--------------------------------------------------------------------------------
1 | import pandas
2 | import numpy
3 | import geopandas
4 |
5 |
6 | class StratigraphicColumn:
7 | """
8 | A class containing all the fault and fold summaries and relationships
9 |
10 | Attributes
11 | ----------
12 | column: list
13 | List of stratigraphic units in time order
14 | groups: list
15 | List of stratigraphic groups in time order
16 | stratigraphicUnitColumns: numpy.dtype
17 | Column names and types for stratigraphic unit summary
18 | stratigraphicUnits: pandas.DataFrame
19 | The stratigraphic units
20 | lithologyUnitColumns: numpy.dtype
21 | Column names and types for lithology layer summary
22 | lithologyUnits: pandas.DataFrame
23 | The lithology units
24 |
25 | """
26 |
27 | def __init__(self):
28 | """
29 | The initialiser for the stratigraphic units. All attributes are defaulted
30 | """
31 | self.column = []
32 | self.groups = []
33 |
34 | # Create empty dataframes for units
35 | self.stratigraphicUnitColumns = numpy.dtype(
36 | [
37 | ("layerId", int),
38 | ("name", str),
39 | ("minAge", float),
40 | ("maxAge", float),
41 | ("group", str),
42 | ("supergroup", str),
43 | ("colour", str),
44 | ]
45 | )
46 | self.stratigraphicUnits = pandas.DataFrame(
47 | numpy.empty(0, dtype=self.stratigraphicUnitColumns)
48 | )
49 | self.stratigraphicUnits = self.stratigraphicUnits.set_index("name")
50 |
51 | self.lithologyUnitColumns = numpy.dtype(
52 | [
53 | ("layerId", int),
54 | ("name", str),
55 | ("minAge", float),
56 | ("maxAge", float),
57 | ("group", str),
58 | ("colour", str),
59 | ]
60 | )
61 | self.lithologyUnits = pandas.DataFrame(numpy.empty(0, dtype=self.lithologyUnitColumns))
62 | self.lithologyUnits = self.lithologyUnits.set_index("name")
63 |
64 | def findStratigraphicUnit(self, id):
65 | """
66 | Find the unit in the units list based on its layerId or name
67 |
68 | Args:
69 | id (int or str):
70 | The layerId or name to look for
71 |
72 | Returns:
73 | pandas.DataFrame: The sliced data frame containing the requested unit
74 | """
75 | if issubclass(type(id), int):
76 | return self.stratigraphicUnits[self.stratigraphicUnits["layerId"] == id]
77 | elif issubclass(type(id), str):
78 | return self.stratigraphicUnits[self.stratigraphicUnits["name"] == id]
79 | else:
80 | print("ERROR: Unknown identifier type used to find stratigraphic unit")
81 |
82 | def findLithologyUnit(self, id):
83 | """
84 | Find the lithology unit in the units list based on its layerId or name
85 |
86 | Args:
87 | id (int or str):
88 | The layerId or name to look for
89 |
90 | Returns:
91 | pandas.DataFrame: The sliced data frame containing the requested unit
92 | """
93 | if issubclass(type(id), int):
94 | return self.lithologyUnits[self.lithologyUnits["layerId"] == id]
95 | elif issubclass(type(id), str):
96 | return self.lithologyUnits[self.lithologyUnits["name"] == id]
97 | else:
98 | print("ERROR: Unknown identifier type used to find lithology unit")
99 |
100 | def addStratigraphicUnit(self, unit):
101 | """
102 | Add stratigraphic unit to the units list
103 |
104 | Args:
105 | fault (pandas.DataFrame or dict):
106 | The unit information to add
107 | """
108 | if issubclass(type(unit), pandas.DataFrame) or issubclass(type(unit), dict):
109 | if "name" in unit.keys():
110 | if unit["name"] in self.stratigraphicUnits.index:
111 | print("Replacing stratigraphic unit", unit["name"])
112 | self.stratigraphicUnits.loc[unit["name"]] = unit
113 | else:
114 | print("No name field in stratigraphic unit", unit)
115 | else:
116 | print("Cannot add unit to dataframe with type", type(unit))
117 |
118 | def addLithologyUnit(self, unit):
119 | """
120 | Add lithology unit to the units list
121 |
122 | Args:
123 | fault (pandas.DataFrame or dict):
124 | The unit information to add
125 | """
126 | if issubclass(type(unit), pandas.DataFrame) or issubclass(type(unit), dict):
127 | if "name" in unit.keys():
128 | if unit["name"] in self.lithologyUnits.index:
129 | print("Replacing lithology unit", unit["name"])
130 | self.lithologyUnits.loc[unit["name"]] = unit
131 | else:
132 | print("No name field in lithology unit", unit)
133 | else:
134 | print("Cannot add unit to dataframe with type", type(unit))
135 |
136 | def populate(self, geology_map_data: geopandas.GeoDataFrame):
137 | """
138 | Parse the geodataframe data into the stratigraphic units list
139 |
140 | Args:
141 | geology_map_data (geopandas.GeoDataFrame):
142 | The geodataframe with the unit data
143 | """
144 | if geology_map_data.shape[0] == 0:
145 | return
146 | geology_data = geology_map_data.copy()
147 | geology_data = geology_data.drop_duplicates(subset=["UNITNAME"])
148 | geology_data = geology_data.reset_index(drop=True)
149 | # geology_data = geology_data.dropna(subset=["UNITNAME"])
150 |
151 | self.stratigraphicUnits = pandas.DataFrame(
152 | numpy.empty(geology_data.shape[0], dtype=self.stratigraphicUnitColumns)
153 | )
154 | self.stratigraphicUnits["layerId"] = numpy.arange(geology_data.shape[0])
155 | self.stratigraphicUnits["name"] = geology_data["UNITNAME"]
156 | self.stratigraphicUnits["minAge"] = geology_data["MIN_AGE"]
157 | self.stratigraphicUnits["maxAge"] = geology_data["MAX_AGE"]
158 | self.stratigraphicUnits["group"] = geology_data["GROUP"]
159 | self.stratigraphicUnits["supergroup"] = geology_data["SUPERGROUP"]
160 | self.stratigraphicUnits["colour"] = "#000000"
161 | # self.stratigraphicUnits["indexInGroup"] = -1
162 |
163 | self.groups = list(self.stratigraphicUnits['group'].unique())
164 |
165 | def set_stratigraphic_unit_parameter_by_name(self, name: str, parameter: str, value):
166 | """
167 | Set a specific parameter on a specific stratigraphic unit
168 |
169 | Args:
170 | name (str): The name of the stratigraphic unit
171 | parameter (str): The colmn name of the parameters
172 | value (str or int or float): The value to set
173 | """
174 | self.stratigraphicUnits.iloc[self.stratigraphicUnits["name"] == name][parameter] = value
175 |
176 | def sort_from_relationship_list(self, relationshipList: list):
177 | """
178 | Sort the stratigraphic column based on the list of name
179 |
180 | Args:
181 | relationshipList (list):
182 | The order of the units by name
183 | """
184 | sorter = dict(zip(relationshipList, range(len(relationshipList))))
185 | self.stratigraphicUnits["stratigraphic_Order"] = self.stratigraphicUnits["name"].map(sorter)
186 | self.stratigraphicUnits.sort_values(["stratigraphic_Order"], inplace=True)
187 |
--------------------------------------------------------------------------------
/map2loop/throw_calculator.py:
--------------------------------------------------------------------------------
1 | from abc import ABC, abstractmethod
2 | import beartype
3 | import pandas
4 | import geopandas
5 | from .mapdata import MapData
6 |
7 |
8 | class ThrowCalculator(ABC):
9 | """
10 | Base Class of Throw Calculator used to force structure of ThrowCalculator
11 |
12 | Args:
13 | ABC (ABC): Derived from Abstract Base Class
14 | """
15 |
16 | def __init__(self):
17 | """
18 | Initialiser of for Sorter
19 | """
20 | self.throw_calculator_label = "ThrowCalculatorBaseClass"
21 |
22 | def type(self):
23 | """
24 | Getter for subclass type label
25 |
26 | Returns:
27 | str: Name of subclass
28 | """
29 | return self.throw_calculator_label
30 |
31 | @beartype.beartype
32 | @abstractmethod
33 | def compute(
34 | self,
35 | faults: pandas.DataFrame,
36 | stratigraphic_order: list,
37 | basal_contacts: geopandas.GeoDataFrame,
38 | map_data: MapData,
39 | ) -> pandas.DataFrame:
40 | """
41 | Execute throw calculator method (abstract method)
42 |
43 | Args:
44 | faults (pandas.DataFrame): the data frame of the faults to add throw values to
45 | stratigraphic_order (list): a list of unit names sorted from youngest to oldest
46 | basal_contacts (geopandas.GeoDataFrame): basal contact geo data with locations and unit names of the contacts (columns must contain ["ID","basal_unit","type","geometry"])
47 | map_data (map2loop.MapData): a catchall so that access to all map data is available
48 |
49 | Returns:
50 | pandas.DataFrame: fault data frame with throw values (avgDisplacement and avgDownthrowDir) filled in
51 | """
52 | pass
53 |
54 |
55 | class ThrowCalculatorAlpha(ThrowCalculator):
56 | """
57 | ThrowCalculator class which estimates fault throw values based on units, basal_contacts and stratigraphic order
58 | """
59 |
60 | def __init__(self):
61 | """
62 | Initialiser for alpha version of the throw calculator
63 | """
64 | self.throw_calculator_label = "ThrowCalculatorAlpha"
65 |
66 | @beartype.beartype
67 | def compute(
68 | self,
69 | faults: pandas.DataFrame,
70 | stratigraphic_order: list,
71 | basal_contacts: pandas.DataFrame,
72 | map_data: MapData,
73 | ) -> pandas.DataFrame:
74 | """
75 | Execute throw calculator method takes fault data, basal_contacts and stratigraphic order and attempts to estimate fault throw.
76 |
77 | Args:
78 | faults (pandas.DataFrame): the data frame of the faults to add throw values to
79 | stratigraphic_order (list): a list of unit names sorted from youngest to oldest
80 | basal_contacts (geopandas.GeoDataFrame): basal contact geo data with locations and unit names of the contacts (columns must contain ["ID","basal_unit","type","geometry"])
81 | map_data (map2loop.MapData): a catchall so that access to all map data is available
82 |
83 | Returns:
84 | pandas.DataFrame: fault data frame with throw values (avgDisplacement and avgDownthrowDir) filled in
85 | """
86 | # For each fault take the geometric join of all contact lines and that fault line
87 |
88 | # For each contact join take the length of that join as an approximation of the minimum throw of the fault
89 |
90 | # Take all the min throw approximations and set the largest one as the avgDisplacement
91 |
92 | # If a fault has no contact lines the maximum throw should be less than the thickness of the containing
93 | # unit (if we exclude map height changes and fault angle)
94 |
95 | # Set any remaining displacement values to default value
96 | faults["avgDisplacement"] = faults.apply(
97 | lambda row: 100 if row["avgDisplacement"] == -1 else row["avgDisplacement"], axis=1
98 | )
99 | return faults
100 |
--------------------------------------------------------------------------------
/map2loop/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "3.2.2"
2 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ['setuptools']
3 | build-backend = 'setuptools.build_meta'
4 | [project]
5 | name = 'map2loop'
6 | description = 'Generate 3D model data from 2D maps.'
7 | authors = [{name = 'Loop team'}]
8 | readme = 'README.md'
9 | requires-python = '>=3.8'
10 | keywords = [ "earth sciences",
11 | "geology",
12 | "3-D modelling",
13 | "structural geology",
14 | "uncertainty",]
15 | license = {text = 'MIT'}
16 | classifiers = [
17 | "Development Status :: 4 - Beta",
18 | "Intended Audience :: Science/Research",
19 | "Natural Language :: English",
20 | "Programming Language :: Python",
21 | "Programming Language :: Python :: 3",
22 | "License :: OSI Approved :: MIT License",
23 | "Operating System :: OS Independent",
24 | "Topic :: Scientific/Engineering",
25 | "Topic :: Scientific/Engineering :: GIS",
26 | ]
27 | dynamic = ['version','dependencies']
28 |
29 | [project.urls]
30 | Documentation = 'https://Loop3d.org/map2loop/'
31 | "Bug Tracker" = 'https://github.com/Loop3D/map2loop/issues'
32 | "Source Code" = 'https://github.com/Loop3D/map2loop'
33 |
34 | [tool.setuptools.dynamic]
35 | dependencies = { file = ["dependencies.txt"]}
36 | version = { attr = "map2loop.version.__version__" }
37 |
38 | [tool.setuptools.packages.find]
39 | include = ['map2loop', 'map2loop.*']
40 |
41 | [tool.setuptools.package-data]
42 | map2loop = [
43 | "dependencies.txt",
44 | "_datasets/geodata_files/hamersley/*"
45 | ]
46 |
47 | [tool.black]
48 | line-length = 100
49 | skip-string-normalization = true
50 | target-version = ['py39']
51 | exclude = '\.eggs|\.git|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist|node_modules'
52 | skip-magic-trailing-comma = true
53 |
54 | # Add an option to remove the trailing comma of separated lists
55 | # but it doesn't exist because python devs don't like beautiful code.
56 | # black specifically adds trailing commas due to git diff lengths
57 | # which is an insufficient reason to destroy beautiful code making
58 | # it look like an unfinished thought or an incomplete
59 | [tool.blackdoc]
60 | # From https://numpydoc.readthedocs.io/en/latest/format.html
61 | # The length of docstring lines should be kept to 75 characters to facilitate
62 | # reading the docstrings in text terminals.
63 | line-length = 75
64 |
65 | [tool.build_sphinx]
66 | source-dir = 'doc'
67 | build-dir = './doc/build'
68 | all_files = 1
69 |
70 | [tool.upload_sphinx]
71 | upload-dir = 'doc/build/html'
72 |
73 | [tool.pydocstyle]
74 | match = '(?!coverage).*.py'
75 | convention = "numpy"
76 | add-ignore = ["D404"]
77 |
78 | [tool.codespell]
79 | skip = '*.pyc,*.txt,*.gif,*.png,*.jpg,*.ply,*.vtk,*.vti,*.vtu,*.js,*.html,*.doctree,*.ttf,*.woff,*.woff2,*.eot,*.mp4,*.inv,*.pickle,*.ipynb,flycheck*,./.git/*,./.hypothesis/*,*.yml,doc/_build/*,./doc/images/*,./dist/*,*~,.hypothesis*,./doc/examples/*,*.mypy_cache/*,*cover,./tests/tinypages/_build/*,*/_autosummary/*'
80 | quiet-level = 3
81 |
82 |
83 | [tool.ruff]
84 | exclude = ['.git', 'pycache__', 'build', 'dist', 'doc/examples', 'doc/_build']
85 | line-length = 100
86 | indent-width = 4
87 | target-version = 'py39'
88 |
89 | [tool.ruff.lint]
90 | external = ["E131", "D102", "D105"]
91 | ignore = [
92 | # whitespace before ':'
93 | "E203",
94 | # line break before binary operator
95 | # "W503",
96 | # line length too long
97 | "E501",
98 | # do not assign a lambda expression, use a def
99 | "E731",
100 | # too many leading '#' for block comment
101 | "E266",
102 | # ambiguous variable name
103 | "E741",
104 | # module level import not at top of file
105 | "E402",
106 | # Quotes (temporary)
107 | "Q0",
108 | # bare excepts (temporary)
109 | # "B001", "E722",
110 | "E722",
111 | # we already check black
112 | # "BLK100",
113 | # 'from module import *' used; unable to detect undefined names
114 | "F403",
115 | ]
116 | fixable = ["ALL"]
117 | unfixable = []
118 | extend-select = ["B007", "B010", "C4", "F", "NPY", "PGH004", "RSE", "RUF100"]
119 |
120 | [tool.ruff.lint.flake8-comprehensions]
121 | allow-dict-calls-with-keyword-arguments = true
122 | [tool.ruff.lint.per-file-ignores]
123 | "__init__.py" = ["F401"]
124 |
--------------------------------------------------------------------------------
/release-please-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": {
3 | "map2loop": {
4 | "release-type": "python",
5 | "types": ["feat", "fix"],
6 | "bump-minor-pre-major": true,
7 | "bump-minor-pre-major-pattern": "feat",
8 | "bump-patch-for-minor-pre-major": true,
9 | "bump-patch-for-minor-pre-major-pattern": "fix",
10 | "include-v-in-tag": true
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools.command.sdist import sdist as _sdist
2 | import shutil
3 | from setuptools import setup, find_packages
4 | from pathlib import Path
5 |
6 | # Resolve the absolute path to the directory containing this file:
7 | package_root = Path(__file__).resolve().parent
8 |
9 | # Get the version from the version.py file
10 | version = {}
11 | version_file = package_root / "map2loop" / "version.py"
12 | with version_file.open() as fp:
13 | exec(fp.read(), version)
14 |
15 | # Read dependencies from dependencies.txt
16 | requirements_file = package_root / "dependencies.txt"
17 | with requirements_file.open("r") as f:
18 | install_requires = [line.strip() for line in f if line.strip()]
19 |
20 |
21 | class CustomSDist(_sdist):
22 |
23 | def make_release_tree(self, base_dir, files):
24 | # 1) Let the normal sdist process run first.
25 | super().make_release_tree(base_dir, files)
26 | map2loop_dir = Path(base_dir) / "map2loop"
27 |
28 | # 2) Specify which files to move from the root to map2loop/.
29 | top_level_files = ["dependencies.txt", "LICENSE", "README.md"]
30 |
31 | for filename in top_level_files:
32 | src = Path(base_dir) / filename
33 | dst = map2loop_dir / filename
34 |
35 | # If the source file exists in base_dir, move it to map2loop/.
36 | if src.exists():
37 | shutil.copy(str(src), str(dst))
38 |
39 | setup(
40 | name="map2loop",
41 | install_requires=install_requires,
42 | packages=find_packages(exclude=["tests", "tests.*"]),
43 | package_data={"": ['dependencies.txt']},
44 | include_package_data=True,
45 | license="MIT",
46 | cmdclass={
47 | "sdist": CustomSDist,
48 | },
49 | )
50 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Loop3D/map2loop/1af43e4621a837db02252c05ff8cee0d5d8b3dd0/tests/__init__.py
--------------------------------------------------------------------------------
/tests/data_checks/test_config.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from map2loop.data_checks import validate_config_dictionary
3 |
4 |
5 | @pytest.fixture
6 | def valid_config():
7 | return {
8 | "structure": {
9 | "orientation_type": "dip direction",
10 | "dipdir_column": "azimuth",
11 | "dip_column": "inclinatn",
12 | "description_column": "DESCRIPTION",
13 | "bedding_text": "bed",
14 | "overturned_column": "no_col",
15 | "overturned_text": "blah",
16 | "objectid_column": "geographic",
17 | "desciption_column": "sub_type"
18 | },
19 | "geology": {
20 | "unitname_column": "formatted_",
21 | "alt_unitname_column": "abbreviate",
22 | "group_column": "no_col",
23 | "supergroup_column": "interpreta",
24 | "description_column": "text_descr",
25 | "minage_column": "no_col",
26 | "maxage_column": "no_col",
27 | "rocktype_column": "rank",
28 | "alt_rocktype_column": "type",
29 | "sill_text": "sill",
30 | "intrusive_text": "intrusion",
31 | "volcanic_text": "volc",
32 | "objectid_column": "ID",
33 | "ignore_lithology_codes": ["cover"]
34 | },
35 | "fault": {
36 | "structtype_column": "featuretyp",
37 | "fault_text": "s",
38 | "dip_null_value": "0",
39 | "dipdir_flag": "num",
40 | "dipdir_column": "no_col",
41 | "dip_column": "no_col",
42 | "orientation_type": "dip direction",
43 | "dipestimate_column": "no_col",
44 | "dipestimate_text": "no_col",
45 | "name_column": "no_col",
46 | "objectid_column": "geographic",
47 | "minimum_fault_length": 100.0,
48 | "ignore_fault_codes": []
49 | },
50 | "fold": {
51 | "structtype_column": "featuretyp",
52 | "fold_text": "fold",
53 | "description_column": "no_col",
54 | "synform_text": "syn",
55 | "foldname_column": "NAME",
56 | "objectid_column": "geographic"
57 | }
58 | }
59 |
60 |
61 | def test_valid_config_no_errors(valid_config):
62 | # Should not raise any error
63 | validate_config_dictionary(valid_config)
64 |
65 |
66 | def test_missing_required_section(valid_config):
67 |
68 | config_missing_structure = dict(valid_config)
69 | del config_missing_structure["structure"] # remove required section
70 |
71 | with pytest.raises(ValueError) as exc_info:
72 | validate_config_dictionary(config_missing_structure)
73 | assert "Missing required section 'structure'" in str(exc_info.value)
74 |
75 |
76 | def test_missing_required_key(valid_config):
77 |
78 | config_missing_dip = dict(valid_config)
79 |
80 | del config_missing_dip["structure"]["dip_column"] # remove required key
81 |
82 | with pytest.raises(ValueError) as exc_info:
83 | validate_config_dictionary(config_missing_dip)
84 | assert "Missing required key 'dip_column' for 'structure'" in str(exc_info.value)
85 |
86 |
87 | def test_unrecognized_section(valid_config):
88 |
89 | config_extra_section = dict(valid_config)
90 | config_extra_section["random_section"] = {"random_key": "random_value"}
91 |
92 | with pytest.raises(ValueError) as exc_info:
93 | validate_config_dictionary(config_extra_section)
94 | assert "Unrecognized section 'random_section'" in str(exc_info.value)
95 |
96 |
97 | def test_unrecognized_key_in_section(valid_config):
98 |
99 | config_extra_key = dict(valid_config)
100 | config_extra_key["structure"]["random_key"] = "random_value"
101 |
102 | with pytest.raises(ValueError) as exc_info:
103 | validate_config_dictionary(config_extra_key)
104 | assert "Key 'random_key' is not an allowed key in the 'structure' section." in str(exc_info.value)
105 |
106 |
107 | def test_legacy_key_detected(valid_config):
108 |
109 | config_with_legacy = dict(valid_config)
110 | config_with_legacy["structure"]["otype"] = "legacy_value" # 'otype' --> legacy key
111 | with pytest.raises(ValueError) as exc_info:
112 | validate_config_dictionary(config_with_legacy)
113 | assert "Legacy key found in config - 'otype'" in str(exc_info.value)
114 |
115 |
116 | def test_minimum_fault_length_wrong_type(valid_config):
117 |
118 | config_wrong_mfl = dict(valid_config)
119 | config_wrong_mfl["fault"]["minimum_fault_length"] = "one_hundred" # invalid type
120 |
121 | with pytest.raises(ValueError) as exc_info:
122 | validate_config_dictionary(config_wrong_mfl)
123 | assert "minimum_fault_length must be a number" in str(exc_info.value)
124 |
125 |
126 | def test_minimum_fault_length_missing(valid_config):
127 | """
128 | Remove minimum_fault_length entirely. That should be fine (None -> no check).
129 | """
130 | config_no_mfl = dict(valid_config)
131 | del config_no_mfl["fault"]["minimum_fault_length"]
132 |
133 | # Should not raise any error, as it's optional
134 | validate_config_dictionary(config_no_mfl)
135 |
136 |
--------------------------------------------------------------------------------
/tests/data_checks/test_input_data_faults.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import geopandas as gpd
3 | import shapely.geometry
4 | from map2loop.mapdata import MapData
5 | from map2loop.m2l_enums import Datatype
6 | from map2loop.data_checks import check_fault_fields_validity
7 |
8 |
9 | @pytest.mark.parametrize(
10 | "fault_data, fault_config, expected_validity, expected_message",
11 | [
12 | # Valid data
13 | (
14 | {
15 | "geometry": [
16 | shapely.geometry.LineString([(0, 0), (1, 1)]),
17 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]]),
18 | ],
19 | "FEATURE": ["Fault A", "Fault B"],
20 | "ID": [1, 2],
21 | },
22 | {"structtype_column": "FEATURE", "fault_text": "Fault", "objectid_column": "ID"},
23 | False,
24 | "",
25 | ),
26 | # Invalid geometry
27 | (
28 | {
29 | "geometry": [
30 | shapely.geometry.LineString([(0, 0), (1, 1)]),
31 | shapely.geometry.Polygon(
32 | [(0, 0), (1, 1), (1, 0), (0, 1), (0, 0)]
33 | ), # Invalid geometry
34 | ],
35 | "FEATURE": ["Fault A", "Fault B"],
36 | "ID": [1, 2],
37 | },
38 | {"structtype_column": "FEATURE", "fault_text": "Fault", "objectid_column": "ID"},
39 | True,
40 | "Invalid geometry types found in datatype FAULT. All geometries must be LineString, MultiLineString.",
41 | ),
42 | # Non-string FEATURE column
43 | (
44 | {
45 | "geometry": [
46 | shapely.geometry.LineString([(0, 0), (1, 1)]),
47 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]]),
48 | ],
49 | "FEATURE": [5, 2],
50 | "ID": [1, 2],
51 | },
52 | {"structtype_column": "FEATURE", "fault_text": "Fault", "objectid_column": "ID"},
53 | True,
54 | "Datatype FAULT: Column 'FEATURE' (config key: 'structtype_column') contains non-string values. Please ensure all values in this column are strings.",
55 | ),
56 | # Invalid values in DIP estimate column
57 | (
58 | {
59 | "geometry": [
60 | shapely.geometry.LineString([(0, 0), (1, 1)]),
61 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]]),
62 | ],
63 | "FEATURE": ["Fault", "Fault"],
64 | "NAME": ["Zuleika", "Zuleika"],
65 | "ID": [1, 2],
66 | "DIP": [70, 50],
67 | "STRIKE": [150, None],
68 | "DEC": ["north_east", "southt"],
69 | },
70 | {
71 | "structtype_column": "FEATURE",
72 | "fault_text": "Fault",
73 | "objectid_column": "ID",
74 | "name_column": "NAME",
75 | "dip_column": "DIP",
76 | "dipdir_column": "STRIKE",
77 | "dip_estimate_column": "DEC",
78 | },
79 | True,
80 | "Datatype FAULT: Column 'DEC' contains invalid values. Allowed values: ['north_east', 'south_east', 'south_west', 'north_west', 'north', 'east', 'south', 'west'].",
81 | ),
82 | ],
83 | ids=[
84 | "Valid fault data",
85 | "Invalid geometry",
86 | "Non-string FEATURE column",
87 | "Invalid DIP estimate column",
88 | ],
89 | )
90 | def test_check_fault_fields_validity(fault_data, fault_config, expected_validity, expected_message):
91 | # Dynamically create the mock config for this test case
92 | class MockConfig:
93 | def __init__(self, config):
94 | self.fault_config = config
95 |
96 | # Create a GeoDataFrame
97 | fault_gdf = gpd.GeoDataFrame(fault_data, crs="EPSG:4326")
98 |
99 | # Instantiate the MapData class with the dynamic mock config and data
100 | map_data = MapData()
101 | map_data.config = MockConfig(fault_config)
102 | map_data.raw_data = [None] * len(Datatype.__dict__)
103 | map_data.raw_data[Datatype.FAULT] = fault_gdf
104 |
105 | # Test the check_fault_fields_validity function
106 | validity_check, message = check_fault_fields_validity(map_data)
107 | assert validity_check == expected_validity
108 | assert message == expected_message
109 |
--------------------------------------------------------------------------------
/tests/data_checks/test_input_data_fold.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import geopandas as gpd
3 | import shapely.geometry
4 | from map2loop.mapdata import MapData
5 | from map2loop.m2l_enums import Datatype
6 | from map2loop.data_checks import check_fold_fields_validity
7 |
8 | @pytest.mark.parametrize(
9 | "fold_data, fold_config, expected_validity, expected_message",
10 | [
11 | # Valid data
12 | (
13 | {
14 | "geometry": [
15 | shapely.geometry.LineString([(0, 0), (1, 1)]),
16 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]])
17 | ],
18 | "FEATURE": ["fold A", "fold B"],
19 | "ID": [1, 2],
20 | "description": ["desc1", "desc2"]
21 | },
22 | {"structtype_column": "FEATURE", "fold_text": "fold", "objectid_column": "ID", "description_column": "description"},
23 | False,
24 | ""
25 | ),
26 | # Missing geometry
27 | (
28 | {
29 | "geometry": [
30 | shapely.geometry.LineString([(0,0), (0,0)]), # Invalid type
31 | shapely.geometry.LineString([(0, 0), (1, 1)])
32 | ],
33 | "FEATURE": ["fold A", "fold B"],
34 | "ID": [1, 2],
35 | "description": ["desc1", "desc2"]
36 | },
37 | {"structtype_column": "FEATURE", "fold_text": "fold", "objectid_column": "ID", "description_column": "description"},
38 | True,
39 | "Invalid geometry types found in datatype FOLD. All geometries must be LineString, MultiLineString."
40 | ),
41 | # Non-string FEATURE column
42 | (
43 | {
44 | "geometry": [
45 | shapely.geometry.LineString([(0, 0), (1, 1)]),
46 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]])
47 | ],
48 | "FEATURE": [123, 456], # Invalid type
49 | "ID": [1, 2],
50 | "description": ["desc1", "desc2"]
51 | },
52 | {"structtype_column": "FEATURE", "fold_text": "fold", "objectid_column": "ID", "description_column": "description"},
53 | True,
54 | "Datatype FOLD: Column 'FEATURE' (config key: 'structtype_column') contains non-string values. Please ensure all values in this column are strings."
55 | ),
56 | # Missing ID column
57 | (
58 | {
59 | "geometry": [
60 | shapely.geometry.LineString([(0, 0), (1, 1)]),
61 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]])
62 | ],
63 | "FEATURE": ["fold A", "fold B"],
64 | "description": ["desc1", "desc2"]
65 | },
66 | {"structtype_column": "FEATURE", "fold_text": "fold", "objectid_column": "ID", "description_column": "description"},
67 | False,
68 | ""
69 | ),
70 | # Duplicate ID values
71 | (
72 | {
73 | "geometry": [
74 | shapely.geometry.LineString([(0, 0), (1, 1)]),
75 | shapely.geometry.MultiLineString([[(0, 0), (1, 1)], [(1, 1), (2, 2)]])
76 | ],
77 | "FEATURE": ["fold A", "fold B"],
78 | "ID": [1, 1], # Duplicate values
79 | "description": ["desc1", "desc2"]
80 | },
81 | {"structtype_column": "FEATURE", "fold_text": "fold", "objectid_column": "ID", "description_column": "description"},
82 | True,
83 | "Datatype FOLD: Column 'ID' (config key: 'objectid_column') contains duplicate values."
84 | ),
85 | ],
86 | ids=[
87 | "Valid fold data",
88 | "Invalid geometry",
89 | "Non-string FEATURE column",
90 | "Missing ID column",
91 | "Duplicate ID values"
92 | ]
93 | )
94 | def test_check_fold_fields_validity(fold_data, fold_config, expected_validity, expected_message):
95 | # Dynamically create the mock config for this test case
96 | class MockConfig:
97 | def __init__(self, config):
98 | self.fold_config = config
99 |
100 | # Create a GeoDataFrame
101 | fold_gdf = gpd.GeoDataFrame(fold_data, crs="EPSG:4326")
102 |
103 | # Instantiate the MapData class with the dynamic mock config and data
104 | map_data = MapData()
105 | map_data.config = MockConfig(fold_config)
106 | map_data.raw_data = [None] * len(Datatype.__dict__)
107 | map_data.raw_data[Datatype.FOLD] = fold_gdf
108 |
109 | # Test the check_fold_fields_validity function
110 | validity_check, message = check_fold_fields_validity(map_data)
111 | assert validity_check == expected_validity
112 | assert message == expected_message
113 |
--------------------------------------------------------------------------------
/tests/data_checks/test_input_data_geology.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import geopandas as gpd
3 | import shapely.geometry
4 | from map2loop.mapdata import MapData
5 | from map2loop.data_checks import check_geology_fields_validity
6 |
7 | # Datatype Enum
8 | class Datatype:
9 | GEOLOGY = 0
10 |
11 | # Config
12 | class MockConfig:
13 | def __init__(self):
14 | self.geology_config = {
15 | "unitname_column": "UNITNAME",
16 | "alt_unitname_column": "CODE",
17 | "group_column": "GROUP",
18 | "supergroup_column": "SUPERGROUP",
19 | "description_column": "DESCRIPTION",
20 | "rocktype_column": "ROCKTYPE1",
21 | "alt_rocktype_column": "ROCKTYPE2",
22 | "minage_column": "MIN_AGE",
23 | "maxage_column": "MAX_AGE",
24 | "objectid_column": "ID",
25 | "ignore_lithology_codes": [],
26 | }
27 |
28 | @pytest.mark.parametrize(
29 | "geology_data, expected_validity, expected_message",
30 | [
31 | # Valid data
32 | (
33 | {
34 | "geometry": [shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])],
35 | "UNITNAME": ["Sandstone"],
36 | "CODE": ["SST"],
37 | "GROUP": ["Sedimentary"],
38 | "SUPERGROUP": ["Mesozoic"],
39 | "DESCRIPTION": ["A type of sandstone"],
40 | "ROCKTYPE1": ["Clastic"],
41 | "ROCKTYPE2": ["Quartz"],
42 | "MIN_AGE": [150.0],
43 | "MAX_AGE": [200.0],
44 | "ID": [1],
45 | },
46 | False,
47 | "",
48 | ),
49 | # Invalid geometry
50 | (
51 | {
52 | "geometry": [shapely.geometry.Polygon([(0, 0), (1, 1), (1, 0), (0, 1), (0, 0)])],
53 | "UNITNAME": ["Sandstone"],
54 | "CODE": ["SST"],
55 | "GROUP": ["Sedimentary"],
56 | "SUPERGROUP": ["Mesozoic"],
57 | "DESCRIPTION": ["A type of sandstone"],
58 | "ROCKTYPE1": ["Clastic"],
59 | "ROCKTYPE2": ["Quartz"],
60 | "MIN_AGE": [150.0],
61 | "MAX_AGE": [200.0],
62 | "ID": [1],
63 | },
64 | False,
65 | "",
66 | ),
67 | # Missing required column
68 | (
69 | {
70 | "geometry": [shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])],
71 | "UNITNAME": ["Sandstone"],
72 | # "CODE": ["SST"], # Missing required column
73 | "GROUP": ["Sedimentary"],
74 | "SUPERGROUP": ["Mesozoic"],
75 | "DESCRIPTION": ["A type of sandstone"],
76 | "ROCKTYPE1": ["Clastic"],
77 | "ROCKTYPE2": ["Quartz"],
78 | "MIN_AGE": [150.0],
79 | "MAX_AGE": [200.0],
80 | "ID": [1],
81 | },
82 | True,
83 | "Datatype GEOLOGY: Required column with config key 'alt_unitname_column' (column: 'CODE') is missing from the data.",
84 | ),
85 | # Non-string value in required column
86 | (
87 | {
88 | "geometry": [shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])],
89 | "UNITNAME": ["Sandstone"],
90 | "CODE": [2], # Non-string value
91 | "GROUP": ["Sedimentary"],
92 | "SUPERGROUP": ["Mesozoic"],
93 | "DESCRIPTION": ["A type of sandstone"],
94 | "ROCKTYPE1": ["Clastic"],
95 | "ROCKTYPE2": ["Quartz"],
96 | "MIN_AGE": [150.0],
97 | "MAX_AGE": [200.0],
98 | "ID": [1],
99 | },
100 | True,
101 | "Datatype GEOLOGY: Column 'alt_unitname_column' (column: 'CODE') must contain only values.",
102 | ),
103 | # NaN or blank value in required column
104 | (
105 | {
106 | "geometry": [shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])],
107 | "UNITNAME": [""], # Blank value
108 | "CODE": ["SST"],
109 | "GROUP": ["Sedimentary"],
110 | "SUPERGROUP": ["Mesozoic"],
111 | "DESCRIPTION": ["A type of sandstone"],
112 | "ROCKTYPE1": ["Clastic"],
113 | "ROCKTYPE2": ["Quartz"],
114 | "MIN_AGE": [150.0],
115 | "MAX_AGE": [200.0],
116 | "ID": [1],
117 | },
118 | True,
119 | "Datatype GEOLOGY: Column 'unitname_column' (column: 'UNITNAME') contains blank (empty) values. Please ensure all values are populated.",
120 | ),
121 | # Duplicate ID values
122 | (
123 | {
124 | "geometry": [
125 | shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
126 | shapely.geometry.Polygon([(0, 0), (10, 0), (1, 1), (0, 10)]),
127 | ],
128 | "UNITNAME": ["fr", "df"],
129 | "CODE": ["SST", "FGH"],
130 | "GROUP": ["Sedimentary", "Ign"],
131 | "SUPERGROUP": ["Mesozoic", "Arc"],
132 | "DESCRIPTION": ["A", "B"],
133 | "ROCKTYPE1": ["A", "B"],
134 | "ROCKTYPE2": ["Quartz", "FDS"],
135 | "MIN_AGE": [150.0, 200],
136 | "MAX_AGE": [200.0, 250],
137 | "ID": [1, 1], # Duplicate ID
138 | },
139 | True,
140 | "Datatype GEOLOGY: Column 'ID' (config key: 'objectid_column') contains duplicate values.",
141 | ),
142 | # nan in id
143 | (
144 | {
145 | "geometry": [
146 | shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
147 | shapely.geometry.Polygon([(0, 0), (10, 0), (1, 1), (0, 10)]),
148 | ],
149 | "UNITNAME": ["fr", "df"],
150 | "CODE": ["SST", "FGH"],
151 | "GROUP": ["Sedimentary", "Ign"],
152 | "SUPERGROUP": ["Mesozoic", "Arc"],
153 | "DESCRIPTION": ["A", "B"],
154 | "ROCKTYPE1": ["A", "B"],
155 | "ROCKTYPE2": ["Quartz", "FDS"],
156 | "MIN_AGE": [150.0, 200],
157 | "MAX_AGE": [200.0, 250],
158 | "ID": [1, None],
159 | },
160 | True,
161 | "Datatype GEOLOGY: Column 'ID' (config key: 'objectid_column') contains non-numeric or NaN values. Please rectify the values, or remove this key from the config dictionary to let map2loop assign IDs.",
162 | ),
163 | # nan in unit name
164 | (
165 | {
166 | "geometry": [
167 | shapely.geometry.Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
168 | shapely.geometry.Polygon([(0, 0), (10, 0), (1, 1), (0, 10)]),
169 | ],
170 | "UNITNAME": ["fr", None],
171 | "CODE": ["SST", "FGH"],
172 | "GROUP": ["Sedimentary", "Ign"],
173 | "SUPERGROUP": ["Mesozoic", "Arc"],
174 | "DESCRIPTION": ["A", "B"],
175 | "ROCKTYPE1": ["A", "B"],
176 | "ROCKTYPE2": ["Quartz", "FDS"],
177 | "MIN_AGE": [150.0, 200],
178 | "MAX_AGE": [200.0, 250],
179 | "ID": [1, 1], # Duplicate ID
180 | },
181 | True,
182 | "Datatype GEOLOGY: Column 'unitname_column' (column: 'UNITNAME') must contain only values.",
183 | ),
184 | ],
185 | )
186 |
187 |
188 |
189 | def test_check_geology_fields_validity(geology_data, expected_validity, expected_message):
190 | # Create a GeoDataFrame
191 | geology_gdf = gpd.GeoDataFrame(geology_data, crs="EPSG:4326")
192 |
193 | # Instantiate the MapData class with the mock config and data
194 | map_data = MapData()
195 | map_data.config = MockConfig()
196 | map_data.raw_data = [None] * len(Datatype.__dict__)
197 | map_data.raw_data[Datatype.GEOLOGY] = geology_gdf
198 |
199 | # Test the check_geology_fields_validity function
200 | validity_check, message = check_geology_fields_validity(map_data)
201 |
202 | assert validity_check == expected_validity
203 | assert message == expected_message
--------------------------------------------------------------------------------
/tests/data_checks/test_input_data_structure.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import geopandas as gpd
3 | import shapely.geometry
4 | from map2loop.mapdata import MapData
5 | from map2loop.data_checks import check_structure_fields_validity
6 |
7 | # Datatype Enum
8 | class Datatype:
9 | STRUCTURE = 1
10 |
11 | # Config
12 | class MockConfig:
13 | def __init__(self):
14 | self.structure_config = {
15 | "dipdir_column": "DIPDIR",
16 | "dip_column": "DIP",
17 | "description_column": "DESCRIPTION",
18 | "overturned_column": "OVERTURNED",
19 | "objectid_column": "ID",
20 | }
21 |
22 |
23 | @pytest.mark.parametrize(
24 | "structure_data, expected_validity, expected_message",
25 | [
26 | # Valid data
27 | (
28 | {
29 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
30 | "DIPDIR": [45.0, 135.0],
31 | "DIP": [30.0, 45.0],
32 | "DESCRIPTION": ["Description1", "Description2"],
33 | "OVERTURNED": ["Yes", "No"],
34 | "ID": [1, 2],
35 | },
36 | False,
37 | "",
38 | ),
39 | # Invalid geometry
40 | (
41 | {
42 | "geometry": [
43 | shapely.geometry.Point(0, 0),
44 | shapely.geometry.Polygon(
45 | [(0, 0), (1, 1), (1, 0), (0, 1), (0, 0)]
46 | ), # Invalid geometry
47 | ],
48 | "DIPDIR": [45.0, 135.0],
49 | "DIP": [30.0, 45.0],
50 | "DESCRIPTION": ["Description1", "Description2"],
51 | "OVERTURNED": ["Yes", "No"],
52 | "ID": [1, 2],
53 | },
54 | True,
55 | "Invalid geometry types found in datatype STRUCTURE. All geometries must be Point, MultiPoint.",
56 | ),
57 | # Missing required column
58 | (
59 | {
60 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
61 | # "DIPDIR": [45.0, 135.0], # Missing required column
62 | "DIP": [30.0, 45.0],
63 | "DESCRIPTION": ["Description1", "Description2"],
64 | "OVERTURNED": ["Yes", "No"],
65 | "ID": [1, 2],
66 | },
67 | True,
68 | "Datatype STRUCTURE: Required column with config key 'dipdir_column' (column: 'DIPDIR') is missing from the data.",
69 | ),
70 | # Non-numeric value in numeric column
71 | (
72 | {
73 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
74 | "DIPDIR": ["A", "B"], # Non-numeric value
75 | "DIP": [30.0, 45.0],
76 | "DESCRIPTION": ["Description1", "Description2"],
77 | "OVERTURNED": ["Yes", "No"],
78 | "ID": [1, 2],
79 | },
80 | True,
81 | "Datatype STRUCTURE: Column 'dipdir_column' (column: 'DIPDIR') must contain only numeric values.",
82 | ),
83 | # NaN or blank value in required column
84 | (
85 | {
86 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
87 | "DIPDIR": [None, 3], # NaN value
88 | "DIP": [30.0, 45.0],
89 | "DESCRIPTION": ["Description1", "Description2"],
90 | "OVERTURNED": ["Yes", "No"],
91 | "ID": [1, 2],
92 | },
93 | True,
94 | "Datatype STRUCTURE: Column 'dipdir_column' (column: 'DIPDIR') contains null values. Please ensure all values are present.",
95 | ),
96 | # Duplicate ID column
97 | (
98 | {
99 | "geometry": [shapely.geometry.Point(0, 0), shapely.geometry.Point(1, 1)],
100 | "DIPDIR": [45.0, 135.0],
101 | "DIP": [30.0, 45.0],
102 | "DESCRIPTION": ["Description1", "Description2"],
103 | "OVERTURNED": ["Yes", "No"],
104 | "ID": [1, 1], # Duplicate ID
105 | },
106 | True,
107 | "Datatype STRUCTURE: Column 'ID' (config key: 'objectid_column') contains duplicate values.",
108 | ),
109 | ],
110 | )
111 | def test_check_structure_fields_validity(structure_data, expected_validity, expected_message):
112 | # Create a GeoDataFrame
113 | structure_gdf = gpd.GeoDataFrame(structure_data, crs="EPSG:4326")
114 |
115 | # Instantiate the MapData class with the mock config and data
116 | map_data = MapData()
117 | map_data.config = MockConfig()
118 | map_data.raw_data = [None] * len(Datatype.__dict__)
119 | map_data.raw_data[Datatype.STRUCTURE] = structure_gdf
120 |
121 | # Test the check_structure_fields_validity function
122 | validity_check, message = check_structure_fields_validity(map_data)
123 | assert validity_check == expected_validity
124 | assert message == expected_message
125 |
--------------------------------------------------------------------------------
/tests/mapdata/test_mapdata_assign_random_colours_to_units.py:
--------------------------------------------------------------------------------
1 | ### This file tests the function colour_units() in map2loop/mapdata.py
2 | ### Two main test cases are considered: cases in which there is clut file and cases in which there is no clut file
3 |
4 | import pandas as pd
5 | from map2loop.mapdata import MapData
6 |
7 |
8 | # are random colours being assigned to stratigraphic units in cases where no clut file is provided?
9 | def test_colour_units_no_clut_file():
10 | # Create a sample DataFrame with missing 'colour' values
11 | data = {"name": ["Unit1", "Unit2", "Unit3"], "colour": [None, None, None]}
12 | stratigraphic_units = pd.DataFrame(data)
13 |
14 | # Instantiate the class and call the method
15 | md = MapData()
16 | md.colour_filename = None # Ensure no file is used
17 | result = md.colour_units(stratigraphic_units)
18 |
19 | # check that there are no duplicates in the 'unit' column
20 | assert result['name'].is_unique, "colour_units() in mapdata.py producing duplicate units"
21 |
22 | # Check that the 'colour' column has been assigned random colors
23 | assert (
24 | len(result["colour"].dropna()) == 3
25 | ), "function MapData.colour_units not assigning the right len of random colours"
26 |
27 | # are the generated colours valid hex colours?
28 | colours = result["colour"]
29 |
30 | assert colours.apply(
31 | isinstance, args=(str,)
32 | ).all(), (
33 | "function MapData.colour_units without clut file not assigning random hex colours as str"
34 | )
35 | assert colours.str.startswith(
36 | "#"
37 | ).all(), "function MapData.colour_units without clut file not generating the right hex codes with # at the start"
38 | assert (
39 | colours.str.len().isin([7, 4]).all()
40 | ), "function MapData.colour_units without clut file not generating the right hex codes with 7 or 4 characters"
41 |
42 |
43 | def test_colour_units_with_colour_file(tmp_path):
44 | # Create a strati units df with missing 'colour' values
45 | data = {"name": ["Unit1", "Unit2", "Unit3"], "colour": [None, None, None]}
46 | stratigraphic_units = pd.DataFrame(data)
47 |
48 | # Create a temp clut file
49 | colour_data = {"UNITNAME": ["Unit1", "Unit2"], "colour": ["#112233", "#445566"]}
50 | colour_lookup_df = pd.DataFrame(colour_data)
51 | colour_filename = tmp_path / "colour_lookup.csv"
52 | colour_lookup_df.to_csv(colour_filename, index=False)
53 |
54 | # Instantiate the class and call the method
55 | md = MapData()
56 | md.colour_filename = str(colour_filename)
57 | result = md.colour_units(stratigraphic_units)
58 |
59 | # check that there are no duplicates in the 'unit' column
60 | assert result['name'].is_unique, "colour_units() in mapdata.py producing duplicate units"
61 |
62 | # Check that the 'colour' column has been merged correctly and missing colors are assigned
63 | expected_colors = ["#112233", "#445566"]
64 | assert (
65 | result["colour"].iloc[0] == expected_colors[0]
66 | ), "function MapData.colour_units with clut file not assigning the right colour from the lookup file"
67 | assert (
68 | result["colour"].iloc[1] == expected_colors[1]
69 | ), "function MapData.colour_units with clut file not assigning the right colour from the lookup file"
70 | assert isinstance(
71 | result["colour"].iloc[2], str
72 | ), "function MapData.colour_units with clut file not assigning random hex colours as str"
73 | assert (
74 | result["colour"].iloc[2].startswith("#")
75 | ), "function MapData.colour_units with clut file not generating the right hex codes with # at the start"
76 | assert len(result["colour"].iloc[2]) in {
77 | 7,
78 | 4,
79 | }, "function MapData.colour_units with clut file not generating the right hex codes with 7 or 4 characters"
80 |
--------------------------------------------------------------------------------
/tests/mapdata/test_mapdata_dipdir.py:
--------------------------------------------------------------------------------
1 | ### This file tests the function parse_structure_map() in map2loop/mapdata.py
2 | ### at the moment only tests for DIPDIR values lower than 360 degrees
3 | ### TODO: add more tests for this function
4 |
5 | import pytest
6 | import geopandas
7 | import shapely
8 | from map2loop.mapdata import MapData
9 | from map2loop.m2l_enums import Datatype
10 |
11 |
12 | def test_if_m2l_returns_all_sampled_structures_with_DIPDIR_lower_than_360():
13 |
14 | # call the class
15 | md = MapData()
16 |
17 | # add config definition
18 | md.config.structure_config = {
19 | "dipdir_column": "DIPDIR",
20 | "dip_column": "DIP",
21 | "description_column": "DESCRIPTION",
22 | "bedding_text": "Bedding",
23 | "objectid_column": "ID",
24 | "overturned_column": "facing",
25 | "overturned_text": "DOWN",
26 | "orientation_type": "strike",
27 | }
28 |
29 | # create mock data
30 | data = {
31 | 'geometry': [shapely.Point(1, 1), shapely.Point(2, 2), shapely.Point(3, 3)],
32 | 'DIPDIR': [45.0, 370.0, 420.0],
33 | 'DIP': [30.0, 60.0, 50],
34 | 'OVERTURNED': ["False", "True", "True"],
35 | 'DESCRIPTION': ["Bedding", "Bedding", "Bedidng"],
36 | 'ID': [1, 2, 3],
37 | }
38 |
39 | # build geodataframe to hold the data
40 | data = geopandas.GeoDataFrame(data)
41 |
42 | # set it as the raw_data
43 | md.raw_data[Datatype.STRUCTURE] = data
44 |
45 | # make it parse the structure map and raise exception if error in parse_structure_map
46 |
47 | try:
48 | md.parse_structure_map()
49 | except Exception as e:
50 | pytest.fail(f"parse_structure_map raised an exception: {e}")
51 |
52 | # check if all values below 360
53 | assert (
54 | md.data[Datatype.STRUCTURE]['DIPDIR'].all() < 360
55 | ), "MapData.STRUCTURE is producing DIPDIRs > 360 degrees"
56 |
57 |
58 | #
59 |
--------------------------------------------------------------------------------
/tests/mapdata/test_minimum_fault_length.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import geopandas as gpd
3 | import shapely
4 | import numpy
5 |
6 | from map2loop.mapdata import MapData
7 | from map2loop.m2l_enums import VerboseLevel, Datatype
8 |
9 |
10 | @pytest.fixture
11 | def setup_map_data():
12 | # Create a mock MapData object with verbose level set to ALL
13 | map_data = MapData(verbose_level=VerboseLevel.ALL)
14 |
15 | # Simulate config with no minimum_fault_length set
16 | map_data.config.fault_config['minimum_fault_length'] = -1.0
17 |
18 | return map_data
19 |
20 |
21 | # for cases when there is no minimum_fault_length set in the config by user - does it update from map?
22 | def test_update_minimum_fault_length_from_faults(setup_map_data):
23 | map_data = setup_map_data
24 |
25 | # Define a test bounding box (in meters)
26 | bounding_box = {
27 | "minx": 0,
28 | "miny": 0,
29 | "maxx": 10000, # width = 10,000 meters
30 | "maxy": 10000, # height = 10,000 meters
31 | }
32 |
33 | # Set the bounding box in the map data
34 | map_data.set_bounding_box(bounding_box)
35 |
36 | # update config
37 | map_data.config.fault_config['name_column'] = 'NAME'
38 | map_data.config.fault_config['dip_column'] = 'DIP'
39 |
40 | # Define a dummy fault GeoDataFrame with faults of varying lengths
41 | faults = gpd.GeoDataFrame(
42 | {
43 | 'geometry': [
44 | shapely.geometry.LineString(
45 | [(0, 0), (50, 50)]
46 | ), # Fault 1 (small, length ~70.7 meters)
47 | shapely.geometry.LineString(
48 | [(0, 0), (3000, 3000)]
49 | ), # Fault 2 (length ~4242 meters)
50 | shapely.geometry.LineString(
51 | [(0, 0), (7000, 7000)]
52 | ), # Fault 3 (length ~9899 meters)
53 | ],
54 | 'NAME': ['Fault_1', 'Fault_2', 'Fault_3'],
55 | 'DIP': [60, 45, 30],
56 | }
57 | )
58 |
59 | faults.crs = "EPSG: 7850"
60 |
61 | # get the cropped fault dataset from parse_fault_map
62 | map_data.raw_data[Datatype.FAULT] = faults
63 | map_data.parse_fault_map()
64 | cropped_faults = map_data.data[Datatype.FAULT]
65 |
66 | # calculate 5% length of the bounding box area
67 | expected_minimum_fault_length = numpy.sqrt(
68 | 0.05
69 | * (bounding_box['maxx'] - bounding_box['minx'])
70 | * (bounding_box['maxy'] - bounding_box['miny'])
71 | )
72 |
73 | # Verify that the minimum_fault_length was calculated correctly
74 | assert map_data.minimum_fault_length == pytest.approx(expected_minimum_fault_length, rel=1e-3)
75 |
76 | # There should only be two faults remaining (the second and third ones)
77 | assert len(cropped_faults) == 2
78 |
79 | # Ensure that the remaining faults are the correct ones
80 | remaining_lengths = cropped_faults.geometry.length
81 | assert all(remaining_lengths >= expected_minimum_fault_length)
82 | assert cropped_faults.geometry.equals(
83 | faults.iloc[1:]['geometry']
84 | ) # Faults 2 and 3 geometries should be the same in the faults raw and faults cropped
85 |
86 |
87 | # are faults with length less than minimum_fault_length removed from the dataset?
88 | def test_cropping_faults_by_minimum_fault_length(setup_map_data):
89 | map_data = setup_map_data
90 |
91 | # Set minimum_fault_length in the config to 10
92 | map_data.config.fault_config['minimum_fault_length'] = 10.0
93 |
94 | map_data.config.fault_config['name_column'] = 'NAME'
95 | map_data.config.fault_config['dip_column'] = 'DIP'
96 |
97 | # Create a mock faults dataset with lengths < 10 and > 10
98 | faults = gpd.GeoDataFrame(
99 | {
100 | 'geometry': [
101 | shapely.geometry.LineString([(0, 0), (2, 2)]), # Length ~2.83 (should be cropped)
102 | shapely.geometry.LineString([(0, 0), (5, 5)]), # Length ~7.07 (should be cropped)
103 | shapely.geometry.LineString([(0, 0), (10, 10)]), # Length ~14.14 (should remain)
104 | ],
105 | 'NAME': ['Fault_1', 'Fault_2', 'Fault_3'],
106 | 'DIP': [60, 45, 30],
107 | 'DIPDIR': [90, 120, 150],
108 | }
109 | )
110 |
111 | # Set the raw data in the map_data object to simulate loaded fault data
112 | map_data.raw_data[Datatype.FAULT] = faults
113 | map_data.parse_fault_map()
114 | cropped_faults = map_data.data[Datatype.FAULT]
115 |
116 | # Assert that only 1 fault remains (the one with length ~14.14)
117 | assert (
118 | len(cropped_faults) == 1
119 | ), f"Expected only 1 fault remaining after cropping, but found {len(cropped_faults)}"
120 |
121 | # Optionally, check that the remaining fault has the correct geometry (the long one)
122 | assert cropped_faults.iloc[0].geometry.length == pytest.approx(
123 | 14.14, 0.01
124 | ), f"Expected remaining fault length to be 14.14, but got {cropped_faults.iloc[0].geometry.length}"
125 |
--------------------------------------------------------------------------------
/tests/mapdata/test_set_get_recreate_bounding_box.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from map2loop.mapdata import MapData
3 | import geopandas
4 | import shapely
5 |
6 |
7 | @pytest.fixture
8 | def md():
9 | return MapData()
10 |
11 |
12 | def test_set_bounding_box_with_tuple(md):
13 | bounding_box = (0, 10, 0, 10)
14 | md.set_bounding_box(bounding_box)
15 |
16 | assert md.bounding_box == {
17 | "minx": 0,
18 | "maxx": 10,
19 | "miny": 0,
20 | "maxy": 10,
21 | "top": 0,
22 | "base": 2000,
23 | }, "MapData.set_bounding_box() not working as expected"
24 |
25 |
26 | def test_set_bounding_box_with_dict(md):
27 | bounding_box = {"minx": 0, "maxx": 10, "miny": 0, "maxy": 10, "top": 5, "base": 15}
28 | md.set_bounding_box(bounding_box)
29 |
30 | assert md.bounding_box == bounding_box, "MapData.set_bounding_box() not working as expected"
31 |
32 |
33 | def test_bounding_box_keys(md):
34 | bounding_box = (0, 10, 0, 10)
35 | md.set_bounding_box(bounding_box)
36 |
37 | for key in ["minx", "maxx", "miny", "maxy", "top", "base"]:
38 | assert key in md.bounding_box, f"MapData.bounding_box missing key: {key}"
39 |
40 |
41 | def test_bounding_box_polygon(md):
42 | bounding_box = (0, 10, 0, 10)
43 | md.set_bounding_box(bounding_box)
44 |
45 | minx, miny, maxx, maxy = 0, 0, 10, 10
46 | lat_point_list = [miny, miny, maxy, maxy, miny]
47 | lon_point_list = [minx, maxx, maxx, minx, minx]
48 | expected_polygon = geopandas.GeoDataFrame(
49 | index=[0],
50 | crs=md.working_projection,
51 | geometry=[shapely.Polygon(zip(lon_point_list, lat_point_list))],
52 | )
53 |
54 | assert md.bounding_box_polygon.equals(
55 | expected_polygon
56 | ), "MapData.bounding_box_polygon not returning the correct GeoDataFrame"
57 |
58 |
59 | def test_get_bounding_box_as_dict(md):
60 | bounding_box = {"minx": 0, "maxx": 10, "miny": 0, "maxy": 10, "top": 5, "base": 15}
61 | md.set_bounding_box(bounding_box)
62 | result = md.get_bounding_box()
63 |
64 | assert (
65 | result == bounding_box
66 | ), "MapData.get_bounding_box() not returning the correct bounding box"
67 |
68 |
69 | def test_get_bounding_box_as_polygon(md):
70 | bounding_box = (0, 10, 0, 10)
71 | md.set_bounding_box(bounding_box)
72 | result = md.get_bounding_box(polygon=True)
73 |
74 | assert isinstance(
75 | result, geopandas.GeoDataFrame
76 | ), "MapData.get_bounding_box(polygon=True) not returning a GeoDataFrame"
77 | assert result.equals(
78 | md.bounding_box_polygon
79 | ), "MapData.get_bounding_box(polygon=True) not returning the correct GeoDataFrame"
80 |
81 |
82 | def test_recreate_bounding_box_str(md):
83 | bounding_box = (0, 10, 0, 10)
84 | md.set_working_projection("EPSG:4326")
85 | md.set_bounding_box(bounding_box)
86 | md.recreate_bounding_box_str()
87 |
88 | expected_str = "0,0,10,10,EPSG:4326"
89 | assert (
90 | md.bounding_box_str == expected_str
91 | ), "MapData.recreate_bounding_box_str() not working as expected"
92 |
93 |
94 | def test_set_bounding_box_with_missing_keys(md):
95 | bounding_box = {
96 | "minx": 0,
97 | "maxx": 10,
98 | "miny": 0
99 | # Missing "maxy", "top", "base"
100 | }
101 | with pytest.raises(KeyError):
102 | md.set_bounding_box(
103 | bounding_box
104 | ), "MapData.set_bounding_box accepting wrong argument, but should raise KeyError"
105 |
--------------------------------------------------------------------------------
/tests/mapdata/test_set_get_working_projection.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from map2loop.mapdata import MapData
3 |
4 |
5 | @pytest.mark.parametrize(
6 | "projection, expected_projection, bounding_box, expected_warning",
7 | [
8 | (4326, "EPSG:4326", None, None), # happy path with int projection
9 | ("EPSG:3857", "EPSG:3857", None, None), # happy path with str projection
10 | (9999, "EPSG:9999", None, None), # edge case with high int projection
11 | ("EPSG:9999", "EPSG:9999", None, None), # edge case with high str projection
12 | (None, None, None, True), # error case with None
13 | ([], None, None, True), # error case with list
14 | ({}, None, None, True), # error case with dict
15 | ],
16 | ids=[
17 | "int_projection",
18 | "str_projection",
19 | "high_int_projection",
20 | "high_str_projection",
21 | "none_projection",
22 | "list_projection",
23 | "dict_projection",
24 | ],
25 | )
26 | def test_set_working_projection(projection, expected_projection, bounding_box, expected_warning):
27 | # Set up MapData object
28 | map_data = MapData()
29 | map_data.bounding_box = bounding_box
30 |
31 | # Call the method
32 | map_data.set_working_projection(projection)
33 |
34 | # Assert the working projection is correctly set
35 | assert map_data.working_projection == expected_projection, (
36 | f"Expected working_projection to be {expected_projection}, but got {map_data.working_projection}"
37 | )
38 |
39 | # Check for the presence of warnings via side effects (if applicable)
40 | if expected_warning:
41 | assert map_data.working_projection is None, (
42 | "Expected working_projection to remain None when an invalid projection is provided"
43 | )
44 |
--------------------------------------------------------------------------------
/tests/project/test_config_arguments.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import pathlib
3 | from map2loop.project import Project
4 | import map2loop
5 |
6 | # ------------------------------------------------------------------------------
7 | # Common fixtures or helper data (bounding box, minimal filenames, etc.)
8 | # ------------------------------------------------------------------------------
9 |
10 | @pytest.fixture
11 | def minimal_bounding_box():
12 | return {
13 | "minx": 515687.31005864,
14 | "miny": 7493446.76593407,
15 | "maxx": 562666.860106543,
16 | "maxy": 7521273.57407786,
17 | "base": -3200,
18 | "top": 3000,
19 | }
20 |
21 | @pytest.fixture
22 | def geology_file():
23 | return str(
24 | pathlib.Path(map2loop.__file__).parent
25 | / pathlib.Path('_datasets/geodata_files/hamersley/geology.geojson')
26 | )
27 |
28 | @pytest.fixture
29 | def structure_file():
30 | return str(
31 | pathlib.Path(map2loop.__file__).parent
32 | / pathlib.Path('_datasets/geodata_files/hamersley/structure.geojson')
33 | )
34 |
35 | @pytest.fixture
36 | def dtm_file():
37 | return str(
38 | pathlib.Path(map2loop.__file__).parent
39 | / pathlib.Path('_datasets/geodata_files/hamersley/dtm_rp.tif')
40 | )
41 |
42 | @pytest.fixture
43 | def valid_config_dictionary():
44 | """
45 | A valid config dictionary that meets the 'structure' and 'geology' requirements
46 | """
47 | return {
48 | "structure": {
49 | "dipdir_column": "azimuth2",
50 | "dip_column": "dip"
51 | },
52 | "geology": {
53 | "unitname_column": "unitname",
54 | "alt_unitname_column": "code",
55 | }
56 | }
57 |
58 |
59 |
60 | # 1) config_filename and config_dictionary both present should raise ValueError
61 | def test_config_filename_and_dictionary_raises_error(
62 | minimal_bounding_box, geology_file, dtm_file, structure_file, valid_config_dictionary
63 | ):
64 |
65 | with pytest.raises(ValueError, match="Both 'config_filename' and 'config_dictionary' were provided"):
66 | Project(
67 | bounding_box=minimal_bounding_box,
68 | working_projection="EPSG:28350",
69 | geology_filename=geology_file,
70 | dtm_filename=dtm_file,
71 | structure_filename=structure_file,
72 | config_filename="dummy_config.json",
73 | config_dictionary=valid_config_dictionary,
74 | )
75 |
76 | # 2) No config_filename or config_dictionary should raise ValueError
77 | def test_no_config_provided_raises_error(
78 | minimal_bounding_box, geology_file, dtm_file, structure_file
79 | ):
80 |
81 | with pytest.raises(ValueError, match="A config file is required to run map2loop"):
82 | Project(
83 | bounding_box=minimal_bounding_box,
84 | working_projection="EPSG:28350",
85 | geology_filename=geology_file,
86 | dtm_filename=dtm_file,
87 | structure_filename=structure_file,
88 | )
89 |
90 | # 3) Passing an unexpected argument should raise TypeError
91 | def test_unexpected_argument_raises_error(
92 | minimal_bounding_box, geology_file, dtm_file, structure_file, valid_config_dictionary
93 | ):
94 |
95 | with pytest.raises(TypeError, match="unexpected keyword argument 'config_file'"):
96 | Project(
97 | bounding_box=minimal_bounding_box,
98 | working_projection="EPSG:28350",
99 | geology_filename=geology_file,
100 | dtm_filename=dtm_file,
101 | structure_filename=structure_file,
102 | config_dictionary=valid_config_dictionary,
103 | config_file="wrong_kwarg.json",
104 | )
105 |
106 | # 4) Dictionary missing a required key should raise ValueError
107 |
108 | def test_dictionary_missing_required_key_raises_error(
109 | minimal_bounding_box, geology_file, dtm_file, structure_file
110 | ):
111 |
112 | invalid_dictionary = {
113 | "structure": {"dipdir_column": "azimuth2", "dip_column": "dip"},
114 | "geology": {"unitname_column": "unitname"} # alt_unitname_column missing
115 | }
116 |
117 | with pytest.raises(ValueError, match="Missing required key 'alt_unitname_column' for 'geology'"):
118 | Project(
119 | bounding_box=minimal_bounding_box,
120 | working_projection="EPSG:28350",
121 | geology_filename=geology_file,
122 | dtm_filename=dtm_file,
123 | structure_filename=structure_file,
124 | config_dictionary=invalid_dictionary,
125 | )
126 |
127 | # 5) All good => The Project should be created without errors
128 | def test_good_config_runs_successfully(
129 | minimal_bounding_box, geology_file, dtm_file, structure_file, valid_config_dictionary
130 | ):
131 | project = None
132 | try:
133 | project = Project(
134 | bounding_box=minimal_bounding_box,
135 | working_projection="EPSG:28350",
136 | geology_filename=geology_file,
137 | dtm_filename=dtm_file,
138 | structure_filename=structure_file,
139 | config_dictionary=valid_config_dictionary,
140 | )
141 | except Exception as e:
142 | pytest.fail(f"Project initialization raised an unexpected exception: {e}")
143 |
144 | assert project is not None, "Project was not created."
145 | assert project.map_data.config.structure_config["dipdir_column"] == "azimuth2"
146 | assert project.map_data.config.structure_config["dip_column"] == "dip"
147 | assert project.map_data.config.geology_config["unitname_column"] == "unitname"
148 | assert project.map_data.config.geology_config["alt_unitname_column"] == "code"
--------------------------------------------------------------------------------
/tests/project/test_ignore_codes_setters_getters.py:
--------------------------------------------------------------------------------
1 | import pathlib
2 | from map2loop.project import Project
3 | from map2loop.m2l_enums import Datatype
4 | import map2loop
5 | from unittest.mock import patch
6 |
7 |
8 | # Sample test function for lithology and fault ignore codes
9 | def test_set_get_ignore_codes():
10 | # Set up a sample bounding box and other required data
11 | bbox_3d = {
12 | "minx": 515687.31005864,
13 | "miny": 7493446.76593407,
14 | "maxx": 562666.860106543,
15 | "maxy": 7521273.57407786,
16 | "base": -3200,
17 | "top": 3000,
18 | }
19 |
20 | # Set up the config dictionary
21 | config_dictionary = {
22 | "structure": {"dipdir_column": "azimuth2", "dip_column": "dip"},
23 | "geology": {"unitname_column": "unitname", "alt_unitname_column": "code"},
24 | "fault": {'structtype_column': 'feature', 'fault_text': 'Fault'},
25 | }
26 | with patch.object(Project, 'validate_required_inputs', return_value=None):
27 | project = Project(
28 | working_projection='EPSG:28350',
29 | bounding_box=bbox_3d,
30 | geology_filename=str(
31 | pathlib.Path(map2loop.__file__).parent
32 | / pathlib.Path('_datasets/geodata_files/hamersley/geology.geojson')
33 | ),
34 | fault_filename=str(
35 | pathlib.Path(map2loop.__file__).parent
36 | / pathlib.Path('_datasets/geodata_files/hamersley/faults.geojson')
37 | ),
38 | dtm_filename=str(
39 | pathlib.Path(map2loop.__file__).parent
40 | / pathlib.Path('_datasets/geodata_files/hamersley/dtm_rp.tif')
41 | ),
42 | config_dictionary=config_dictionary,
43 | structure_filename="",
44 | )
45 |
46 | # Define test ignore codes for lithology and faults
47 | lithology_codes = ["cover", "Fortescue_Group", "A_FO_od"]
48 | fault_codes = ['Fault_9', "NotAFault"]
49 |
50 | # Test Lithology ignore codes
51 | project.set_ignore_lithology_codes(lithology_codes)
52 | assert (
53 | project.map_data.get_ignore_lithology_codes() == lithology_codes
54 | ), "Lithology ignore codes mismatch"
55 |
56 | # Test Fault ignore codes
57 | project.set_ignore_fault_codes(fault_codes)
58 | assert project.map_data.get_ignore_fault_codes() == fault_codes, "Fault ignore codes mismatch"
59 |
60 | project.map_data.parse_fault_map()
61 | # check if the skipped fault has been removed now:
62 | assert not project.map_data.get_map_data(Datatype.FAULT)['NAME'].str.contains('Fault_9').any()
63 |
64 | project.map_data.parse_geology_map()
65 | # check if the skipped lithology has been removed now:
66 | assert (
67 | not project.map_data.get_map_data(Datatype.GEOLOGY)['UNITNAME']
68 | .str.contains('Fortescue_Group')
69 | .any()
70 | )
71 | assert (
72 | not project.map_data.get_map_data(Datatype.GEOLOGY)['UNITNAME'].str.contains('cover').any()
73 | )
74 |
--------------------------------------------------------------------------------
/tests/project/test_plot_hamersley.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from map2loop.project import Project
3 | from map2loop.m2l_enums import VerboseLevel
4 | from unittest.mock import patch
5 | from pyproj.exceptions import CRSError
6 | import requests
7 | import os
8 |
9 | # Define constants for common parameters
10 | bbox_3d = {
11 | "minx": 515687.31005864,
12 | "miny": 7493446.76593407,
13 | "maxx": 562666.860106543,
14 | "maxy": 7521273.57407786,
15 | "base": -3200,
16 | "top": 3000,
17 | }
18 | loop_project_filename = "wa_output.loop3d"
19 |
20 | # create a project function
21 | def create_project(state_data="WA", projection="EPSG:28350"):
22 | return Project(
23 | use_australian_state_data=state_data,
24 | working_projection=projection,
25 | bounding_box=bbox_3d,
26 | verbose_level=VerboseLevel.NONE,
27 | loop_project_filename=loop_project_filename,
28 | overwrite_loopprojectfile=True,
29 | )
30 |
31 |
32 | # is the project running?
33 | def test_project_execution():
34 |
35 | proj = create_project()
36 | try:
37 | proj.run_all(take_best=True)
38 | # if there's a timeout:
39 | except requests.exceptions.ReadTimeout:
40 | print("Timeout occurred, skipping the test.") # Debugging line
41 | pytest.skip(
42 | "Skipping the project test from server data due to timeout while attempting to run proj.run_all"
43 | )
44 |
45 | # if no timeout:
46 | # is there a project?
47 | assert proj is not None, "Plot Hamersley Basin failed to execute"
48 | # is there a LPF?
49 | assert os.path.exists(
50 | loop_project_filename
51 | ), f"Expected file {loop_project_filename} was not created"
52 |
53 |
54 | # Is the test_project_execution working - ie, is the test skipped on timeout?
55 | def test_timeout_handling():
56 | # Mock `openURL` in `owslib.util` to raise a ReadTimeout directly
57 | with patch("owslib.util.openURL"):
58 | # Run `test_project_execution` and check if the skip occurs
59 | result = pytest.main(
60 | ["-q", "--tb=short", "--disable-warnings", "-k", "test_project_execution"]
61 | )
62 | assert (
63 | result.value == pytest.ExitCode.OK
64 | ), "The test was not skipped as expected on timeout."
65 |
66 |
67 | # does the project fail when the CRS is invalid?
68 | def test_expect_crs_error():
69 | try:
70 | with pytest.raises(CRSError):
71 | create_project(projection="InvalidCRS")
72 | print("CRSError was raised as expected.")
73 | except requests.exceptions.ReadTimeout:
74 | print("Timeout occurred, skipping test_expect_crs_error.")
75 | pytest.skip("Skipping test_expect_crs_error due to a timeout.")
76 |
77 |
78 | # does the project fail when the Aus state name is invalid?
79 | def test_expect_state_error():
80 | try:
81 | with pytest.raises(ValueError):
82 | create_project(state_data="InvalidState")
83 | print("ValueError was raised as expected.")
84 | except requests.exceptions.ReadTimeout:
85 | print("Timeout occurred, skipping test_expect_state_error.")
86 | pytest.skip("Skipping test_expect_state_error due to a timeout.")
87 |
88 |
89 | # does the project fail when a config file is invalid?
90 | def test_expect_config_error():
91 | try:
92 | with pytest.raises(Exception):
93 | create_project(config_file='InvalidConfig.csv')
94 | print("FileNotFoundError//Exception by catchall in project.py was raised as expected.")
95 | except requests.exceptions.ReadTimeout:
96 | print("Timeout occurred, skipping test_expect_config_error.")
97 | pytest.skip("Skipping test_expect_config_error due to a timeout.")
98 |
--------------------------------------------------------------------------------
/tests/sampler/test_SamplerSpacing.py:
--------------------------------------------------------------------------------
1 | import pandas
2 | from map2loop.sampler import SamplerSpacing
3 | from beartype.roar import BeartypeCallHintParamViolation
4 | import pytest
5 | import shapely
6 | import geopandas
7 | import numpy
8 |
9 | # add test for SamplerSpacing specifically
10 |
11 |
12 | @pytest.fixture
13 | def sampler_spacing():
14 | return SamplerSpacing(spacing=1.0)
15 |
16 |
17 | @pytest.fixture
18 | def correct_geodata():
19 | data = {
20 | 'geometry': [
21 | shapely.LineString([(0, 0), (1, 1), (2, 2)]),
22 | shapely.Polygon([(0, 0), (1, 1), (1, 0), (0, 0)]),
23 | shapely.MultiLineString(
24 | [shapely.LineString([(0, 0), (1, 1)]), shapely.LineString([(2, 2), (3, 3)])]
25 | ),
26 | ],
27 | 'ID': ['1', '2', '3'],
28 | }
29 | return geopandas.GeoDataFrame(data, geometry='geometry')
30 |
31 |
32 | @pytest.fixture
33 | def incorrect_geodata():
34 | data = {'geometry': [shapely.Point(0, 0), "Not a geometry"], 'ID': ['1', '2']}
35 | return pandas.DataFrame(data)
36 |
37 |
38 | # test if correct outputs are generated from the right input
39 | def test_sample_function_correct_data(sampler_spacing, correct_geodata):
40 | result = sampler_spacing.sample(correct_geodata)
41 | assert isinstance(result, pandas.DataFrame)
42 | assert 'X' in result.columns
43 | assert 'Y' in result.columns
44 | assert 'featureId' in result.columns
45 |
46 |
47 | # add test for incorrect inputs - does it raise a BeartypeCallHintParamViolation error?
48 | def test_sample_function_incorrect_data(sampler_spacing, incorrect_geodata):
49 | with pytest.raises(BeartypeCallHintParamViolation):
50 | sampler_spacing.sample(spatial_data=incorrect_geodata)
51 |
52 |
53 | # for a specific >2 case
54 | def test_sample_function_target_less_than_or_equal_to_2():
55 | sampler_spacing = SamplerSpacing(spacing=1.0)
56 | data = {
57 | 'geometry': [shapely.LineString([(0, 0), (0, 1)]), shapely.LineString([(0, 0), (1, 0)])],
58 | 'ID': ['1', '2'],
59 | }
60 | gdf = geopandas.GeoDataFrame(data, geometry='geometry')
61 | result = sampler_spacing.sample(spatial_data=gdf)
62 | assert len(result) == 0 # No points should be sampled from the linestring
63 |
64 |
65 | # Test if the extracted points are correct
66 | def test_sample_function_extracted_points(sampler_spacing, correct_geodata):
67 |
68 | result = sampler_spacing.sample(correct_geodata)
69 |
70 | expected_points = [
71 | (0.0, 0.0),
72 | (0.707107, 0.707107),
73 | (0.0, 0.0),
74 | (0.707107, 0.707107),
75 | (1.0, 0.414214),
76 | (0.0, 0.0),
77 | [2.0, 2.0],
78 | ]
79 |
80 | sampled_points = list(zip(result['X'], result['Y']))
81 |
82 | distances = [
83 | shapely.geometry.Point(xy).distance(shapely.geometry.Point(ex))
84 | for xy, ex in zip(sampled_points, expected_points)
85 | ]
86 |
87 | assert numpy.absolute(distances).all() == 0.0
88 |
--------------------------------------------------------------------------------
/tests/sampler/test_SamplerSpacing_featureId.py:
--------------------------------------------------------------------------------
1 | import pandas
2 | from map2loop.sampler import SamplerSpacing
3 | import shapely
4 | import geopandas
5 |
6 | geology_original = pandas.read_csv("tests/sampler/geo_test.csv")
7 | geology_original['geometry'] = geology_original['geometry'].apply(shapely.wkt.loads)
8 | geology_original = geopandas.GeoDataFrame(geology_original, crs='epsg:7854')
9 |
10 | sampler_space = 700.0
11 |
12 | sampler = SamplerSpacing(spacing=sampler_space)
13 | geology_samples = sampler.sample(geology_original)
14 |
15 |
16 | # the actual test:
17 | def test_featureId():
18 | for _, poly in geology_original.iterrows():
19 | # check if one polygon, only 0 in featureId
20 | multipolygon = poly['geometry']
21 | corresponding_rows = geology_samples[geology_samples['ID'] == poly['ID']]
22 |
23 | if poly['geometry'].geom_type == 'Polygon':
24 | if poly.geometry.area < sampler_space:
25 | continue
26 | # check if zero featureId
27 | assert (
28 | corresponding_rows['featureId'].unique() == '0'
29 | ), "Polygon with featureId 0 is not sampled."
30 |
31 | # check if in the right place
32 | for _, sample in corresponding_rows.iterrows():
33 | point = shapely.Point(sample['X'], sample['Y']).buffer(1)
34 | assert point.intersects(
35 | poly.geometry
36 | ), f"Point from featureId 0 is not in the correct polygon segment of ID {poly['ID']}."
37 |
38 | if poly['geometry'].geom_type == 'MultiPolygon':
39 | if any(geom.area < sampler_space for geom in multipolygon.geoms):
40 | continue # skip tiny little polys
41 |
42 | # # is the number of segs the same as the geology polygon?
43 | assert len(multipolygon.geoms) == len(
44 | corresponding_rows.featureId.unique()
45 | ), "Mismatch in the number of geo_polygons and featureId"
46 |
47 | for i, polygon in enumerate(poly.geometry.geoms):
48 | polygon_samples = corresponding_rows[corresponding_rows['featureId'] == str(i)]
49 | print(polygon_samples)
50 | for _, sample in polygon_samples.iterrows():
51 | point = shapely.Point(sample['X'], sample['Y']).buffer(
52 | 1
53 | ) # buffer just to make sure
54 | assert point.intersects(
55 | polygon
56 | ), f"Point from featureId {i} is not in the correct polygon segment of ID {poly['ID']}."
57 |
--------------------------------------------------------------------------------
/tests/test_import.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def test_import_map2loop():
5 | try:
6 | import map2loop
7 |
8 | map2loop.__version__
9 | except ImportError:
10 | pytest.fail("Failed to import map2loop module")
11 |
--------------------------------------------------------------------------------
/tests/utils/test_rgb_and_hex_functions.py:
--------------------------------------------------------------------------------
1 | ### This file tests the function generate_random_hex_colors() and hex_to_rgba() in map2loop/utils.py
2 |
3 | import pytest
4 | import re
5 | from map2loop.utils import generate_random_hex_colors, hex_to_rgb
6 |
7 | # does it return the right number of colors?
8 | def test_generate_random_hex_colors_length():
9 | n = 5
10 | colors = generate_random_hex_colors(n)
11 | assert (
12 | len(colors) == n
13 | ), f"utils function generate_random_hex_colors not returning the right number of hex codes.Expected {n} colors, got {len(colors)}"
14 |
15 |
16 | # are the returned hex strings the right format?
17 | def test_generate_random_hex_colors_format():
18 | n = 10
19 | hex_pattern = re.compile(r'^#[0-9A-Fa-f]{6}$')
20 | colors = generate_random_hex_colors(n)
21 | for color in colors:
22 | assert hex_pattern.match(
23 | color
24 | ), f"utils function generate_random_hex_colors not returning hex strings in the right format. Got {color} instead."
25 |
26 |
27 | # is hex conversion to rgba working as expected?
28 | def test_hex_to_rgba_long_hex():
29 | hex_color = "#1a2b3c" # long hex versions
30 | expected_output = (0.10196078431372549, 0.16862745098039217, 0.23529411764705882, 1.0)
31 | assert (
32 | hex_to_rgb(hex_color) == expected_output
33 | ), f"utils function hex_to_rgba not doing hex to rgba conversion correctly. Expected {expected_output}, got {hex_to_rgb(hex_color)}"
34 |
35 |
36 | def test_hex_to_rgba_short_hex():
37 | hex_color = "#abc" # short hex versions
38 | expected_output = (0.6666666666666666, 0.7333333333333333, 0.8, 1.0)
39 | assert hex_to_rgb(hex_color) == expected_output
40 |
41 |
42 | # does it handle invalid inputs correctly?
43 | def test_hex_to_rgba_invalid_hex():
44 | with pytest.raises(ValueError):
45 | hex_to_rgb(
46 | "12FF456"
47 | ), "utils function hex_to_rgba is expected to raise a ValueError when an invalid hex string is passed, but it did not."
48 |
--------------------------------------------------------------------------------