├── .flake8 ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── feature_request.md │ └── questions-.md └── workflows │ ├── conda.yml │ ├── dockerimage.yml │ ├── documentation.yml │ ├── linter.yml │ ├── pypi.yml │ ├── python-publish.yml │ ├── release-please.yml │ └── tester.yml ├── .gitignore ├── .gitmodules ├── .release-please-manifest.json ├── CHANGELOG.md ├── CITATION.cff ├── Dockerfile ├── DockerfileDev ├── LICENSE ├── LoopStructural ├── CHANGELOG.md ├── __init__.py ├── datasets │ ├── __init__.py │ ├── _base.py │ ├── _example_models.py │ └── data │ │ ├── claudius.csv │ │ ├── claudiusbb.txt │ │ ├── duplex.csv │ │ ├── duplexbb.txt │ │ ├── fault_trace │ │ ├── fault_trace.cpg │ │ ├── fault_trace.dbf │ │ ├── fault_trace.prj │ │ ├── fault_trace.shp │ │ └── fault_trace.shx │ │ ├── geological_map_data │ │ ├── bbox.csv │ │ ├── contacts.csv │ │ ├── fault_displacement.csv │ │ ├── fault_edges.txt │ │ ├── fault_locations.csv │ │ ├── fault_orientations.csv │ │ ├── stratigraphic_order.csv │ │ ├── stratigraphic_orientations.csv │ │ └── stratigraphic_thickness.csv │ │ ├── intrusion.csv │ │ ├── intrusionbb.txt │ │ ├── onefoldbb.txt │ │ ├── onefolddata.csv │ │ ├── refolded_bb.txt │ │ ├── refolded_fold.csv │ │ └── tabular_intrusion.csv ├── datatypes │ ├── __init__.py │ ├── _bounding_box.py │ ├── _point.py │ ├── _structured_grid.py │ └── _surface.py ├── export │ ├── exporters.py │ ├── file_formats.py │ ├── geoh5.py │ ├── gocad.py │ └── omf_wrapper.py ├── interpolators │ ├── __init__.py │ ├── _api.py │ ├── _builders.py │ ├── _cython │ │ ├── __init__.py │ │ └── dsi_helper.pyx │ ├── _discrete_fold_interpolator.py │ ├── _discrete_interpolator.py │ ├── _finite_difference_interpolator.py │ ├── _geological_interpolator.py │ ├── _interpolator_builder.py │ ├── _interpolator_factory.py │ ├── _operator.py │ ├── _p1interpolator.py │ ├── _p2interpolator.py │ ├── _surfe_wrapper.py │ └── supports │ │ ├── _2d_base_unstructured.py │ │ ├── _2d_p1_unstructured.py │ │ ├── _2d_p2_unstructured.py │ │ ├── _2d_structured_grid.py │ │ ├── _2d_structured_tetra.py │ │ ├── _3d_base_structured.py │ │ ├── _3d_p2_tetra.py │ │ ├── _3d_structured_grid.py │ │ ├── _3d_structured_tetra.py │ │ ├── _3d_unstructured_tetra.py │ │ ├── __init__.py │ │ ├── _aabb.py │ │ ├── _base_support.py │ │ ├── _face_table.py │ │ └── _support_factory.py ├── modelling │ ├── __init__.py │ ├── core │ │ ├── __init__.py │ │ └── geological_model.py │ ├── features │ │ ├── __init__.py │ │ ├── _analytical_feature.py │ │ ├── _base_geological_feature.py │ │ ├── _cross_product_geological_feature.py │ │ ├── _geological_feature.py │ │ ├── _lambda_geological_feature.py │ │ ├── _projected_vector_feature.py │ │ ├── _region.py │ │ ├── _structural_frame.py │ │ ├── _unconformity_feature.py │ │ ├── builders │ │ │ ├── __init__.py │ │ │ ├── _base_builder.py │ │ │ ├── _fault_builder.py │ │ │ ├── _folded_feature_builder.py │ │ │ ├── _geological_feature_builder.py │ │ │ └── _structural_frame_builder.py │ │ ├── fault │ │ │ ├── __init__.py │ │ │ ├── _fault_function.py │ │ │ ├── _fault_function_feature.py │ │ │ └── _fault_segment.py │ │ └── fold │ │ │ ├── __init__.py │ │ │ ├── _fold.py │ │ │ ├── _fold_rotation_angle_feature.py │ │ │ ├── _foldframe.py │ │ │ ├── _svariogram.py │ │ │ └── fold_function │ │ │ ├── __init__.py │ │ │ ├── _base_fold_rotation_angle.py │ │ │ ├── _fourier_series_fold_rotation_angle.py │ │ │ ├── _lambda_fold_rotation_angle.py │ │ │ └── _trigo_fold_rotation_angle.py │ ├── input │ │ ├── __init__.py │ │ ├── fault_network.py │ │ ├── map2loop_processor.py │ │ ├── process_data.py │ │ └── project_file.py │ └── intrusions │ │ ├── __init__.py │ │ ├── geom_conceptual_models.py │ │ ├── geometric_scaling_functions.py │ │ ├── intrusion_builder.py │ │ ├── intrusion_feature.py │ │ ├── intrusion_frame_builder.py │ │ └── intrusion_support_functions.py ├── utils │ ├── __init__.py │ ├── _surface.py │ ├── _transformation.py │ ├── colours.py │ ├── config.py │ ├── dtm_creator.py │ ├── exceptions.py │ ├── features.py │ ├── helper.py │ ├── json_encoder.py │ ├── linalg.py │ ├── logging.py │ ├── maths.py │ ├── regions.py │ ├── typing.py │ └── utils.py ├── version.py └── visualisation │ └── __init__.py ├── README.md ├── conda ├── conda_build_config.yaml └── meta.yaml ├── docker-compose-win.yml ├── docker-compose.yml ├── docs ├── Dockerfile ├── Makefile ├── build_docs.sh ├── docker-compose.yml ├── make.bat ├── mv_docs.sh ├── requirements.txt └── source │ ├── API.rst │ ├── _static │ ├── custom-icon.js │ ├── docker.rst │ ├── infinity_loop_icon.svg │ ├── overview.rst │ ├── ubuntu_install.rst │ ├── windows_install.rst │ └── wls_install.rst │ ├── _templates │ ├── custom-class-template.rst │ ├── custom-module-template.rst │ ├── custom.css │ └── page.html │ ├── conf.py │ ├── docs_references.bib │ ├── getting_started │ ├── about.rst │ ├── background.rst │ ├── contributors_guide.rst │ ├── index.rst │ ├── installation.rst │ ├── loopstructural_design.rst │ └── todo.rst │ ├── images │ ├── edit_bashrc.png │ ├── fault_frame_figure.png │ ├── githubwebsite_clone.png │ ├── githubwindows_clone.png │ ├── githubwindows_update.png │ ├── image823.png │ ├── jupyter_browser.png │ ├── loop-struct-foot.png │ ├── loop.png │ ├── moba_xterm.png │ ├── powershell_enable_wls.png │ ├── run_jupyter.png │ ├── solver_comparison.png │ ├── structural_frame.png │ ├── ubuntu_login.png │ ├── ubuntu_microsoft_store.png │ └── wls_terminal.png │ ├── index.rst │ └── user_guide │ ├── debugging.rst │ ├── fault_modelling.rst │ ├── fold_modelling.rst │ ├── geological_model.rst │ ├── index.rst │ ├── input_data.rst │ ├── interpolation_options.rst │ ├── map2loop.rst │ ├── preparing_data.rst │ ├── structural_frames.rst │ ├── visualisation.rst │ └── what_is_a_geological_model.rst ├── examples ├── 1_basic │ ├── README.rst │ ├── plot_1_data_prepration.py │ ├── plot_2_surface_modelling.py │ ├── plot_3_model_visualisation.py │ ├── plot_3_multiple_groups.py │ ├── plot_4_using_stratigraphic_column.py │ ├── plot_5_unconformities.py │ ├── plot_6_fault_parameters.py │ └── plot_7_exporting.py ├── 2_fold │ ├── README.rst │ ├── _refolded_folds.py │ └── plot_1_adding_folds_to_surfaces.py ├── 3_fault │ ├── README.rst │ ├── _define_fault_displacement.py │ ├── _faulted_intrusion.py │ └── fault_network.py ├── 4_advanced │ ├── _5_using_logging.py │ ├── _7_local_weights.py │ └── _model_from_geological_map.py └── README.rst ├── pyproject.toml ├── release-please-config.json ├── requirements.txt ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── conftest.py ├── fixtures ├── __init__.py ├── data.py ├── horizontal_data.py └── interpolator.py ├── integration ├── test_fold_models.py ├── test_interpolator.py └── test_refolded.py └── unit ├── datatypes ├── test__structured_grid.py ├── test__surface.py └── test_bounding_box.py ├── input └── test_data_processor.py ├── interpolator ├── elements.txt ├── neighbours.txt ├── nodes.txt ├── test_2d_discrete_support.py ├── test_discrete_interpolator.py ├── test_discrete_supports.py ├── test_geological_interpolator.py ├── test_interpolator_builder.py ├── test_normal_magnitude_interpolators.py ├── test_outside_box.py ├── test_solvers.py └── test_unstructured_supports.py ├── modelling ├── intrusions │ └── test_intrusions.py ├── test__bounding_box.py ├── test__fault_builder.py ├── test_faults_segment.py ├── test_fold_event.py ├── test_geological_feature.py ├── test_geological_feature_builder.py ├── test_geological_model.py └── test_structural_frame.py ├── test_imports.py └── utils ├── test_conversions.py └── test_math.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a bug report to help develop LoopStructural 3 | title: "[BUG] " 4 | labels: [bug] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | attributes: 13 | label: Describe your issue 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Minimal reproducing code example 19 | description: "Include a minimal working example to demonstrate this code. Avoid using external datasets and extra dependencies" 20 | render: python 21 | validations: 22 | required: true 23 | - type: textarea 24 | attributes: 25 | label: Error message 26 | description: "Paste any error messages that your receive" 27 | render: shell 28 | # - type: input 29 | # attributes: 30 | # label: Versions 31 | # description: "Please run the following and paste the results: 32 | # `import sys, LoopStructural, lavavu, pandas, scipy, numpy; print(LoopStructural.__version__,sys.__version__, 33 | # lavavu.__version__,pandas.__version__,scipy.__version__,numpy.__version__) 34 | # validations: 35 | # required: true 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/questions-.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Questions? 3 | about: Need help applying LoopStructural to your dataset? 4 | title: "[Question]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/conda.yml: -------------------------------------------------------------------------------- 1 | name: "🐍 Build and upload Conda packages" 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | conda-deploy: 6 | name: Building conda package for python ${{ matrix.os }}) 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}} 12 | steps: 13 | - uses: conda-incubator/setup-miniconda@v3 14 | with: 15 | auto-update-conda: true 16 | python-version: ${{ matrix.python-version }} 17 | - uses: actions/checkout@v4 18 | - name: update submodules 19 | run: | 20 | git submodule update --init --recursive 21 | - name: Conda build 22 | env: 23 | ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }} 24 | shell: bash -l {0} 25 | run: | 26 | conda install -c conda-forge "conda-build<25" scikit-build numpy cython anaconda-client conda-libmamba-solver -y 27 | conda build -c conda-forge -c loop3d --output-folder conda conda --python ${{matrix.python-version}} 28 | conda convert -p all conda/linux-64/*.tar.bz2 -f -o conda 29 | - name: upload artifacts 30 | uses: actions/upload-artifact@v4 31 | with: 32 | name: conda-build-${{ matrix.python-version }} 33 | path: conda 34 | upload_to_conda: 35 | runs-on: ubuntu-latest 36 | needs: conda-deploy 37 | strategy: 38 | fail-fast: false 39 | matrix: 40 | python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}} 41 | steps: 42 | - uses: actions/download-artifact@v4 43 | with: 44 | name: conda-build-${{ matrix.python-version }} 45 | path: conda 46 | - uses: conda-incubator/setup-miniconda@v3 47 | - name: upload all files to conda-forge 48 | shell: bash -l {0} 49 | env: 50 | ANACONDA_API_TOKEN: ${{ secrets.ANACONDA_TOKEN }} 51 | run: | 52 | conda install -c anaconda anaconda-client -y 53 | anaconda upload --label main conda/*/*.tar.bz2 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/dockerimage.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Hub 2 | on: 3 | release: 4 | types: [edited,created] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Publish to Registry 12 | uses: elgohr/Publish-Docker-Github-Action@master 13 | with: 14 | name: loop3d/loopstructural 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_PASSWORD }} 17 | dockerfile: Dockerfile 18 | tags: "latest,${{ env.STATE_RELEASE_VERSION }}" 19 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: "📚 Build documentation and deploy " 2 | on: 3 | workflow_dispatch: # Able to not use cache by user demand 4 | inputs: 5 | cache: 6 | description: "Use build cache" 7 | required: false 8 | default: "true" 9 | 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | USE_CACHE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.cache == 'true') }} 17 | PYDEVD_DISABLE_FILE_VALIDATION: "1" 18 | PYTEST_ADDOPTS: "--color=yes" 19 | FORCE_COLOR: "True" 20 | 21 | jobs: 22 | doc: 23 | name: Build Documentation 24 | runs-on: ubuntu-20.04 25 | env: 26 | PYVISTA_OFF_SCREEN: "True" 27 | ALLOW_PLOTTING: true 28 | SHELLOPTS: "errexit:pipefail" 29 | steps: 30 | - uses: actions/checkout@v4 31 | 32 | - uses: actions/setup-python@v5 33 | with: 34 | python-version: "3.12" 35 | cache: "pip" 36 | 37 | - uses: awalsh128/cache-apt-pkgs-action@v1.1.3 38 | with: 39 | packages: libosmesa6-dev libgl1-mesa-dev python3-tk pandoc git-restore-mtime 40 | version: 3.0 41 | 42 | - name: Install PyVista and dependencies 43 | run: | 44 | pip install -e .[docs] 45 | 46 | - name: Install custom OSMesa VTK variant 47 | run: | 48 | pip uninstall vtk -y 49 | pip install vtk-osmesa==9.3.0 --index-url https://gitlab.kitware.com/api/v4/projects/13/packages/pypi/simple 50 | 51 | 52 | 53 | - name: Build Documentation 54 | run: make -C docs html 55 | 56 | - name: Dump Sphinx Warnings and Errors 57 | if: always() 58 | run: if [ -e doc/sphinx_warnings.txt ]; then cat doc/sphinx_warnings.txt; fi 59 | 60 | - name: Dump VTK Warnings and Errors 61 | if: always() 62 | run: if [ -e doc/errors.txt ]; then cat doc/errors.txt; fi 63 | 64 | 65 | 66 | 67 | 68 | - name: Upload HTML documentation 69 | if: always() 70 | uses: actions/upload-artifact@v4 71 | with: 72 | name: docs-build 73 | path: docs/build/html/ 74 | 75 | - uses: actions/upload-artifact@v4 76 | with: 77 | name: examples 78 | path: docs/source/_auto_examples/ 79 | 80 | - name: Get Notebooks 81 | run: | 82 | mkdir _notebooks 83 | find docs/source/_auto_examples -type f -name '*.ipynb' | cpio -p -d -v _notebooks/ 84 | 85 | - uses: actions/upload-artifact@v4 86 | with: 87 | name: loopstructural-notebooks 88 | path: _notebooks 89 | 90 | - name: Deploy 🚀 91 | uses: JamesIves/github-pages-deploy-action@4.1.3 92 | with: 93 | branch: gh-pages # The branch the action should deploy to. 94 | folder: docs/build/html # The folder the action should deploy. 95 | 96 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: "✅ Linter" 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.py' 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - '**.py' 13 | workflow_dispatch: 14 | 15 | env: 16 | PROJECT_FOLDER: "LoopStructural" 17 | PYTHON_VERSION: 3.9 18 | permissions: 19 | contents: write 20 | 21 | jobs: 22 | lint-py: 23 | name: Python 🐍 24 | 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Get source code 29 | uses: actions/checkout@v4 30 | 31 | - name: Set up Python 32 | uses: actions/setup-python@v5 33 | with: 34 | cache: "pip" 35 | python-version: ${{ env.PYTHON_VERSION }} 36 | 37 | - name: Install dependencies 38 | run: | 39 | python -m pip install --upgrade pip 40 | pip install black ruff 41 | - name: Autoformat with black 42 | run: | 43 | black . 44 | - name: Lint with ruff 45 | run: | 46 | ruff check ${{env.PROJECT_FOLDER}} --fix 47 | - uses: stefanzweifel/git-auto-commit-action@v5 48 | with: 49 | commit_message: "style: style fixes by ruff and autoformatting by black" 50 | branch: lint/style-fixes-${{ github.run_id }} 51 | create_branch: true 52 | - name: Create Pull Request 53 | uses: peter-evans/create-pull-request@v6 54 | with: 55 | token: ${{ secrets.GITHUB_TOKEN }} 56 | title: "style: auto format fixes" 57 | body: "This PR applies style fixes by black and ruff." 58 | base: master 59 | branch: lint/style-fixes-${{ github.run_id }} 60 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: "📦 PyPI " 2 | on: 3 | workflow_dispatch: 4 | 5 | jobs: 6 | make_sdist: 7 | name: Make SDist 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Build SDist 13 | run: | 14 | pip install build 15 | python -m build 16 | 17 | - uses: actions/upload-artifact@v4 18 | with: 19 | name: dist 20 | path: dist/ 21 | 22 | upload_to_pypi: 23 | needs: ["make_sdist"] 24 | runs-on: "ubuntu-latest" 25 | 26 | steps: 27 | - uses: actions/download-artifact@v4 28 | with: 29 | name: dist 30 | path: dist 31 | - uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | skip_existing: true 34 | verbose: true 35 | user: ${{ secrets.PYPI_USERNAME }} 36 | password: ${{ secrets.PYPI_PASSWORD }} -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | name: Python package build and publish 2 | 3 | on: 4 | release: 5 | types: [edited, created] 6 | 7 | jobs: 8 | build_wheels_macos: 9 | name: Build wheels 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Build wheels 18 | uses: joerick/cibuildwheel@v1.10.0 19 | env: 20 | CIBW_ARCHS_MACOS: x86_64 universal2 21 | CIBW_BUILD: "cp36-* cp37-* cp38-* cp39-*" 22 | CIBW_BEFORE_BUILD: "pip install numpy==1.18 cython" #make sure numpy is the same version as required by LS 23 | 24 | - name: Publish wheels to PyPI 25 | env: 26 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 27 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 28 | run : | 29 | pip install twine 30 | python -m twine upload ./wheelhouse/*.whl 31 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | env: 6 | PACKAGE_NAME: LoopStructural 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | issues: write 11 | 12 | name: release-please 13 | jobs: 14 | release-please: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: GoogleCloudPlatform/release-please-action@v4 18 | id: release 19 | with: 20 | path: LoopStructural 21 | 22 | outputs: 23 | release_created: ${{ steps.release.outputs.releases_created }} 24 | package: 25 | needs: release-please 26 | if: ${{ needs.release-please.outputs.release_created == 'true'}} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Trigger build for pypi and upload 30 | run: | 31 | curl -X POST \ 32 | -H "Authorization: token ${{ secrets.GH_PAT }}" \ 33 | -H "Accept: application/vnd.github.v3+json" \ 34 | https://api.github.com/repos/Loop3d/${{env.PACKAGE_NAME}}/actions/workflows/pypi.yml/dispatches \ 35 | -d '{"ref":"master"}' 36 | - name: Trigger build for conda and upload 37 | run: | 38 | curl -X POST \ 39 | -H "Authorization: token ${{ secrets.GH_PAT }}" \ 40 | -H "Accept: application/vnd.github.v3+json" \ 41 | https://api.github.com/repos/Loop3d/${{env.PACKAGE_NAME}}/actions/workflows/conda.yml/dispatches \ 42 | -d '{"ref":"master"}' 43 | 44 | 45 | -------------------------------------------------------------------------------- /.github/workflows/tester.yml: -------------------------------------------------------------------------------- 1 | name: "🎳 Tester" 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - '**.py' 9 | - .github/workflows/tester.yml 10 | 11 | pull_request: 12 | branches: 13 | - master 14 | paths: 15 | - '**.py' 16 | - .github/workflows/tester.yml 17 | workflow_dispatch: 18 | jobs: 19 | continuous-integration: 20 | name: Continuous integration ${{ matrix.os }} python ${{ matrix.python-version }} 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | os: ${{ fromJSON(vars.BUILD_OS)}} 26 | python-version: ${{ fromJSON(vars.PYTHON_VERSIONS)}} 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: conda-incubator/setup-miniconda@v3 30 | 31 | with: 32 | python-version: ${{ matrix.python }} 33 | - name: Installing dependencies 34 | shell: bash -l {0} 35 | run: | 36 | conda install -c conda-forge numpy scipy scikit-image scikit-learn pytest networkx osqp matplotlib -y 37 | - name: Building and install 38 | shell: bash -l {0} 39 | run: | 40 | pip install . --user 41 | - name: pytest 42 | shell: bash -l {0} 43 | run: | 44 | pytest 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | #vtk data 107 | *.vtk 108 | *.vtu 109 | *.xml 110 | 111 | # pycharm 112 | .idea/* 113 | #cython c files 114 | FME/cython/*.c 115 | 116 | # python environment 117 | fme_lg/* 118 | *.swp 119 | *.c 120 | .vscode/settings.json 121 | *.pkl 122 | docs/source/LoopStructural.interpolators.rst 123 | docs/source/LoopStructural.modelling.rst 124 | docs/source/LoopStructural.utils.rst 125 | docs/source/LoopStructural.visualisation.rst 126 | docs/source/_autosummary/* 127 | docs/source/_auto_examples/* 128 | docs/source/auto_examples/* 129 | examples/*/*.png 130 | docs/source/test/* 131 | examples/*.png 132 | dev/scalar_field 133 | dev/unconf 134 | docs/source/sg_execution_times.rst 135 | conda/index.html 136 | conda/channeldata.json 137 | conda/noarch 138 | conda/win-64 -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/.gitmodules -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "LoopStructural": "1.6.14" 3 | } 4 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: 1.2.0 2 | message: "If you use this software, please cite it as below." 3 | authors: 4 | - family-names: "Grose" 5 | given-names: "Lachlan" 6 | orcid: "https://orcid.org/0000-0001-8089-7775" 7 | - family-names: "Ailleres" 8 | given-names: "Laurent" 9 | orcid: "https://orcid.org/0000-0002-1897-4394" 10 | - family-names: "Laurent" 11 | given-names: "Gautier" 12 | orcid: "https://orcid.org/0000-0003-0638-3391" 13 | - family-names: "Jessell" 14 | given-names: "Mark" 15 | orcid: "https://orcid.org/0000-0002-0375-7311v" 16 | title: "LoopStructural" 17 | version: 1.0 18 | doi: 10.5194/gmd-14-3915-2021 19 | date-released: 2020-10-06 20 | url: "https://github.com/Loop3d/LoopStructural" 21 | -------------------------------------------------------------------------------- /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 | gfortran \ 12 | openmpi-bin \ 13 | libopenmpi-dev \ 14 | make 15 | # RUN conda install -c conda-forge python=3.9 -y 16 | RUN conda install -c conda-forge -c loop3d\ 17 | pip \ 18 | map2model\ 19 | hjson\ 20 | owslib\ 21 | beartype\ 22 | gdal=3.5.2\ 23 | rasterio=1.2.10 \ 24 | meshio\ 25 | scikit-learn \ 26 | cython \ 27 | numpy \ 28 | pandas \ 29 | scipy \ 30 | pymc3 \ 31 | jupyter \ 32 | pyamg \ 33 | # arviz==0.11.0 \ 34 | pygraphviz \ 35 | geopandas \ 36 | shapely \ 37 | ipywidgets \ 38 | ipyleaflet \ 39 | folium \ 40 | jupyterlab \ 41 | nodejs \ 42 | rasterio\ 43 | geopandas\ 44 | -y 45 | 46 | RUN pip install ipyfilechooser 47 | RUN jupyter nbextension enable --py --sys-prefix ipyleaflet 48 | RUN pip install lavavu-osmesa mplstereonet 49 | 50 | ENV LD_LIBRARY_PATH=/opt/conda/lib/python3.10/site-packages/lavavu/ 51 | 52 | 53 | ENV NB_USER jovyan 54 | ENV NB_UID 1000 55 | ENV HOME /home/${NB_USER} 56 | 57 | RUN adduser --disabled-password \ 58 | --gecos "Default user" \ 59 | --uid ${NB_UID} \ 60 | ${NB_USER} 61 | WORKDIR ${HOME} 62 | 63 | USER root 64 | RUN chown -R ${NB_UID} ${HOME} 65 | 66 | RUN pip install snakeviz 67 | 68 | # Add Tini 69 | ENV TINI_VERSION v0.19.0 70 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 71 | RUN chmod +x /tini 72 | ENTRYPOINT ["/tini", "--"] 73 | 74 | USER ${NB_USER} 75 | 76 | RUN mkdir notebooks 77 | RUN git clone https://github.com/Loop3D/map2loop-2.git map2loop 78 | RUN git clone https://github.com/Loop3D/LoopProjectFile.git 79 | RUN git clone https://github.com/TOMOFAST/Tomofast-x.git 80 | RUN pip install LoopStructural 81 | RUN pip install -e map2loop 82 | RUN pip install -e LoopProjectFile 83 | # WORKDIR Tomofast-x 84 | # RUN make 85 | WORKDIR ${HOME}/notebooks 86 | 87 | # RUN pip install -e LoopStructural 88 | CMD ["jupyter", "lab", "--ip='0.0.0.0'", "--NotebookApp.token=''", "--no-browser" ] 89 | 90 | EXPOSE 8050 91 | EXPOSE 8080:8090 -------------------------------------------------------------------------------- /DockerfileDev: -------------------------------------------------------------------------------- 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 | gfortran \ 12 | openmpi-bin \ 13 | libopenmpi-dev \ 14 | make 15 | # RUN conda install -c conda-forge python=3.9 -y 16 | RUN conda install -c conda-forge "python<=3.9" \ 17 | pip \ 18 | scikit-learn \ 19 | cython \ 20 | numpy \ 21 | pandas \ 22 | scipy \ 23 | pymc3 \ 24 | jupyter \ 25 | pyamg \ 26 | # arviz==0.11.0 \ 27 | pygraphviz \ 28 | geopandas \ 29 | shapely \ 30 | ipywidgets \ 31 | ipyleaflet \ 32 | folium \ 33 | jupyterlab \ 34 | nodejs \ 35 | rasterio\ 36 | -y 37 | 38 | RUN pip install ipyfilechooser 39 | RUN jupyter nbextension enable --py --sys-prefix ipyleaflet 40 | RUN pip install lavavu-osmesa==1.8.32 pyevtk 41 | 42 | ENV NB_USER jovyan 43 | ENV NB_UID 1000 44 | ENV HOME /home/${NB_USER} 45 | 46 | RUN adduser --disabled-password \ 47 | --gecos "Default user" \ 48 | --uid ${NB_UID} \ 49 | ${NB_USER} 50 | WORKDIR ${HOME} 51 | 52 | USER root 53 | RUN chown -R ${NB_UID} ${HOME} 54 | 55 | RUN pip install snakeviz 56 | 57 | # Add Tini 58 | ENV TINI_VERSION v0.19.0 59 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 60 | RUN chmod +x /tini 61 | ENTRYPOINT ["/tini", "--"] 62 | 63 | USER ${NB_USER} 64 | 65 | RUN mkdir notebooks 66 | RUN git clone https://github.com/Loop3D/LoopStructural.git 67 | RUN git clone https://github.com/Loop3D/map2loop-2.git map2loop 68 | RUN git clone https://github.com/Loop3D/LoopProjectFile.git 69 | RUN git clone https://github.com/TOMOFAST/Tomofast-x.git 70 | RUN pip install -e LoopStructural 71 | RUN pip install -e map2loop 72 | RUN pip install -e LoopProjectFile 73 | # WORKDIR Tomofast-x 74 | # RUN make 75 | WORKDIR ${HOME} 76 | # RUN pip install -e LoopStructural 77 | CMD ["jupyter", "notebook", "--ip='0.0.0.0'", "--NotebookApp.token=''", "--no-browser" ] 78 | 79 | EXPOSE 8050 80 | EXPOSE 8080:8090 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Lachlan Grose 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 | -------------------------------------------------------------------------------- /LoopStructural/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | LoopStructural 3 | ============== 4 | 5 | """ 6 | 7 | import logging 8 | from logging.config import dictConfig 9 | 10 | __all__ = ["GeologicalModel"] 11 | import tempfile 12 | from pathlib import Path 13 | from .version import __version__ 14 | 15 | experimental = False 16 | ch = logging.StreamHandler() 17 | formatter = logging.Formatter("%(levelname)s: %(asctime)s: %(filename)s:%(lineno)d -- %(message)s") 18 | ch.setFormatter(formatter) 19 | ch.setLevel(logging.WARNING) 20 | loggers = {} 21 | from .modelling.core.geological_model import GeologicalModel 22 | from .interpolators._api import LoopInterpolator 23 | from .interpolators import InterpolatorBuilder 24 | from .datatypes import BoundingBox 25 | from .utils import log_to_console, log_to_file, getLogger, rng, get_levels 26 | 27 | logger = getLogger(__name__) 28 | logger.info("Imported LoopStructural") 29 | 30 | 31 | def setLogging(level="info"): 32 | """ 33 | Set the logging parameters for log file 34 | 35 | Parameters 36 | ---------- 37 | filename : string 38 | name of file or path to file 39 | level : str, optional 40 | 'info', 'warning', 'error', 'debug' mapped to logging levels, by default 'info' 41 | """ 42 | import LoopStructural 43 | 44 | logger = getLogger(__name__) 45 | 46 | levels = get_levels() 47 | level = levels.get(level, logging.WARNING) 48 | LoopStructural.ch.setLevel(level) 49 | 50 | for name in LoopStructural.loggers: 51 | logger = logging.getLogger(name) 52 | logger.setLevel(level) 53 | logger.info(f'Set logging to {level}') 54 | -------------------------------------------------------------------------------- /LoopStructural/datasets/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Demo Datasets 3 | ============= 4 | 5 | Various datasets used for documentation and tutorials. 6 | """ 7 | 8 | from ._base import load_claudius 9 | from ._base import load_grose2017 10 | from ._base import load_grose2018 11 | from ._base import load_grose2019 12 | from ._base import load_laurent2016 13 | from ._base import load_noddy_single_fold 14 | from ._base import load_intrusion 15 | from ._base import normal_vector_headers 16 | from ._base import strike_dip_headers 17 | from ._base import value_headers 18 | from ._base import load_unconformity 19 | from ._base import load_duplex 20 | from ._base import load_tabular_intrusion 21 | from ._base import load_geological_map_data 22 | from ._base import load_fault_trace 23 | from ._base import load_horizontal 24 | -------------------------------------------------------------------------------- /LoopStructural/datasets/_example_models.py: -------------------------------------------------------------------------------- 1 | vis = True 2 | try: 3 | pass 4 | except: 5 | print("No visualisation") 6 | vis = False 7 | 8 | 9 | def _build_claudius(): 10 | pass 11 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/claudiusbb.txt: -------------------------------------------------------------------------------- 1 | 5.488000000000000000e+05 7.816600000000000000e+06 -1.101000000000000000e+04 2 | 5.525000000000000000e+05 7.822000000000000000e+06 -8.400000000000000000e+03 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/duplexbb.txt: -------------------------------------------------------------------------------- 1 | -1.000000000000000000e+01 -1.000000000000000000e+00 -1.000000000000000000e+00 2 | 1.000000000000000000e+01 3.000000000000000000e+00 1.000000000000000000e+01 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/fault_trace/fault_trace.cpg: -------------------------------------------------------------------------------- 1 | UTF-8 -------------------------------------------------------------------------------- /LoopStructural/datasets/data/fault_trace/fault_trace.dbf: -------------------------------------------------------------------------------- 1 | z a[idN 2 | fault_nameCP **********fault_1 **********fault_2  -------------------------------------------------------------------------------- /LoopStructural/datasets/data/fault_trace/fault_trace.prj: -------------------------------------------------------------------------------- 1 | PROJCS["GDA2020_MGA_Zone_53",GEOGCS["GCS_GDA2020",DATUM["GDA2020",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",500000.0],PARAMETER["False_Northing",10000000.0],PARAMETER["Central_Meridian",135.0],PARAMETER["Scale_Factor",0.9996],PARAMETER["Latitude_Of_Origin",0.0],UNIT["Meter",1.0]] -------------------------------------------------------------------------------- /LoopStructural/datasets/data/fault_trace/fault_trace.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/LoopStructural/datasets/data/fault_trace/fault_trace.shp -------------------------------------------------------------------------------- /LoopStructural/datasets/data/fault_trace/fault_trace.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/LoopStructural/datasets/data/fault_trace/fault_trace.shx -------------------------------------------------------------------------------- /LoopStructural/datasets/data/geological_map_data/bbox.csv: -------------------------------------------------------------------------------- 1 | origin, 519572.569,7489723.89,-4800.0 2 | maximum,551978.745,7516341.01,1200.0 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/geological_map_data/fault_displacement.csv: -------------------------------------------------------------------------------- 1 | Fault,displacement 2 | Fault_2997,90.48575714705343 3 | Fault_3496,84.0 4 | Fault_3498,98.01101917761947 5 | Fault_7439,84.4739703254439 6 | Fault_12647,88.47256501865093 7 | Fault_12658,34.0 8 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/geological_map_data/fault_edges.txt: -------------------------------------------------------------------------------- 1 | Fault_7439, Fault_3496 2 | Fault_2997, Fault_3498 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/geological_map_data/fault_orientations.csv: -------------------------------------------------------------------------------- 1 | ,X,Y,Z,gx,gy,gz,coord,feature_name 2 | 0,541963.19163038,7493283.13978594,674.3337120305703,0.5637102340673046,0.7000439341002919,0.43837114678907735,0,Fault_2997 3 | 1,545755.3624073934,7489723.89,674.3337120305703,0.5637102340673046,0.7000439341002919,0.43837114678907735,0,Fault_2997 4 | 2,534479.09627295,7498804.10098736,518.9818313655426,0.5637102340673046,0.7000439341002919,0.43837114678907735,0,Fault_2997 5 | 3,525794.77713101,7496712.51283815,473.15194527242295,-0.8407355773657558,-0.5312602839648115,0.10452846326765346,0,Fault_3496 6 | 4,530012.248380578,7489723.89,473.15194527242295,-0.8407355773657558,-0.5312602839648115,0.10452846326765346,0,Fault_3496 7 | 5,522189.26567998,7502103.99834995,547.9009647774983,-0.8407355773657558,-0.5312602839648115,0.10452846326765346,0,Fault_3496 8 | 6,532914.74969844,7501129.60123437,596.9398929098236,0.7158482261571691,0.5305223305892705,0.45399049973954686,0,Fault_3498 9 | 7,539513.18738029,7491597.13566214,596.9398929098236,0.7158482261571691,0.5305223305892705,0.45399049973954686,0,Fault_3498 10 | 8,530175.96563729,7504196.10320143,574.9156341302787,0.7158482261571691,0.5305223305892705,0.45399049973954686,0,Fault_3498 11 | 9,534321.30492983,7491059.07179509,558.9449048693546,0.23786642062354135,0.9523715100858164,0.19080899537654492,0,Fault_7439 12 | 10,540588.0629057533,7489723.89,558.9449048693546,0.23786642062354135,0.9523715100858164,0.19080899537654492,0,Fault_7439 13 | 11,527475.04337437,7492999.02684152,493.1021139078194,0.23786642062354135,0.9523715100858164,0.19080899537654492,0,Fault_7439 14 | 12,524494.08762163,7496605.00666576,535.0082514443101,0.8284417463508296,0.52005480646225,0.20791169081775948,0,Fault_12647 15 | 13,520343.17884566,7501712.97896174,535.0082514443101,0.8284417463508296,0.52005480646225,0.20791169081775948,0,Fault_12647 16 | 14,527869.336680077,7489723.89,448.2019668469705,0.8284417463508296,0.52005480646225,0.20791169081775948,0,Fault_12647 17 | 15,548978.50085605,7508211.38333105,627.0135395186778,0.804019186643728,0.32179674875417574,0.5000000000000001,0,Fault_12658 18 | 16,546639.32683299,7512670.78252025,627.0135395186778,0.804019186643728,0.32179674875417574,0.5000000000000001,0,Fault_12658 19 | 17,551978.745,7499330.080140844,536.5018389409632,0.804019186643728,0.32179674875417574,0.5000000000000001,0,Fault_12658 20 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/geological_map_data/stratigraphic_order.csv: -------------------------------------------------------------------------------- 1 | group,index in group,unit name 2 | 0,0,Turee_Creek_Group 3 | 0,1,Boolgeeda_Iron_Formation 4 | 0,2,Woongarra_Rhyolite 5 | 0,3,Weeli_Wolli_Formation 6 | 0,4,Brockman_Iron_Formation 7 | 0,5,Mount_McRae_Shale_and_Mount_Sylvia_Formation 8 | 0,6,Wittenoom_Formation 9 | 0,7,Marra_Mamba_Iron_Formation 10 | 0,8,Jeerinah_Formation 11 | 0,9,Fortescue_Group 12 | 0,10,Bunjinah_Formation 13 | 0,11,Pyradie_Formation 14 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/geological_map_data/stratigraphic_thickness.csv: -------------------------------------------------------------------------------- 1 | ,thickness 2 | Mount_McRae_Shale_and_Mount_Sylvia_Formation,224.5 3 | Marra_Mamba_Iron_Formation,152.0 4 | Boolgeeda_Iron_Formation,166.5 5 | Woongarra_Rhyolite,389.0 6 | Jeerinah_Formation,600.0 7 | Brockman_Iron_Formation,557.0 8 | Wittenoom_Formation,236.0 9 | Weeli_Wolli_Formation,241.5 10 | Turee_Creek_Group,162.0 11 | Fortescue_Group,236.0 12 | Bunjinah_Formation,236.0 13 | Pyradie_Formation,236.0 14 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/intrusionbb.txt: -------------------------------------------------------------------------------- 1 | 0.000000000000000000e+00 0.000000000000000000e+00 -4.500000000000000000e+03 2 | 1.000000000000000000e+04 1.000000000000000000e+04 5.000000000000000000e+02 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/onefoldbb.txt: -------------------------------------------------------------------------------- 1 | 0.000000000000000000e+00 0.000000000000000000e+00 5.000000000000000000e+03 2 | 1.000000000000000000e+04 7.000000000000000000e+03 1.000000000000000000e+04 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/refolded_bb.txt: -------------------------------------------------------------------------------- 1 | 0.000000000000000000e+00 0.000000000000000000e+00 0.000000000000000000e+00 2 | 1.000000000000000000e+04 1.000000000000000000e+04 5.000000000000000000e+03 3 | -------------------------------------------------------------------------------- /LoopStructural/datasets/data/tabular_intrusion.csv: -------------------------------------------------------------------------------- 1 | feature_name,X,Y,Z,coord,val,gx,gy,gz,intrusion_contact_type,intrusion_anisotropy,intrusion_side 2 | stratigraphy,0.00,0.00,2.00,,0,,,,,, 3 | stratigraphy,0.00,5.00,2.00,,0,,,,,, 4 | stratigraphy,5.00,0.00,2.00,,0,,,,,, 5 | stratigraphy,5.00,5.00,2.00,,0,,,,,, 6 | stratigraphy,2.50,2.50,2.00,,,0,0,1,,, 7 | tabular_intrusion,3.04,2.12,2.18,,,,,,roof,stratigraphy, 8 | tabular_intrusion,4.02,3.85,2.14,,,,,,roof,stratigraphy,TRUE 9 | tabular_intrusion,2.02,3.16,2.02,,,,,,roof,stratigraphy, 10 | tabular_intrusion,2.12,2.16,2.02,,,,,,roof,stratigraphy, 11 | tabular_intrusion,1.14,3.50,2.04,,,,,,roof,,TRUE 12 | tabular_intrusion,4.08,3.02,1.18,,,,,,floor,,TRUE 13 | tabular_intrusion,1.14,1.06,1.16,,,,,,floor,,TRUE 14 | tabular_intrusion,4.20,2.18,1.12,,,,,,floor,,TRUE 15 | tabular_intrusion,1.02,3.02,1.12,,,,,,floor,,TRUE 16 | tabular_intrusion_frame,2.00,2.00,2.00,0,,0,0,-1,,, 17 | tabular_intrusion_frame,3.00,1.00,2.00,0,,0,0,-1,,, 18 | tabular_intrusion_frame,1.00,3.00,2.00,0,,0,0,-1,,, 19 | tabular_intrusion_frame,3.00,2.00,1.00,1,0,,,,,, 20 | tabular_intrusion_frame,3.00,2.00,1.00,1,,0,1,0,,, 21 | tabular_intrusion_frame,2.50,1.00,1.00,2,0,,,,,, 22 | tabular_intrusion_frame,2.50,2.00,1.00,2,0,,,,,, 23 | tabular_intrusion_frame,2.50,2.00,1.00,2,,1,0,0,,, 24 | -------------------------------------------------------------------------------- /LoopStructural/datatypes/__init__.py: -------------------------------------------------------------------------------- 1 | from ._surface import Surface 2 | from ._bounding_box import BoundingBox 3 | from ._point import ValuePoints, VectorPoints 4 | from ._structured_grid import StructuredGrid 5 | -------------------------------------------------------------------------------- /LoopStructural/export/file_formats.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exported file formats 3 | """ 4 | 5 | from enum import Enum 6 | 7 | 8 | class FileFormat(Enum): 9 | """Enumeration of file export formats""" 10 | 11 | OBJ = 1 # Not supported yet 12 | VTK = 2 13 | GOCAD = 3 14 | GLTF = 4 # Not supported yet 15 | NUMPY = 5 16 | -------------------------------------------------------------------------------- /LoopStructural/export/gocad.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def _write_feat_surfs_gocad(surf, file_name): 5 | """ 6 | Writes out a GOCAD TSURF file for each surface in list 7 | 8 | Parameters 9 | ---------- 10 | surf_list: [ SimpleNamespace() ... ] 11 | Details of the surfaces, as a list of SimpleNamespace() objects. Fields are: 12 | verts: vertices, numpy ndarray with dtype = float64 & shape = (N,3) 13 | faces: faces, numpy ndarray with dtype = int32 & shape = (M,3) 14 | values: values, numpy ndarray with dtype = float32 & shape = (N,) 15 | normals: normals, numpy ndarray with dtype = float32 & shape = (N,3) 16 | name: name of feature e.g. fault or supergroup, string 17 | 18 | file_name: string 19 | Desired filename 20 | 21 | Returns 22 | ------- 23 | True if successful 24 | 25 | """ 26 | from pathlib import Path 27 | 28 | properties_header = None 29 | if surf.properties: 30 | 31 | properties_header = f"""PROPERTIES {' '.join(list(surf.properties.keys()))} 32 | NO_DATA_VALUES -99999 33 | PROPERTY_CLASSES {' '.join(list(surf.properties.keys()))} 34 | """ 35 | 36 | file_name = Path(file_name).with_suffix(".ts") 37 | with open(f"{file_name}", "w") as fd: 38 | fd.write( 39 | f"""GOCAD TSurf 1 40 | HEADER {{ 41 | *solid*color: #ffa500 42 | ivolmap: false 43 | imap: false 44 | name: {surf.name} 45 | }} 46 | GOCAD_ORIGINAL_COORDINATE_SYSTEM 47 | NAME Default 48 | PROJECTION Unknown 49 | DATUM Unknown 50 | AXIS_NAME X Y Z 51 | AXIS_UNIT m m m 52 | ZPOSITIVE Elevation 53 | END_ORIGINAL_COORDINATE_SYSTEM 54 | GEOLOGICAL_FEATURE {surf.name} 55 | GEOLOGICAL_TYPE fault 56 | {properties_header if properties_header else ""} 57 | PROPERTY_CLASS_HEADER X {{ 58 | kind: X 59 | unit: m 60 | }} 61 | PROPERTY_CLASS_HEADER Y {{ 62 | kind: Y 63 | unit: m 64 | }} 65 | PROPERTY_CLASS_HEADER Z {{ 66 | kind: Z 67 | unit: m 68 | is_z: on 69 | }} 70 | PROPERTY_CLASS_HEADER vector3d {{ 71 | kind: Length 72 | unit: m 73 | }} 74 | TFACE 75 | """ 76 | ) 77 | v_idx = 1 78 | v_map = {} 79 | for idx, vert in enumerate(surf.vertices): 80 | if not np.isnan(vert[0]) and not np.isnan(vert[1]) and not np.isnan(vert[2]): 81 | fd.write(f"VRTX {v_idx:} {vert[0]} {vert[1]} {vert[2]}") 82 | if surf.properties: 83 | for value in surf.properties.values(): 84 | fd.write(f" {value[idx]}") 85 | fd.write("\n") 86 | v_map[idx] = v_idx 87 | v_idx += 1 88 | for face in surf.triangles: 89 | if face[0] in v_map and face[1] in v_map and face[2] in v_map: 90 | fd.write(f"TRGL {v_map[face[0]]} {v_map[face[1]]} {v_map[face[2]]} \n") 91 | fd.write("END\n") 92 | return True 93 | 94 | 95 | # def _write_pointset(points, file_name): 96 | # """ 97 | # Write out a GOCAD VS file for a pointset 98 | 99 | # Parameters 100 | # ---------- 101 | # points: SimpleNamespace() 102 | # Details of the points, as a SimpleNamespace() object. Fields are: 103 | # locations: locations, numpy ndarray with dtype = float64 & shape = (N,3) 104 | # vectors: vectors, numpy ndarray with dtype = float64 & shape = (N,3) 105 | # name: name of feature e.g. fault or supergroup, string 106 | 107 | # file_name: string 108 | # Desired filename 109 | 110 | # Returns 111 | # ------- 112 | # True if successful 113 | 114 | # """ 115 | # from pathlib import Path 116 | 117 | # file_name = Path(file_name).with_suffix(".vs") 118 | # with open(f"{file_name}", "w") as fd: 119 | # fd.write( 120 | # f"""GOCAD VSet 1 121 | # HEADER {{ 122 | # name: {points.name} 123 | # }} 124 | # GOCAD_ORIGINAL_COORDINATE_SYSTEM 125 | # NAME Default 126 | # PROJECTION Unknown 127 | -------------------------------------------------------------------------------- /LoopStructural/export/omf_wrapper.py: -------------------------------------------------------------------------------- 1 | try: 2 | import omf 3 | except ImportError: 4 | raise ImportError( 5 | "You need to install the omf package to use this feature. " 6 | "You can install it with: pip install mira-omf" 7 | ) 8 | import numpy as np 9 | import datetime 10 | import os 11 | 12 | 13 | def get_project(filename): 14 | if os.path.exists(filename): 15 | 16 | try: 17 | reader = omf.OMFReader(filename) 18 | project = reader.get_project() 19 | except (FileNotFoundError, ValueError): 20 | project = omf.Project(name='LoopStructural Model') 21 | return project 22 | else: 23 | return omf.Project(name='LoopStructural Model') 24 | 25 | 26 | def get_cell_attributes(loopobject): 27 | attributes = [] 28 | if loopobject.cell_properties: 29 | for k, v in loopobject.cell_properties.items(): 30 | v = np.array(v) 31 | if len(v.shape) > 1 and v.shape[1] > 1: 32 | for i in range(v.shape[1]): 33 | attributes.append( 34 | omf.ScalarData(name=f'{k}_{i}', array=v[:, i], location="faces") 35 | ) 36 | else: 37 | attributes.append(omf.ScalarData(name=k, array=v, location="faces")) 38 | 39 | return attributes 40 | 41 | 42 | def get_point_attributed(loopobject): 43 | attributes = [] 44 | if loopobject.properties: 45 | for k, v in loopobject.properties.items(): 46 | v = np.array(v) 47 | if len(v.shape) > 1 and v.shape[1] > 1: 48 | for i in range(v.shape[1]): 49 | attributes.append( 50 | omf.ScalarData(name=f'{k}_{i}', array=v[:, i], location="vertices") 51 | ) 52 | else: 53 | attributes.append(omf.ScalarData(name=k, array=v, location="vertices")) 54 | 55 | return attributes 56 | 57 | 58 | def add_surface_to_omf(surface, filename): 59 | 60 | attributes = [] 61 | attributes += get_cell_attributes(surface) 62 | attributes += get_point_attributed(surface) 63 | surface = omf.SurfaceElement( 64 | geometry=omf.SurfaceGeometry( 65 | vertices=surface.vertices, 66 | triangles=surface.triangles, 67 | ), 68 | data=attributes, 69 | name=surface.name, 70 | ) 71 | project = get_project(filename) 72 | 73 | project.elements += [surface] 74 | project.metadata = { 75 | "coordinate_reference_system": "epsg 3857", 76 | "date_created": datetime.datetime.utcnow(), 77 | "version": "v1.3", 78 | "revision": "10", 79 | } 80 | omf.OMFWriter(project, filename) 81 | 82 | 83 | def add_pointset_to_omf(points, filename): 84 | 85 | attributes = [] 86 | attributes += get_point_attributed(points) 87 | 88 | points = omf.PointSetElement( 89 | vertices=points.locations, 90 | attributes=attributes, 91 | name=points.name, 92 | ) 93 | 94 | project = get_project(filename) 95 | project.elements += [points] 96 | omf.OMFWriter(project, filename) 97 | 98 | 99 | def add_structured_grid_to_omf(grid, filename): 100 | print('Open Mining Format cannot store structured grids') 101 | return 102 | # attributes = [] 103 | # attributes += get_cell_attributes(grid) 104 | # attributes += get_point_attributed(grid) 105 | 106 | # vol = omf.TensorGridBlockModel( 107 | # name=grid.name, 108 | # tensor_u=np.ones(grid.nsteps[0]).astype(float), 109 | # tensor_v=np.ones(grid.nsteps[0]).astype(float), 110 | # tensor_w=np.ones(grid.nsteps[0]).astype(float), 111 | # origin=grid.origin, 112 | # attributes=attributes, 113 | # ) 114 | # project = get_project(filename) 115 | # project.elements += [vol] 116 | # omf.save(project, filename, mode='w') 117 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/_cython/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/LoopStructural/interpolators/_cython/__init__.py -------------------------------------------------------------------------------- /LoopStructural/interpolators/_interpolator_factory.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | from .supports import SupportFactory 3 | from . import ( 4 | interpolator_map, 5 | InterpolatorType, 6 | support_interpolator_map, 7 | interpolator_string_map, 8 | ) 9 | from LoopStructural.datatypes import BoundingBox 10 | import numpy as np 11 | 12 | 13 | class InterpolatorFactory: 14 | @staticmethod 15 | def create_interpolator( 16 | interpolatortype: Optional[Union[str, InterpolatorType]] = None, 17 | boundingbox: Optional[BoundingBox] = None, 18 | nelements: Optional[int] = None, 19 | element_volume: Optional[float] = None, 20 | support=None, 21 | buffer: Optional[float] = None, 22 | ): 23 | if interpolatortype is None: 24 | raise ValueError("No interpolator type specified") 25 | if boundingbox is None: 26 | raise ValueError("No bounding box specified") 27 | 28 | if isinstance(interpolatortype, str): 29 | interpolatortype = interpolator_string_map[interpolatortype] 30 | if support is None: 31 | # raise Exception("Support must be specified") 32 | 33 | supporttype = support_interpolator_map[interpolatortype][boundingbox.dimensions] 34 | 35 | support = SupportFactory.create_support_from_bbox( 36 | supporttype, 37 | bounding_box=boundingbox, 38 | nelements=nelements, 39 | element_volume=element_volume, 40 | buffer=buffer, 41 | ) 42 | return interpolator_map[interpolatortype](support) 43 | 44 | @staticmethod 45 | def from_dict(d): 46 | d = d.copy() 47 | interpolator_type = d.pop("type", None) 48 | if interpolator_type is None: 49 | raise ValueError("No interpolator type specified") 50 | return InterpolatorFactory.create_interpolator(interpolator_type, **d) 51 | 52 | @staticmethod 53 | def get_supported_interpolators(): 54 | return interpolator_map.keys() 55 | 56 | @staticmethod 57 | def create_interpolator_with_data( 58 | interpolatortype: str, 59 | boundingbox: BoundingBox, 60 | nelements: int, 61 | element_volume: Optional[float] = None, 62 | support=None, 63 | value_constraints: Optional[np.ndarray] = None, 64 | gradient_norm_constraints: Optional[np.ndarray] = None, 65 | gradient_constraints: Optional[np.ndarray] = None, 66 | ): 67 | interpolator = InterpolatorFactory.create_interpolator( 68 | interpolatortype, boundingbox, nelements, element_volume, support 69 | ) 70 | if value_constraints is not None: 71 | interpolator.set_value_constraints(value_constraints) 72 | if gradient_norm_constraints is not None: 73 | interpolator.set_normal_constraints(gradient_norm_constraints) 74 | if gradient_constraints is not None: 75 | interpolator.set_gradient_constraints(gradient_constraints) 76 | interpolator.setup() 77 | return interpolator 78 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/_operator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Finite difference masks 3 | """ 4 | 5 | import numpy as np 6 | 7 | from ..utils import getLogger 8 | 9 | logger = getLogger(__name__) 10 | 11 | 12 | class Operator(object): 13 | """ 14 | Finite difference masks for adding constraints for the derivatives and second derivatives 15 | Operator.Dx_mask gives derivative in x direction 16 | """ 17 | 18 | z = np.zeros((3, 3)) 19 | Dx_mask = np.array([z, [[0.0, 0.0, 0.0], [-0.5, 0.0, 0.5], [0.0, 0.0, 0.0]], z]) 20 | Dy_mask = Dx_mask.swapaxes(1, 2) 21 | Dz_mask = Dx_mask.swapaxes(0, 2) 22 | 23 | Dxx_mask = np.array([z, [[0, 0, 0], [1, -2, 1], [0, 0, 0]], z]) 24 | Dyy_mask = Dxx_mask.swapaxes(1, 2) 25 | Dzz_mask = Dxx_mask.swapaxes(0, 2) 26 | 27 | Dxy_mask = np.array([z, [[-0.25, 0, 0.25], [0, 0, 0], [0.25, 0, -0.25]], z]) / np.sqrt(2) 28 | Dxz_mask = Dxy_mask.swapaxes(0, 1) 29 | Dyz_mask = Dxy_mask.swapaxes(0, 2) 30 | 31 | # from https://en.wikipedia.org/wiki/Discrete_Laplace_operator 32 | Lapacian = np.array( 33 | [ 34 | [[0, 0, 0], [0, 1, 0], [0, 0, 0]], # first plane 35 | [[0, 1, 0], [1, -6, 1], [0, 1, 0]], # second plane 36 | [[0, 0, 0], [0, 1, 0], [0, 0, 0]], # third plane 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/_2d_p1_unstructured.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tetmesh based on cartesian grid for piecewise linear interpolation 3 | """ 4 | 5 | import logging 6 | 7 | import numpy as np 8 | from ._2d_base_unstructured import BaseUnstructured2d 9 | from . import SupportType 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class P1Unstructured2d(BaseUnstructured2d): 15 | """ """ 16 | 17 | def __init__(self, elements, vertices, neighbours): 18 | BaseUnstructured2d.__init__(self, elements, vertices, neighbours) 19 | self.type = SupportType.P1Unstructured2d 20 | 21 | def evaluate_shape_derivatives(self, locations, elements=None): 22 | """ 23 | compute dN/ds (1st row), dN/dt(2nd row) 24 | """ 25 | inside = None 26 | if elements is not None: 27 | inside = np.zeros(self.n_elements, dtype=bool) 28 | inside[elements] = True 29 | locations = np.array(locations) 30 | if elements is None: 31 | vertices, c, tri, inside = self.get_element_for_location(locations) 32 | else: 33 | tri = elements 34 | M = np.ones((elements.shape[0], 3, 3)) 35 | M[:, :, 1:] = self.vertices[self.elements[elements], :][:, :3, :] 36 | points_ = np.ones((locations.shape[0], 3)) 37 | points_[:, 1:] = locations 38 | # minv = np.linalg.inv(M) 39 | # c = np.einsum("lij,li->lj", minv, points_) 40 | 41 | vertices = self.nodes[self.elements[tri][:, :3]] 42 | jac = np.zeros((tri.shape[0], 2, 2)) 43 | jac[:, 0, 0] = vertices[:, 1, 0] - vertices[:, 0, 0] 44 | jac[:, 0, 1] = vertices[:, 1, 1] - vertices[:, 0, 1] 45 | jac[:, 1, 0] = vertices[:, 2, 0] - vertices[:, 0, 0] 46 | jac[:, 1, 1] = vertices[:, 2, 1] - vertices[:, 0, 1] 47 | # N = np.zeros((tri.shape[0], 6)) 48 | 49 | # dN containts the derivatives of the shape functions 50 | dN = np.array([[-1.0, 1.0, 0.0], [-1.0, 0.0, 1.0]]) 51 | 52 | # find the derivatives in x and y by calculating the dot product between the jacobian^-1 and the 53 | # derivative matrix 54 | # d_n = np.einsum('ijk,ijl->ilk',np.linalg.inv(jac),dN) 55 | d_n = np.linalg.inv(jac) 56 | # d_n = d_n.swapaxes(1,2) 57 | d_n = d_n @ dN 58 | # d_n = d_n.swapaxes(2, 1) 59 | # d_n = np.dot(np.linalg.inv(jac),dN) 60 | return d_n, tri, inside 61 | 62 | def evaluate_shape(self, locations): 63 | locations = np.array(locations) 64 | vertices, c, tri, inside = self.get_element_for_location(locations, return_verts=False) 65 | # c = np.dot(np.array([1,x,y]),np.linalg.inv(M)) # convert to barycentric coordinates 66 | # order of bary coord is (1-s-t,s,t) 67 | N = c # np.zeros((c.shape[0],3)) #evaluate shape functions at barycentric coordinates 68 | return N, tri, inside 69 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/_2d_structured_tetra.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/LoopStructural/interpolators/supports/_2d_structured_tetra.py -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class SupportType(IntEnum): 5 | """ 6 | Enum for the different interpolator types 7 | 8 | 1-9 should cover interpolators with supports 9 | 9+ are data supported 10 | """ 11 | 12 | StructuredGrid2D = 0 13 | StructuredGrid = 1 14 | UnStructuredTetMesh = 2 15 | P1Unstructured2d = 3 16 | P2Unstructured2d = 4 17 | BaseUnstructured2d = 5 18 | BaseStructured = 6 19 | TetMesh = 10 20 | P2UnstructuredTetMesh = 11 21 | DataSupported = 12 22 | 23 | 24 | from ._2d_base_unstructured import BaseUnstructured2d 25 | from ._2d_p1_unstructured import P1Unstructured2d 26 | from ._2d_p2_unstructured import P2Unstructured2d 27 | from ._2d_structured_grid import StructuredGrid2D 28 | from ._3d_structured_grid import StructuredGrid 29 | from ._3d_unstructured_tetra import UnStructuredTetMesh 30 | from ._3d_structured_tetra import TetMesh 31 | from ._3d_p2_tetra import P2UnstructuredTetMesh 32 | 33 | 34 | def no_support(*args, **kwargs): 35 | return None 36 | 37 | 38 | support_map = { 39 | SupportType.StructuredGrid2D: StructuredGrid2D, 40 | SupportType.StructuredGrid: StructuredGrid, 41 | SupportType.UnStructuredTetMesh: UnStructuredTetMesh, 42 | SupportType.P1Unstructured2d: P1Unstructured2d, 43 | SupportType.P2Unstructured2d: P2Unstructured2d, 44 | SupportType.TetMesh: TetMesh, 45 | SupportType.P2UnstructuredTetMesh: P2UnstructuredTetMesh, 46 | SupportType.DataSupported: no_support, 47 | } 48 | 49 | from ._support_factory import SupportFactory 50 | 51 | __all__ = [ 52 | "BaseUnstructured2d", 53 | "P1Unstructured2d", 54 | "P2Unstructured2d", 55 | "StructuredGrid2D", 56 | "StructuredGrid", 57 | "UnStructuredTetMesh", 58 | "TetMesh", 59 | "P2UnstructuredTetMesh", 60 | "support_map", 61 | "SupportType", 62 | ] 63 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/_aabb.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import sparse 3 | 4 | 5 | def _initialise_aabb(grid): 6 | """assigns the tetras to the grid cells where the bounding box 7 | of the tetra element overlaps the grid cell. 8 | It could be changed to use the separating axis theorem, however this would require 9 | significantly more calculations. (12 more I think).. #TODO test timing 10 | """ 11 | # calculate the bounding box for all tetraherdon in the mesh 12 | # find the min/max extents for xyz 13 | # tetra_bb = np.zeros((grid.elements.shape[0], 19, 3)) 14 | minx = np.min(grid.nodes[grid.elements[:, :4], 0], axis=1) 15 | maxx = np.max(grid.nodes[grid.elements[:, :4], 0], axis=1) 16 | miny = np.min(grid.nodes[grid.elements[:, :4], 1], axis=1) 17 | maxy = np.max(grid.nodes[grid.elements[:, :4], 1], axis=1) 18 | 19 | cell_indexes = grid.aabb_grid.global_index_to_cell_index(np.arange(grid.aabb_grid.n_elements)) 20 | corners = grid.aabb_grid.cell_corner_indexes(cell_indexes) 21 | positions = grid.aabb_grid.node_indexes_to_position(corners) 22 | ## Because we known the node orders just select min/max from each 23 | # coordinate. Use these to check whether the tetra is in the cell 24 | x_boundary = positions[:, [0, 1], 0] 25 | y_boundary = positions[:, [0, 2], 1] 26 | a = np.logical_and( 27 | minx[None, :] > x_boundary[:, None, 0], 28 | minx[None, :] < x_boundary[:, None, 1], 29 | ) # min point between cell 30 | b = np.logical_and( 31 | maxx[None, :] < x_boundary[:, None, 1], 32 | maxx[None, :] > x_boundary[:, None, 0], 33 | ) # max point between cell 34 | c = np.logical_and( 35 | minx[None, :] < x_boundary[:, None, 0], 36 | maxx[None, :] > x_boundary[:, None, 0], 37 | ) # min point < than cell & max point > cell 38 | 39 | x_logic = np.logical_or(np.logical_or(a, b), c) 40 | 41 | a = np.logical_and( 42 | miny[None, :] > y_boundary[:, None, 0], 43 | miny[None, :] < y_boundary[:, None, 1], 44 | ) # min point between cell 45 | b = np.logical_and( 46 | maxy[None, :] < y_boundary[:, None, 1], 47 | maxy[None, :] > y_boundary[:, None, 0], 48 | ) # max point between cell 49 | c = np.logical_and( 50 | miny[None, :] < y_boundary[:, None, 0], 51 | maxy[None, :] > y_boundary[:, None, 0], 52 | ) # min point < than cell & max point > cell 53 | 54 | y_logic = np.logical_or(np.logical_or(a, b), c) 55 | logic = np.logical_and(x_logic, y_logic) 56 | 57 | if grid.dimension == 3: 58 | z_boundary = positions[:, [0, 6], 2] 59 | minz = np.min(grid.nodes[grid.elements[:, :4], 2], axis=1) 60 | maxz = np.max(grid.nodes[grid.elements[:, :4], 2], axis=1) 61 | a = np.logical_and( 62 | minz[None, :] > z_boundary[:, None, 0], 63 | minz[None, :] < z_boundary[:, None, 1], 64 | ) # min point between cell 65 | b = np.logical_and( 66 | maxz[None, :] < z_boundary[:, None, 1], 67 | maxz[None, :] > z_boundary[:, None, 0], 68 | ) # max point between cell 69 | c = np.logical_and( 70 | minz[None, :] < z_boundary[:, None, 0], 71 | maxz[None, :] > z_boundary[:, None, 0], 72 | ) # min point < than cell & max point > cell 73 | 74 | z_logic = np.logical_or(np.logical_or(a, b), c) 75 | logic = np.logical_and(logic, z_logic) 76 | 77 | grid._aabb_table = sparse.csr_matrix(logic) 78 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/_base_support.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | import numpy as np 3 | from typing import Tuple 4 | 5 | 6 | class BaseSupport(metaclass=ABCMeta): 7 | """ 8 | Base support class 9 | """ 10 | 11 | @abstractmethod 12 | def __init__(self): 13 | """ 14 | This class is the base 15 | """ 16 | 17 | @abstractmethod 18 | def evaluate_value(self, evaluation_points: np.ndarray, property_array: np.ndarray): 19 | """ 20 | Evaluate the value of the support at the evaluation points 21 | """ 22 | pass 23 | 24 | @abstractmethod 25 | def evaluate_gradient(self, evaluation_points: np.ndarray, property_array: np.ndarray): 26 | """ 27 | Evaluate the gradient of the support at the evaluation points 28 | """ 29 | pass 30 | 31 | @abstractmethod 32 | def inside(self, pos): 33 | """ 34 | Check if a position is inside the support 35 | """ 36 | pass 37 | 38 | @abstractmethod 39 | def onGeometryChange(self): 40 | """ 41 | Called when the geometry changes 42 | """ 43 | pass 44 | 45 | @abstractmethod 46 | def get_element_for_location( 47 | self, pos: np.ndarray 48 | ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: 49 | """ 50 | Get the element for a location 51 | """ 52 | pass 53 | 54 | @abstractmethod 55 | def get_element_gradient_for_location( 56 | self, pos: np.ndarray 57 | ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: 58 | pass 59 | 60 | @property 61 | @abstractmethod 62 | def elements(self): 63 | """ 64 | Return the elements 65 | """ 66 | pass 67 | 68 | @property 69 | @abstractmethod 70 | def n_elements(self): 71 | """ 72 | Return the number of elements 73 | """ 74 | pass 75 | 76 | @property 77 | @abstractmethod 78 | def n_nodes(self): 79 | """ 80 | Return the number of points 81 | """ 82 | pass 83 | 84 | @property 85 | @abstractmethod 86 | def nodes(self): 87 | """ 88 | Return the nodes 89 | """ 90 | pass 91 | 92 | @property 93 | @abstractmethod 94 | def barycentre(self): 95 | """ 96 | Return the number of dimensions 97 | """ 98 | pass 99 | 100 | @property 101 | @abstractmethod 102 | def dimension(self): 103 | """ 104 | Return the number of dimensions 105 | """ 106 | pass 107 | 108 | @property 109 | @abstractmethod 110 | def element_size(self): 111 | """ 112 | Return the element size 113 | """ 114 | pass 115 | 116 | @abstractmethod 117 | def vtk(self, node_properties={}, cell_properties={}): 118 | """ 119 | Return a vtk object 120 | """ 121 | pass 122 | 123 | @abstractmethod 124 | def set_nelements(self, nelements) -> int: 125 | pass 126 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/_face_table.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import sparse 3 | 4 | 5 | def _init_face_table(grid): 6 | """ 7 | Fill table containing elements that share a face, and another 8 | table that contains the nodes for a face. 9 | """ 10 | # need to identify the shared nodes for pairs of elements 11 | # we do this by creating a sparse matrix that has N rows (number of elements) 12 | # and M columns (number of nodes). 13 | # We then fill the location where a node is in an element with true 14 | # Then we create a table for the pairs of elements in the mesh 15 | # we have the neighbour relationships, which are the 4 neighbours for each element 16 | # create a new table that shows the element index repeated four times 17 | # flatten both of these arrays so we effectively have a table with pairs of neighbours 18 | # disgard the negative neighbours because these are border neighbours 19 | rows = np.tile(np.arange(grid.n_elements)[:, None], (1, grid.dimension + 1)) 20 | elements = grid.elements 21 | neighbours = grid.neighbours 22 | # add array of bool to the location where there are elements for each node 23 | 24 | # use this to determine shared faces 25 | 26 | element_nodes = sparse.coo_matrix( 27 | ( 28 | np.ones(elements.shape[0] * (grid.dimension + 1)), 29 | (rows.ravel(), elements[:, : grid.dimension + 1].ravel()), 30 | ), 31 | shape=(grid.n_elements, grid.n_nodes), 32 | dtype=bool, 33 | ).tocsr() 34 | n1 = np.tile(np.arange(neighbours.shape[0], dtype=int)[:, None], (1, grid.dimension + 1)) 35 | n1 = n1.flatten() 36 | n2 = neighbours.flatten() 37 | n1 = n1[n2 >= 0] 38 | n2 = n2[n2 >= 0] 39 | el_rel = np.zeros((grid.neighbours.flatten().shape[0], 2), dtype=int) 40 | el_rel[:] = -1 41 | el_rel[np.arange(n1.shape[0]), 0] = n1 42 | el_rel[np.arange(n1.shape[0]), 1] = n2 43 | el_rel = el_rel[el_rel[:, 0] >= 0, :] 44 | 45 | # el_rel2 = np.zeros((grid.neighbours.flatten().shape[0], 2), dtype=int) 46 | grid._shared_element_relationships[:] = -1 47 | el_pairs = sparse.coo_matrix((np.ones(el_rel.shape[0]), (el_rel[:, 0], el_rel[:, 1]))).tocsr() 48 | i, j = sparse.tril(el_pairs).nonzero() 49 | grid._shared_element_relationships[: len(i), 0] = i 50 | grid._shared_element_relationships[: len(i), 1] = j 51 | 52 | grid._shared_element_relationships = grid.shared_element_relationships[ 53 | grid.shared_element_relationships[:, 0] >= 0, : 54 | ] 55 | 56 | faces = element_nodes[grid.shared_element_relationships[:, 0], :].multiply( 57 | element_nodes[grid.shared_element_relationships[:, 1], :] 58 | ) 59 | shared_faces = faces[np.array(np.sum(faces, axis=1) == grid.dimension).flatten(), :] 60 | row, col = shared_faces.nonzero() 61 | row = row[row.argsort()] 62 | col = col[row.argsort()] 63 | shared_face_index = np.zeros((shared_faces.shape[0], grid.dimension), dtype=int) 64 | shared_face_index[:] = -1 65 | shared_face_index[row.reshape(-1, grid.dimension)[:, 0], :] = col.reshape(-1, grid.dimension) 66 | grid._shared_elements[np.arange(grid.shared_element_relationships.shape[0]), :] = ( 67 | shared_face_index 68 | ) 69 | # resize 70 | grid._shared_elements = grid.shared_elements[: len(grid.shared_element_relationships), :] 71 | -------------------------------------------------------------------------------- /LoopStructural/interpolators/supports/_support_factory.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.interpolators.supports import support_map, SupportType 2 | import numpy as np 3 | from typing import Optional 4 | 5 | 6 | class SupportFactory: 7 | @staticmethod 8 | def create_support(support_type, **kwargs): 9 | if support_type is None: 10 | raise ValueError("No support type specified") 11 | if isinstance(support_type, str): 12 | support_type = SupportType._member_map_[support_type].numerator 13 | return support_map[support_type](**kwargs) 14 | 15 | @staticmethod 16 | def from_dict(d): 17 | d = d.copy() 18 | support_type = d.pop("type", None) 19 | if support_type is None: 20 | raise ValueError("No support type specified") 21 | return SupportFactory.create_support(support_type, **d) 22 | 23 | @staticmethod 24 | def create_support_from_bbox( 25 | support_type, bounding_box, nelements, element_volume=None, buffer: Optional[float] = None 26 | ): 27 | if isinstance(support_type, str): 28 | support_type = SupportType._member_map_[support_type].numerator 29 | if buffer is not None: 30 | bounding_box = bounding_box.with_buffer(buffer=buffer) 31 | if element_volume is not None: 32 | nelements = int(np.prod(bounding_box.length) / element_volume) 33 | if nelements is not None: 34 | bounding_box.nelements = nelements 35 | 36 | return support_map[support_type]( 37 | origin=bounding_box.origin, 38 | step_vector=bounding_box.step_vector, 39 | nsteps=bounding_box.nsteps, 40 | ) 41 | -------------------------------------------------------------------------------- /LoopStructural/modelling/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Geological modelling classes and functions 3 | 4 | """ 5 | 6 | __all__ = [ 7 | "GeologicalModel", 8 | "ProcessInputData", 9 | "Map2LoopProcessor", 10 | "LoopProjectfileProcessor", 11 | ] 12 | from ..utils import getLogger 13 | from ..utils import LoopImportError 14 | from .core.geological_model import GeologicalModel 15 | 16 | logger = getLogger(__name__) 17 | from ..modelling.input import ( 18 | ProcessInputData, 19 | Map2LoopProcessor, 20 | ) 21 | 22 | try: 23 | from ..modelling.input.project_file import LoopProjectfileProcessor 24 | except (LoopImportError, ImportError): 25 | class LoopProjectfileProcessor(ProcessInputData): 26 | """ 27 | Dummy class to handle the case where LoopProjectFile is not installed. 28 | This will raise a warning when used. 29 | """ 30 | 31 | def __init__(self, *args, **kwargs): 32 | raise LoopImportError( 33 | "LoopProjectFile cannot be imported. Please install LoopProjectFile." 34 | ) 35 | 36 | -------------------------------------------------------------------------------- /LoopStructural/modelling/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/LoopStructural/modelling/core/__init__.py -------------------------------------------------------------------------------- /LoopStructural/modelling/features/__init__.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class FeatureType(IntEnum): 5 | """ """ 6 | 7 | BASE = 0 8 | INTERPOLATED = 1 9 | STRUCTURALFRAME = 2 10 | REGION = 3 11 | FOLDED = 4 12 | ANALYTICAL = 5 13 | LAMBDA = 6 14 | UNCONFORMITY = 7 15 | INTRUSION = 8 16 | FAULT = 9 17 | DOMAINFAULT = 10 18 | INACTIVEFAULT = 11 19 | ONLAPUNCONFORMITY = 12 20 | 21 | 22 | # from .builders._geological_feature_builder import GeologicalFeatureBuilder 23 | from ._base_geological_feature import BaseFeature 24 | from ._geological_feature import GeologicalFeature 25 | from ._lambda_geological_feature import LambdaGeologicalFeature 26 | 27 | # from .builders._geological_feature_builder import GeologicalFeatureBuilder 28 | from ._structural_frame import StructuralFrame 29 | from ._cross_product_geological_feature import CrossProductGeologicalFeature 30 | 31 | from ._unconformity_feature import UnconformityFeature 32 | from ._analytical_feature import AnalyticalGeologicalFeature 33 | from ._projected_vector_feature import ProjectedVectorFeature 34 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/_cross_product_geological_feature.py: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | import numpy as np 4 | from typing import Optional 5 | 6 | from ...modelling.features import BaseFeature 7 | 8 | from ...utils import getLogger 9 | 10 | logger = getLogger(__name__) 11 | 12 | 13 | class CrossProductGeologicalFeature(BaseFeature): 14 | def __init__( 15 | self, 16 | name: str, 17 | geological_feature_a: BaseFeature, 18 | geological_feature_b: BaseFeature, 19 | ): 20 | """ 21 | 22 | Create a geological feature for a vector field using the cross 23 | product between 24 | two existing features 25 | Parameters 26 | ---------- 27 | name: feature name 28 | geological_feature_a: first feature 29 | geological_feature_b: second feature 30 | 31 | 32 | Parameters 33 | ---------- 34 | name : str 35 | name of the feature 36 | geological_feature_a : BaseFeature 37 | Left hand side of cross product 38 | geological_feature_b : BaseFeature 39 | Right hand side of cross product 40 | """ 41 | super().__init__(name) 42 | self.geological_feature_a = geological_feature_a 43 | self.geological_feature_b = geological_feature_b 44 | self.value_feature = None 45 | 46 | def evaluate_gradient(self, locations: np.ndarray, ignore_regions=False) -> np.ndarray: 47 | """ 48 | Calculate the gradient of the geological feature by using numpy to 49 | calculate the cross 50 | product between the two existing feature gradients. 51 | This means both features have to be evaluated for the locations 52 | 53 | Parameters 54 | ---------- 55 | locations 56 | 57 | Returns 58 | ------- 59 | 60 | """ 61 | v1 = self.geological_feature_a.evaluate_gradient(locations, ignore_regions) 62 | # v1 /= np.linalg.norm(v1,axis=1)[:,None] 63 | v2 = self.geological_feature_b.evaluate_gradient(locations, ignore_regions) 64 | # v2 /= np.linalg.norm(v2,axis=1)[:,None] 65 | return np.cross(v1, v2, axisa=1, axisb=1) 66 | 67 | def evaluate_value(self, evaluation_points: np.ndarray, ignore_regions=False) -> np.ndarray: 68 | """ 69 | Return 0 because there is no value for this feature 70 | Parameters 71 | ---------- 72 | evaluation_points 73 | 74 | Returns 75 | ------- 76 | 77 | """ 78 | values = np.zeros(evaluation_points.shape[0]) 79 | if self.value_feature is not None: 80 | values[:] = self.value_feature.evaluate_value(evaluation_points, ignore_regions) 81 | return values 82 | 83 | def mean(self): 84 | if self.value_feature: 85 | return self.value_feature.mean() 86 | return 0.0 87 | 88 | def min(self): 89 | if self.value_feature: 90 | return self.value_feature.min() 91 | return 0.0 92 | 93 | def max(self): 94 | if self.value_feature: 95 | return self.value_feature.max() 96 | return 0.0 97 | 98 | def get_data(self, value_map: Optional[dict] = None): 99 | return 100 | 101 | def copy(self, name: Optional[str] = None): 102 | if name is None: 103 | name = f'{self.name}_copy' 104 | return CrossProductGeologicalFeature( 105 | name, self.geological_feature_a, self.geological_feature_b 106 | ) 107 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/_projected_vector_feature.py: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | import numpy as np 4 | from typing import Optional 5 | 6 | from ...modelling.features import BaseFeature 7 | 8 | from ...utils import getLogger 9 | 10 | logger = getLogger(__name__) 11 | 12 | 13 | class ProjectedVectorFeature(BaseFeature): 14 | def __init__( 15 | self, 16 | name: str, 17 | vector: np.ndarray, 18 | plane_feature: BaseFeature, 19 | ): 20 | """ 21 | 22 | Create a geological feature by projecting a vector onto a feature representing a plane 23 | E.g. project a thickness vector onto an axial surface 24 | 25 | Parameters 26 | ---------- 27 | name: feature name 28 | vector: the vector to project 29 | plane_feature: the plane 30 | 31 | 32 | Parameters 33 | ---------- 34 | name : str 35 | name of the feature 36 | geological_feature_a : BaseFeature 37 | Left hand side of cross product 38 | geological_feature_b : BaseFeature 39 | Right hand side of cross product 40 | """ 41 | super().__init__(name) 42 | self.plane_feature = plane_feature 43 | self.vector = vector 44 | 45 | self.value_feature = None 46 | 47 | def evaluate_gradient(self, locations: np.ndarray, ignore_regions=False) -> np.ndarray: 48 | """ 49 | Calculate the gradient of the geological feature by using numpy to 50 | calculate the cross 51 | product between the two existing feature gradients. 52 | This means both features have to be evaluated for the locations 53 | 54 | Parameters 55 | ---------- 56 | locations 57 | 58 | Returns 59 | ------- 60 | 61 | """ 62 | 63 | # project s0 onto axis plane B X A X B 64 | plane_normal = self.plane_feature.evaluate_gradient(locations, ignore_regions) 65 | vector = np.tile(self.vector, (locations.shape[0], 1)) 66 | 67 | projected_vector = np.cross( 68 | plane_normal, np.cross(vector, plane_normal, axisa=1, axisb=1), axisa=1, axisb=1 69 | ) 70 | return projected_vector 71 | 72 | def evaluate_value(self, evaluation_points: np.ndarray, ignore_regions=False) -> np.ndarray: 73 | """ 74 | Return 0 because there is no value for this feature 75 | Parameters 76 | ---------- 77 | evaluation_points 78 | 79 | Returns 80 | ------- 81 | 82 | """ 83 | values = np.zeros(evaluation_points.shape[0]) 84 | if self.value_feature is not None: 85 | values[:] = self.value_feature.evaluate_value(evaluation_points, ignore_regions) 86 | return values 87 | 88 | def mean(self): 89 | if self.value_feature: 90 | return self.value_feature.mean() 91 | return 0.0 92 | 93 | def min(self): 94 | if self.value_feature: 95 | return self.value_feature.min() 96 | return 0.0 97 | 98 | def max(self): 99 | if self.value_feature: 100 | return self.value_feature.max() 101 | return 0.0 102 | 103 | def get_data(self, value_map: Optional[dict] = None): 104 | return 105 | 106 | def copy(self, name: Optional[str] = None): 107 | if name is None: 108 | name = f'{self.name}_copy' 109 | return ProjectedVectorFeature( 110 | name=name, vector=self.vector, plane_feature=self.plane_feature 111 | ) 112 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/_region.py: -------------------------------------------------------------------------------- 1 | class Region: 2 | def __init__(self, feature, value, sign): 3 | self.feature = feature 4 | self.value = value 5 | self.sign = sign 6 | 7 | def __call__(self, xyz): 8 | if self.sign: 9 | return self.feature.evaluate_value(xyz) > 0 10 | else: 11 | return self.feature.evaluate_value(xyz) < 0 12 | 13 | def to_json(self): 14 | return { 15 | "feature": self.feature.name, 16 | "value": self.value, 17 | "sign": self.sign, 18 | } 19 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/_unconformity_feature.py: -------------------------------------------------------------------------------- 1 | from ...modelling.features import GeologicalFeature 2 | from ...modelling.features import FeatureType 3 | 4 | import numpy as np 5 | 6 | 7 | class UnconformityFeature(GeologicalFeature): 8 | """ """ 9 | 10 | def __init__(self, feature: GeologicalFeature, value: float, sign=True, onlap=False): 11 | """ 12 | 13 | Parameters 14 | ---------- 15 | feature 16 | value 17 | """ 18 | # create a shallow(ish) copy of the geological feature 19 | # just don't link the regions 20 | GeologicalFeature.__init__( 21 | self, 22 | name=f"__{feature.name}_unconformity", 23 | faults=feature.faults, 24 | regions=[], # feature.regions.copy(), # don't want to share regionsbetween unconformity and # feature.regions, 25 | builder=feature.builder, 26 | model=feature.model, 27 | ) 28 | self.value = value 29 | self.type = FeatureType.UNCONFORMITY if onlap is False else FeatureType.ONLAPUNCONFORMITY 30 | self.sign = sign 31 | self.parent = feature 32 | 33 | @property 34 | def faults(self): 35 | return self.parent.faults 36 | 37 | def to_json(self): 38 | json = super().to_json() 39 | json["value"] = self.value 40 | json["sign"] = self.sign 41 | json["parent"] = self.parent.name 42 | return json 43 | 44 | def inverse(self): 45 | """Returns an unconformity feature with the sign flipped 46 | The feature is a shallow copy with the parent being set to 47 | the parent of this feature 48 | 49 | Returns 50 | ------- 51 | UnconformityFeature 52 | _description_ 53 | """ 54 | uc = UnconformityFeature( 55 | self.parent, 56 | self.value, 57 | sign=not self.sign, 58 | onlap=self.type == FeatureType.ONLAPUNCONFORMITY, 59 | ) 60 | uc.name = self.name + "_inverse" 61 | return uc 62 | 63 | def evaluate(self, pos: np.ndarray) -> np.ndarray: 64 | """ 65 | 66 | Parameters 67 | ---------- 68 | pos : numpy array 69 | locations to evaluate whether below or above unconformity 70 | 71 | Returns 72 | ------- 73 | np.ndarray.dtype(bool) 74 | true if above the unconformity, false if below 75 | """ 76 | if self.sign: 77 | return self.evaluate_value(pos) <= self.value 78 | if not self.sign: 79 | return self.evaluate_value(pos) >= self.value 80 | 81 | def __call__(self, pos) -> np.ndarray: 82 | return self.evaluate(pos) 83 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/builders/__init__.py: -------------------------------------------------------------------------------- 1 | from ._base_builder import BaseBuilder 2 | from ._geological_feature_builder import GeologicalFeatureBuilder 3 | from ._folded_feature_builder import FoldedFeatureBuilder 4 | from ._structural_frame_builder import StructuralFrameBuilder 5 | from ._fault_builder import FaultBuilder 6 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/fault/__init__.py: -------------------------------------------------------------------------------- 1 | from ._fault_function import Composite, CubicFunction, Ones, Zeros, FaultDisplacement 2 | from ._fault_function_feature import FaultDisplacementFeature 3 | from ._fault_segment import FaultSegment 4 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/fault/_fault_function_feature.py: -------------------------------------------------------------------------------- 1 | from ....modelling.features import BaseFeature, StructuralFrame 2 | from typing import Optional 3 | from ....utils import getLogger 4 | 5 | logger = getLogger(__name__) 6 | 7 | 8 | class FaultDisplacementFeature(BaseFeature): 9 | """ """ 10 | 11 | def __init__( 12 | self, 13 | fault_frame, 14 | displacement, 15 | name="fault_displacement", 16 | model=None, 17 | faults=[], 18 | regions=[], 19 | builder=None, 20 | ): 21 | """ 22 | Geological feature representing the fault displacement 23 | 24 | Parameters 25 | ---------- 26 | fault_frame - geometry of the fault 27 | displacement - function defining fault displacement 28 | """ 29 | BaseFeature.__init__(self, f"{name}_displacement", model, faults, regions, builder) 30 | self.fault_frame = StructuralFrame( 31 | f"{fault_frame.name}_displacementframe", 32 | [fault_frame[0].copy(), fault_frame[1].copy(), fault_frame[2].copy()], 33 | ) 34 | self.displacement = displacement 35 | 36 | def evaluate_value(self, location): 37 | """ 38 | Return the value of the fault displacement 39 | 40 | Parameters 41 | ---------- 42 | location 43 | 44 | Returns 45 | ------- 46 | 47 | """ 48 | fault_suface = self.fault_frame.features[0].evaluate_value(location) 49 | fault_displacement = self.fault_frame.features[1].evaluate_value(location) 50 | fault_strike = self.fault_frame.features[2].evaluate_value(location) 51 | d = self.displacement(fault_suface, fault_displacement, fault_strike) 52 | return d 53 | 54 | def evaluate_gradient(self, location): 55 | """ 56 | get the scaled displacement 57 | 58 | Parameters 59 | ---------- 60 | location 61 | 62 | Returns 63 | ------- 64 | 65 | """ 66 | fault_suface = self.fault_frame.features[0].evaluate_value(location) 67 | fault_displacement = self.fault_frame.features[1].evaluate_value(location) 68 | fault_strike = self.fault_frame.features[2].evaluate_value(location) 69 | d = self.displacement(fault_suface, fault_displacement, fault_strike) 70 | return d 71 | 72 | def evaluate_on_surface(self, location): 73 | """ 74 | TODO what is this for? 75 | """ 76 | fault_displacement = self.fault_frame.features[1].evaluate_value(location) 77 | fault_strike = self.fault_frame.features[2].evaluate_value(location) 78 | d = self.displacement.evaluate(fault_displacement, fault_strike) 79 | return d 80 | 81 | def get_data(self, value_map: Optional[dict] = None): 82 | pass 83 | 84 | def copy(self, name: Optional[str] = None): 85 | raise NotImplementedError("Not implemented yet") 86 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/fold/__init__.py: -------------------------------------------------------------------------------- 1 | """ """ 2 | 3 | from ._fold import FoldEvent 4 | from ._svariogram import SVariogram 5 | from ._fold_rotation_angle_feature import FoldRotationAngleFeature 6 | from ._foldframe import FoldFrame 7 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/fold/_fold_rotation_angle_feature.py: -------------------------------------------------------------------------------- 1 | from ....modelling.features import BaseFeature 2 | from ....utils import getLogger 3 | 4 | logger = getLogger(__name__) 5 | 6 | 7 | class FoldRotationAngleFeature(BaseFeature): 8 | """ """ 9 | 10 | def __init__( 11 | self, 12 | fold_frame, 13 | rotation, 14 | name="fold_rotation_angle", 15 | model=None, 16 | faults=[], 17 | regions=[], 18 | builder=None, 19 | ): 20 | """ 21 | 22 | Parameters 23 | ---------- 24 | fold_frame 25 | rotation 26 | """ 27 | BaseFeature.__init__(self, f"{name}_displacement", model, faults, regions, builder) 28 | self.fold_frame = fold_frame 29 | self.rotation = rotation 30 | 31 | def evaluate_value(self, location): 32 | """ 33 | 34 | Parameters 35 | ---------- 36 | location 37 | 38 | Returns 39 | ------- 40 | 41 | """ 42 | s1 = self.fold_frame.features[0].evaluate_value(location) 43 | r = self.rotation(s1) 44 | return r 45 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/fold/fold_function/__init__.py: -------------------------------------------------------------------------------- 1 | from ._trigo_fold_rotation_angle import TrigoFoldRotationAngleProfile 2 | from ._fourier_series_fold_rotation_angle import FourierSeriesFoldRotationAngleProfile 3 | from enum import Enum 4 | from typing import Optional 5 | import numpy.typing as npt 6 | import numpy as np 7 | 8 | 9 | class FoldRotationType(Enum): 10 | TRIGONOMETRIC = TrigoFoldRotationAngleProfile 11 | FOURIER_SERIES = FourierSeriesFoldRotationAngleProfile 12 | # ADDITIONAL = AdditionalFoldRotationAngle 13 | 14 | def __str__(self): 15 | return self.name 16 | 17 | def __repr__(self): 18 | return self.name 19 | 20 | 21 | def get_fold_rotation_profile( 22 | fold_rotation_type, 23 | rotation_angle: Optional[npt.NDArray[np.float64]] = None, 24 | fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None, 25 | **kwargs, 26 | ): 27 | return fold_rotation_type.value(rotation_angle, fold_frame_coordinate, **kwargs) 28 | -------------------------------------------------------------------------------- /LoopStructural/modelling/features/fold/fold_function/_lambda_fold_rotation_angle.py: -------------------------------------------------------------------------------- 1 | from ._base_fold_rotation_angle import BaseFoldRotationAngleProfile 2 | import numpy as np 3 | import numpy.typing as npt 4 | from typing import Optional, Callable 5 | from .....utils import getLogger 6 | 7 | logger = getLogger(__name__) 8 | 9 | 10 | class LambdaFoldRotationAngleProfile(BaseFoldRotationAngleProfile): 11 | def __init__( 12 | self, 13 | fn: Callable[[np.ndarray], np.ndarray], 14 | rotation_angle: Optional[npt.NDArray[np.float64]] = None, 15 | fold_frame_coordinate: Optional[npt.NDArray[np.float64]] = None, 16 | ): 17 | """The fold frame function using the lambda profile from Laurent 2016 18 | 19 | Parameters 20 | ---------- 21 | rotation_angle : npt.NDArray[np.float64], optional 22 | the calculated fold rotation angle from observations in degrees, by default None 23 | fold_frame_coordinate : npt.NDArray[np.float64], optional 24 | fold frame coordinate scalar field value, by default None 25 | lambda_ : float, optional 26 | lambda parameter, by default 0 27 | """ 28 | super().__init__(rotation_angle, fold_frame_coordinate) 29 | self._function = fn 30 | 31 | @property 32 | def params(self): 33 | return {} 34 | 35 | def update_params(self, params): 36 | pass 37 | 38 | def initial_guess( 39 | self, 40 | wavelength: float | None = None, 41 | calculate_wavelength: bool = True, 42 | svariogram_parameters: dict = {}, 43 | reset: bool = False, 44 | ) -> np.ndarray: 45 | return np.array([]) 46 | -------------------------------------------------------------------------------- /LoopStructural/modelling/input/__init__.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.modelling.input.map2loop_processor import Map2LoopProcessor 2 | from LoopStructural.modelling.input.process_data import ProcessInputData 3 | -------------------------------------------------------------------------------- /LoopStructural/modelling/input/fault_network.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class FaultNetwork: 5 | def __init__(self, faults): 6 | """A fault network is a basic graph structure that 7 | can return the faults for building a geological model 8 | 9 | Parameters 10 | ---------- 11 | faults : list 12 | list of fault names 13 | """ 14 | self.faults = faults 15 | self.fault_edge_count = np.zeros(len(faults), dtype=int) 16 | self.fault_edges = dict(zip(faults, np.arange(len(faults), dtype=int))) 17 | self.fault_edge_properties = {} 18 | # connections 19 | self.connections = {} 20 | 21 | def add_connection(self, fault1, fault2, properties=None): 22 | """fault 1 is younger than fault2 23 | 24 | Parameters 25 | ---------- 26 | fault1 : string 27 | name of younger fault 28 | fault2 : string 29 | name of older fault 30 | """ 31 | self.connections[fault2] = fault1 32 | self.fault_edge_properties[(fault1, fault2)] = properties 33 | # self.fault_edge_count[self.fault_edges[fault1]] +=1 34 | self.fault_edge_count[self.fault_edges[fault1]] += 1 35 | 36 | def get_fault_iterators(self): 37 | """ 38 | Returns 39 | ------- 40 | iterators : list 41 | list of fault iterators 42 | """ 43 | fault_idxs = np.where(self.fault_edge_count == 0)[0] 44 | iters = [] 45 | for f in fault_idxs: 46 | fault = self.faults[f] 47 | iters.append(FaultNetworkIter(fault, self)) 48 | return iters 49 | 50 | 51 | class FaultNetworkIter: 52 | """Iterator object to return the next oldest fault in a fault network following edges""" 53 | 54 | def __init__(self, faultname, fault_network): 55 | """[summary] 56 | 57 | Parameters 58 | ---------- 59 | faultname : string 60 | unique name of the fault 61 | fault_network : FaultNetwork 62 | the fault network with edges 63 | """ 64 | self.faultname = faultname 65 | self.fault_network = fault_network 66 | 67 | def __next__(self): 68 | """next method for iterator 69 | 70 | Returns 71 | ------- 72 | FaultNetworkIterator 73 | iterator for the next fault, None if the fault is end of an edge 74 | """ 75 | if self.faultname in self.fault_network.connections: 76 | return FaultNetworkIter( 77 | self.fault_network.connections[self.faultname], self.fault_network 78 | ) 79 | else: 80 | return None 81 | -------------------------------------------------------------------------------- /LoopStructural/modelling/intrusions/__init__.py: -------------------------------------------------------------------------------- 1 | from .intrusion_feature import IntrusionFeature 2 | from .intrusion_frame_builder import IntrusionFrameBuilder 3 | from .intrusion_builder import IntrusionBuilder 4 | from .geom_conceptual_models import ( 5 | ellipse_function, 6 | constant_function, 7 | obliquecone_function, 8 | ) 9 | from .geometric_scaling_functions import ( 10 | geometric_scaling_parameters, 11 | thickness_from_geometric_scaling, 12 | contact_pts_using_geometric_scaling, 13 | ) 14 | 15 | __all__ = [ 16 | "IntrusionFeature", 17 | "IntrusionFrameBuilder", 18 | "IntrusionBuilder", 19 | "ellipse_function", 20 | "constant_function", 21 | "obliquecone_function", 22 | "geometric_scaling_parameters", 23 | "thickness_from_geometric_scaling", 24 | "contact_pts_using_geometric_scaling", 25 | ] 26 | -------------------------------------------------------------------------------- /LoopStructural/modelling/intrusions/geometric_scaling_functions.py: -------------------------------------------------------------------------------- 1 | # import scipy as sc 2 | import scipy.stats as sct 3 | 4 | import numpy as np 5 | import pandas as pd 6 | 7 | from ...utils import getLogger, rng 8 | 9 | logger = getLogger(__name__) 10 | 11 | 12 | def geometric_scaling_parameters( 13 | intrusion_type: str, 14 | ): 15 | """ 16 | Get geometric scaling parameters for a given intrusion type 17 | 18 | Parameters 19 | ---------- 20 | intrusion_type : str 21 | intrusion type 22 | 23 | Returns 24 | ------- 25 | tuple(float, float, float, float) 26 | scaling parameters 27 | """ 28 | geom_scaling_a_avg = { 29 | "plutons": 0.81, 30 | "laccoliths": 0.92, 31 | "major_mafic_sills": 0.85, 32 | "mesoscale_mafic_sills": 0.49, 33 | "minor_mafic_sills": 0.91, 34 | } 35 | geom_scaling_a_stdv = { 36 | "plutons": 0.12, 37 | "laccoliths": 0.11, 38 | "major_mafic_sills": 0.1, 39 | "mesoscale_mafic_sills": 0.13, 40 | "minor_mafic_sills": 0.25, 41 | } 42 | geom_scaling_b_avg = { 43 | "plutons": 1.08, 44 | "laccoliths": 0.12, 45 | "major_mafic_sills": 0.01, 46 | "mesoscale_mafic_sills": 0.47, 47 | "minor_mafic_sills": 0.27, 48 | } 49 | geom_scaling_b_stdv = { 50 | "plutons": 1.38, 51 | "laccoliths": 0.02, 52 | "major_mafic_sills": 0.02, 53 | "mesoscale_mafic_sills": 0.33, 54 | "minor_mafic_sills": 0.04, 55 | } 56 | 57 | a_avg = geom_scaling_a_avg.get(intrusion_type) 58 | a_stdv = geom_scaling_a_stdv.get(intrusion_type) 59 | b_avg = geom_scaling_b_avg.get(intrusion_type) 60 | b_stdv = geom_scaling_b_stdv.get(intrusion_type) 61 | 62 | return a_avg, a_stdv, b_avg, b_stdv 63 | 64 | 65 | def thickness_from_geometric_scaling(length: float, intrusion_type: str) -> float: 66 | """Calculate thickness of intrusion using geometric scaling parameters 67 | 68 | Parameters 69 | ---------- 70 | length : float 71 | intrusion length 72 | intrusion_type : str 73 | type of intrusion 74 | 75 | Returns 76 | ------- 77 | float 78 | thickness of intrusion 79 | """ 80 | 81 | a_avg, a_stdv, b_avg, b_stdv = geometric_scaling_parameters(intrusion_type) 82 | 83 | n_realizations = 10000 84 | maxT = 0 85 | a = sct.norm.ppf(rng.random(n_realizations), loc=a_avg, scale=a_stdv) 86 | b = sct.norm.ppf(rng.random(n_realizations), loc=b_avg, scale=b_stdv) 87 | maxT = b * np.power(length, a) 88 | maxT[maxT < 0] = None 89 | mean_t = np.nanmean(maxT) 90 | 91 | logger.info("Building intrusion of thickness {}".format(mean_t)) 92 | 93 | return mean_t 94 | 95 | 96 | def contact_pts_using_geometric_scaling( 97 | thickness: float, points_df: pd.DataFrame, inflation_vector: np.ndarray 98 | ): 99 | """Generate contact points for an intrusion using geometric scaling parameter and the 100 | inflation vector to translate the points 101 | 102 | Parameters 103 | ---------- 104 | thickness : float 105 | intrusion thickness 106 | points_df : pd.DataFrame 107 | dataframe of contact points 108 | inflation_vector : np.ndarray 109 | inflation direction of the intrusion 110 | 111 | Returns 112 | ------- 113 | tuple 114 | contact points 115 | """ 116 | translation_vector = ( 117 | inflation_vector 118 | / np.linalg.norm(inflation_vector, axis=1).reshape(1, len(inflation_vector)).T 119 | ) * thickness 120 | points_translated = points_df.loc[:, ["X", "Y", "Z"]].copy() + translation_vector 121 | points_translated_xyz = points_translated.to_numpy() 122 | 123 | return points_translated, points_translated_xyz 124 | -------------------------------------------------------------------------------- /LoopStructural/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utils 3 | ===== 4 | """ 5 | 6 | from .logging import getLogger, log_to_file, log_to_console, get_levels 7 | from .exceptions import ( 8 | LoopException, 9 | LoopImportError, 10 | InterpolatorError, 11 | LoopTypeError, 12 | LoopValueError, 13 | ) 14 | from ._transformation import EuclideanTransformation 15 | from .helper import ( 16 | get_data_bounding_box, 17 | get_data_bounding_box_map, 18 | ) 19 | 20 | # from ..datatypes._bounding_box import BoundingBox 21 | from .maths import ( 22 | get_dip_vector, 23 | get_strike_vector, 24 | get_vectors, 25 | strikedip2vector, 26 | plungeazimuth2vector, 27 | azimuthplunge2vector, 28 | normal_vector_to_strike_and_dip, 29 | rotate, 30 | ) 31 | from .helper import create_surface, create_box 32 | from .regions import RegionEverywhere, RegionFunction, NegativeRegion, PositiveRegion 33 | 34 | from .json_encoder import LoopJSONEncoder 35 | import numpy as np 36 | 37 | rng = np.random.default_rng() 38 | 39 | from ._surface import LoopIsosurfacer, surface_list 40 | from .colours import random_colour, random_hex_colour 41 | -------------------------------------------------------------------------------- /LoopStructural/utils/colours.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.utils import rng 2 | 3 | 4 | def random_colour(n: int = 1, cmap='tab20'): 5 | """ 6 | Generate a list of random colours 7 | 8 | Parameters 9 | ---------- 10 | n : int 11 | Number of colours to generate 12 | cmap : str, optional 13 | Name of the matplotlib colour map to use, by default 'tab20' 14 | 15 | Returns 16 | ------- 17 | list 18 | List of colours in the form of (r,g,b,a) tuples 19 | """ 20 | from matplotlib import colormaps as cm 21 | 22 | colours = [] 23 | for _i in range(n): 24 | colours.append(cm.get_cmap(cmap)(rng.random())) 25 | 26 | return colours 27 | 28 | 29 | def random_hex_colour(n: int = 1, cmap='tab20'): 30 | """ 31 | Generate a list of random colours 32 | 33 | Parameters 34 | ---------- 35 | n : int 36 | Number of colours to generate 37 | cmap : str, optional 38 | Name of the matplotlib colour map to use, by default 'tab20' 39 | 40 | Returns 41 | ------- 42 | list 43 | List of colours in the form of hex strings 44 | """ 45 | from matplotlib import colormaps as cm 46 | 47 | colours = [] 48 | for _i in range(n): 49 | colours.append(cm.get_cmap(cmap)(rng.random())) 50 | 51 | return [f'#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}' for c in colours] 52 | -------------------------------------------------------------------------------- /LoopStructural/utils/config.py: -------------------------------------------------------------------------------- 1 | class LoopStructuralConfig: 2 | """ 3 | Class to store configuration settings. 4 | """ 5 | 6 | __splay_fault_threshold = 30 7 | __experimental = False 8 | __default_interpolator = "FDI" 9 | __default_nelements = 1e4 10 | __default_solver = "cg" 11 | 12 | # @property 13 | # def experimental(): 14 | # return __experimental 15 | 16 | # @experimental.setter 17 | # def experimental(self, value): 18 | # __experimental = value 19 | -------------------------------------------------------------------------------- /LoopStructural/utils/dtm_creator.py: -------------------------------------------------------------------------------- 1 | from ctypes import Union 2 | from pathlib import Path 3 | 4 | 5 | def create_dtm_with_rasterio(dtm_path: Union[str, Path]): 6 | try: 7 | import rasterio 8 | except ImportError: 9 | print("rasterio not installed. Please install it and try again.") 10 | return 11 | try: 12 | from map2loop.map import MapUtil 13 | 14 | dtm_map = MapUtil(None, dtm=rasterio.open(dtm_path)) 15 | return lambda xyz: dtm_map.evaluate_dtm_at_points(xyz[:, :2]) 16 | except ImportError: 17 | print("map2loop not installed. Please install it and try again") 18 | -------------------------------------------------------------------------------- /LoopStructural/utils/exceptions.py: -------------------------------------------------------------------------------- 1 | from ..utils import getLogger 2 | 3 | logger = getLogger(__name__) 4 | 5 | 6 | class LoopException(Exception): 7 | """ 8 | Base loop exception 9 | """ 10 | 11 | 12 | class LoopImportError(LoopException): 13 | """ """ 14 | 15 | def __init__(self, message, additional_information=None): 16 | super().__init__(message) 17 | self.additional_information = additional_information 18 | 19 | pass 20 | 21 | 22 | class InterpolatorError(LoopException): 23 | pass 24 | 25 | 26 | class LoopTypeError(LoopException): 27 | pass 28 | 29 | 30 | class LoopValueError(LoopException): 31 | pass 32 | -------------------------------------------------------------------------------- /LoopStructural/utils/features.py: -------------------------------------------------------------------------------- 1 | from ..modelling.features import LambdaGeologicalFeature 2 | 3 | X = LambdaGeologicalFeature(lambda pos: pos[:, 0], name="x") 4 | Y = LambdaGeologicalFeature(lambda pos: pos[:, 1], name="y") 5 | Z = LambdaGeologicalFeature(lambda pos: pos[:, 2], name="z") 6 | -------------------------------------------------------------------------------- /LoopStructural/utils/json_encoder.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class LoopJSONEncoder(json.JSONEncoder): 5 | def default(self, obj): 6 | """All jsonable loop objects should have a tojson method 7 | 8 | Parameters 9 | ---------- 10 | obj : LoopStructuralObject 11 | An object from loopstructural 12 | 13 | Returns 14 | ------- 15 | str 16 | string representing the json encoding 17 | """ 18 | return obj.__tojson__() 19 | -------------------------------------------------------------------------------- /LoopStructural/utils/linalg.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def normalise(v): 5 | v = np.array(v) 6 | 7 | np.linalg.norm(v, axis=1) 8 | return v / np.linalg.norm(v) 9 | -------------------------------------------------------------------------------- /LoopStructural/utils/logging.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import LoopStructural 3 | import os 4 | 5 | 6 | def get_levels(): 7 | """dict for converting to logger levels from string 8 | 9 | 10 | Returns 11 | ------- 12 | dict 13 | contains all strings with corresponding logging levels. 14 | """ 15 | return { 16 | "info": logging.INFO, 17 | "warning": logging.WARNING, 18 | "error": logging.ERROR, 19 | "debug": logging.DEBUG, 20 | } 21 | 22 | 23 | def getLogger(name): 24 | logger = logging.getLogger(name) 25 | logger.addHandler(LoopStructural.ch) 26 | # don't pass message back up the chain, what an odd default behavior 27 | logger.propagate = False 28 | # store the loopstructural loggers so we can change values 29 | LoopStructural.loggers[name] = logger 30 | return logger 31 | 32 | 33 | def log_to_file(filename, overwrite=True, level="info"): 34 | """Set the logging parameters for log file 35 | 36 | 37 | Parameters 38 | ---------- 39 | filename : string 40 | name of file or path to file 41 | level : str, optional 42 | 'info', 'warning', 'error', 'debug' mapped to logging levels, by default 'info' 43 | """ 44 | logger = getLogger(__name__) 45 | if os.path.isfile(filename): 46 | logger.warning("Overwriting existing logfile. To avoid this, set overwrite=False") 47 | os.remove(filename) 48 | levels = get_levels() 49 | level = levels.get(level, logging.WARNING) 50 | fh = logging.FileHandler(filename) 51 | fh.setFormatter(LoopStructural.formatter) 52 | fh.setLevel(level) 53 | for logger in LoopStructural.loggers.values(): 54 | for hdlr in logger.handlers[:]: # remove the existing file handlers 55 | if isinstance(hdlr, logging.FileHandler): # fixed two typos here 56 | logger.removeHandler(hdlr) 57 | logger.addHandler(fh) 58 | logger.setLevel(level) 59 | 60 | 61 | def log_to_console(level="warning"): 62 | """Set the level of logging to the console 63 | 64 | 65 | Parameters 66 | ---------- 67 | level : str, optional 68 | 'info', 'warning', 'error', 'debug' mapped to logging levels, by default 'info' 69 | """ 70 | levels = get_levels() 71 | level = levels.get(level, logging.WARNING) 72 | for logger in LoopStructural.loggers.values(): 73 | for hdlr in logger.handlers: 74 | # both stream and file are base stream, so check if not a filehandler 75 | if not isinstance(hdlr, logging.FileHandler): 76 | logger.removeHandler(hdlr) 77 | hdlr = LoopStructural.ch 78 | hdlr.setLevel(level) 79 | logger.addHandler(hdlr) 80 | -------------------------------------------------------------------------------- /LoopStructural/utils/typing.py: -------------------------------------------------------------------------------- 1 | from typing import TypeVar, Union, List 2 | import numbers 3 | 4 | T = TypeVar("T") 5 | Array = Union[List[T]] 6 | 7 | NumericInput = Union[numbers.Number, Array[numbers.Number]] 8 | -------------------------------------------------------------------------------- /LoopStructural/utils/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import re 3 | from ..utils import getLogger 4 | 5 | logger = getLogger(__name__) 6 | 7 | 8 | def strike_symbol(strike): 9 | R = np.zeros((2, 2)) 10 | R[0, 0] = np.cos(np.deg2rad(-strike)) 11 | R[0, 1] = -np.sin(np.deg2rad(-strike)) 12 | R[1, 0] = np.sin(np.deg2rad(-strike)) 13 | R[1, 1] = np.cos(np.deg2rad(-strike)) 14 | R = np.zeros((2, 2)) 15 | R[0, 0] = np.cos(np.deg2rad(-strike)) 16 | R[0, 1] = -np.sin(np.deg2rad(-strike)) 17 | R[1, 0] = np.sin(np.deg2rad(-strike)) 18 | R[1, 1] = np.cos(np.deg2rad(-strike)) 19 | 20 | vec = np.array([0, 1]) 21 | rotated = R @ vec 22 | vec2 = np.array([-0.5, 0]) 23 | r2 = R @ vec2 24 | return rotated, r2 25 | 26 | 27 | def read_voxet(voxetname, propertyfile): 28 | """ 29 | Read a gocad property file and the geometry information from the .vo file 30 | voxetname - is the path to the voxet file 31 | propertyfile is the path to the binary file 32 | Returns 33 | origin numpy array 34 | voxet_extent - is the length of each axis of the voxet 35 | N is the number of steps in the voxet 36 | array is the property values 37 | steps is the size of the step vector for the voxet 38 | """ 39 | array = np.fromfile(propertyfile, dtype="float32") 40 | array = array.astype("f4") # big endian 67 | # array = propertyvalues.newbyteorder() 68 | propertyvalues.tofile(propertyfilename) 69 | -------------------------------------------------------------------------------- /LoopStructural/version.py: -------------------------------------------------------------------------------- 1 | __version__ = "1.6.14" 2 | -------------------------------------------------------------------------------- /LoopStructural/visualisation/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from loopstructuralvisualisation import ( 3 | Loop3DView, 4 | RotationAnglePlotter, 5 | Loop2DView, 6 | StratigraphicColumnView, 7 | ) 8 | except ImportError as e: 9 | print("Please install the loopstructuralvisualisation package") 10 | print("pip install loopstructuralvisualisation") 11 | raise e 12 | -------------------------------------------------------------------------------- /conda/conda_build_config.yaml: -------------------------------------------------------------------------------- 1 | numpy: 2 | - 1.24 3 | python: 4 | - 3.8 5 | - 3.9 6 | - 3.10 7 | - 3.11 8 | - 3.12 9 | -------------------------------------------------------------------------------- /conda/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = "loopstructural" %} 2 | 3 | package: 4 | name: "{{ name|lower }}" 5 | version: "{{ environ.get('GIT_DESCRIBE_TAG', '') }}" 6 | 7 | source: 8 | git_url: https://github.com/Loop3D/LoopStructural 9 | 10 | build: 11 | number: 0 12 | script: "{{ PYTHON }} -m pip install ." 13 | 14 | requirements: 15 | host: 16 | - python 17 | - setuptools 18 | - pip 19 | run: 20 | - python 21 | - numpy >=1.18 22 | - pandas 23 | - scipy >=1.10 24 | - scikit-image 25 | - scikit-learn 26 | - tqdm 27 | 28 | test: 29 | imports: 30 | - LoopStructural 31 | commands: 32 | - pip check 33 | requires: 34 | - pip 35 | 36 | 37 | about: 38 | home: "https://github.com/Loop3D/LoopStructural" 39 | license: MIT 40 | license_family: MIT 41 | license_file: 42 | summary: "Implicit 3D geological modelling library" 43 | doc_url: "https://loop3d.github.io/LoopStructural/" 44 | dev_url: 45 | 46 | extra: 47 | recipe-maintainers: 48 | - lachlangrose 49 | -------------------------------------------------------------------------------- /docker-compose-win.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | structural: 5 | build: 6 | context: ./ 7 | dockerfile: DockerfileDev 8 | volumes: 9 | - C:\Users\lachl\OneDrive\Documents\GitHub\LoopStructural:/home/jovyan/LoopStructural 10 | - C:\Users\lachl\OneDrive\Documents\Loop\notebooks:/home/jovyan/notebooks 11 | ports: 12 | - 8888:8888 13 | - 8050:8050 14 | - 8080-8090:8080-8090 15 | # command: jupyter notebook --ip='0.0.0.0' --NotebookApp.token='' --no-browser 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | structural: 5 | build: 6 | context: ./ 7 | dockerfile: DockerfileDev 8 | volumes: 9 | - /home/lgrose/dev/python/LoopStructural/:/home/jovyan/LoopStructural 10 | - /home/lgrose/LoopStructural/:/home/jovyan/notebooks 11 | - /home/lgrose/dev/python/map2loop-2/:/home/jovyan/map2loop 12 | - /home/lgrose/dev/python/LoopProjectFile/:/home/jovyan/LoopProjectFile 13 | - /home/lgrose/dev/fortran/tomofast/:/home/jovyan/tomofast 14 | ports: 15 | - 8888:8888 16 | - 8050:8050 17 | - 8080-8090:8080-8090 18 | # command: jupyter notebook --ip='0.0.0.0' --NotebookApp.token='' --no-browser 19 | -------------------------------------------------------------------------------- /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 | xvfb 17 | RUN conda install -c conda-forge\ 18 | -c loop3d\ 19 | # python"<=3.8"\ 20 | cython\ 21 | numpy\ 22 | pandas\ 23 | scipy\ 24 | matplotlib\ 25 | sphinx\ 26 | sphinx-gallery\ 27 | myst-parser\ 28 | scikit-learn\ 29 | scikit-image\ 30 | networkx\ 31 | # geopandas\ 32 | libstdcxx-ng\ 33 | meshio\ 34 | python=3.10\ 35 | pydata-sphinx-theme\ 36 | pyvista\ 37 | loopstructuralvisualisation\ 38 | -y 39 | RUN pip install git+https://github.com/geopandas/geopandas.git@v0.10.2 40 | RUN pip install geoh5py 41 | RUN pip install sphinxcontrib-bibtex 42 | ENV TINI_VERSION v0.19.0 43 | ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini 44 | RUN chmod +x /tini 45 | ENTRYPOINT ["/tini", "--"] 46 | 47 | RUN mkdir LoopStructural 48 | -------------------------------------------------------------------------------- /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 | HTMLCOPYDIR = build/html 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /docs/build_docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # set -x 3 | # export DISPLAY=:99.0 4 | # export PYVISTA_OFF_SCREEN=true 5 | # which Xvfb 6 | # Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 7 | # sleep 3 8 | # set +x 9 | # exec "$@" 10 | pip install ./LoopStructural 11 | pip install loopstructuralvisualisation[all] 12 | make -C LoopStructural/docs html -------------------------------------------------------------------------------- /docs/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | structural: 5 | build: 6 | context: ./ 7 | volumes: 8 | - ../:/LoopStructural 9 | command: ./LoopStructural/docs/build_docs.sh 10 | tty: true -------------------------------------------------------------------------------- /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 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 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/mv_docs.sh: -------------------------------------------------------------------------------- 1 | cp -rT build/html ../../LoopStructuralDoc 2 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==3.5.4 2 | sphinx_gallery 3 | sphinxcontrib-bibtex 4 | pydata_sphinx_theme 5 | myst-parser -------------------------------------------------------------------------------- /docs/source/API.rst: -------------------------------------------------------------------------------- 1 | API 2 | --- 3 | 4 | 5 | .. autosummary:: 6 | :caption: API 7 | :toctree: _autosummary 8 | :template: custom-module-template.rst 9 | :recursive: 10 | 11 | LoopStructural 12 | LoopStructural.modelling 13 | LoopStructural.interpolators 14 | LoopStructural.visualisation 15 | LoopStructural.datatypes 16 | -------------------------------------------------------------------------------- /docs/source/_static/custom-icon.js: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * Set a custom icon for pypi as it's not available in the fa built-in brands 3 | */ 4 | FontAwesome.library.add( 5 | (faListOldStyle = { 6 | prefix: "fa-custom", 7 | iconName: "pypi", 8 | icon: [ 9 | 17.313, // viewBox width 10 | 19.807, // viewBox height 11 | [], // ligature 12 | "e001", // unicode codepoint - private use area 13 | "m10.383 0.2-3.239 1.1769 3.1883 1.1614 3.239-1.1798zm-3.4152 1.2411-3.2362 1.1769 3.1855 1.1614 3.2369-1.1769zm6.7177 0.00281-3.2947 1.2009v3.8254l3.2947-1.1988zm-3.4145 1.2439-3.2926 1.1981v3.8254l0.17548-0.064132 3.1171-1.1347zm-6.6564 0.018325v3.8247l3.244 1.1805v-3.8254zm10.191 0.20931v2.3137l3.1777-1.1558zm3.2947 1.2425-3.2947 1.1988v3.8254l3.2947-1.1988zm-8.7058 0.45739c0.00929-1.931e-4 0.018327-2.977e-4 0.027485 0 0.25633 0.00851 0.4263 0.20713 0.42638 0.49826 1.953e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36226 0.13215-0.65608-0.073306-0.65613-0.4588-6.28e-5 -0.38556 0.2938-0.80504 0.65613-0.93662 0.068422-0.024919 0.13655-0.038114 0.20156-0.039466zm5.2913 0.78369-3.2947 1.1988v3.8247l3.2947-1.1981zm-10.132 1.239-3.2362 1.1769 3.1883 1.1614 3.2362-1.1769zm6.7177 0.00213-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2439-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.016195v3.8275l3.244 1.1805v-3.8254zm16.9 0.21143-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm-3.4124 1.2432-3.2947 1.1988v3.8254l3.2947-1.1988zm-6.6585 0.019027v3.8247l3.244 1.1805v-3.8254zm13.485 1.4497-3.2947 1.1988v3.8247l3.2947-1.1981zm-3.4145 1.2411-3.2926 1.2016v3.8247l3.2926-1.2009zm2.4018 0.38127c0.0093-1.83e-4 0.01833-3.16e-4 0.02749 0 0.25633 0.0085 0.4263 0.20713 0.42638 0.49826 1.97e-4 0.38532-0.29327 0.80469-0.65542 0.93662-0.36188 0.1316-0.65525-0.07375-0.65542-0.4588-1.95e-4 -0.38532 0.29328-0.80469 0.65542-0.93662 0.06842-0.02494 0.13655-0.03819 0.20156-0.03947zm-5.8142 0.86403-3.244 1.1805v1.4201l3.244 1.1805z", // svg path (https://simpleicons.org/icons/pypi.svg) 14 | ], 15 | }) 16 | ); 17 | -------------------------------------------------------------------------------- /docs/source/_static/docker.rst: -------------------------------------------------------------------------------- 1 | Docker 2 | ~~~~~~ 3 | 4 | .. container:: toggle 5 | 6 | .. container:: header 7 | 8 | **Installing Docker** 9 | Follow the installation instructions for docker `here `_. 10 | 11 | .. container:: toggle 12 | 13 | .. container:: header 14 | 15 | **Building and running a docker image** 16 | Using a github client (e.g. for windows Github Desktop) 17 | Clone this repository to your local drive and change 18 | directory to the location where you cloned it using 19 | :code:`cd ${LOOPSTRUCTURAL_FOLDER}`. The docker 20 | container can be built by running the following command 21 | .. code-block::console 22 | 23 | docker build -t=loop . 24 | 25 | LoopStructural can be used by running 26 | 27 | .. code-block::console 28 | 29 | run -i -t -p 8888:8888 loop 30 | 31 | This will start a jupyter notebook server running on :code:`localhost:8888` 32 | without password or certificate required. Be aware any changes made 33 | to the notebooks within the notebooks directory will NOT be saved on 34 | your local versions. 35 | 36 | If you want to use your own data with the docker container you will need 37 | to link your local directory (this can be anywhere) with the docker container. 38 | To do this add :code:`-v LOCALDIRPATH:/home/jovyan/shared_volume` to the docker command 39 | so it becomes :code:`docker run -i -t -p 8888:8888 -v LOCALDIRPATH:/home/jovyan/shared_volume loop`. 40 | :code:`LOCALDIRPATH` is the relative or full path to the directory you want to share. -------------------------------------------------------------------------------- /docs/source/_static/infinity_loop_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/source/_static/overview.rst: -------------------------------------------------------------------------------- 1 | LoopStructural Philosophy 2 | ========================== 3 | Loopstructural is an open source 3D geological modelling library with minimal dependencies - *numpy, scipy and scikit-learn for model building and evaluation*. 4 | For visualisation *LavaVu and scikit-image* are required. 5 | 6 | The main entry point for using LoopStructural is to create a GeologicalModel. 7 | The GeologicalModel manages the dataset, model area, creating of different elements inside the GeologicalModel. 8 | Within the GeologicalModel, different elements in the model are represented by GeologicalFeatures. 9 | 10 | GeologicalModel 11 | ~~~~~~~~~~~~~~~ 12 | * Manages the order of the geological features 13 | * Creates geological features, addings 14 | * Rescales data/region 15 | * Relates geological features - e.g. faults are added to stratigraphy 16 | 17 | GeologicalFeature 18 | ~~~~~~~~~~~~~~~~~~ 19 | A base GeologicalFeature is something that can be evaluated anywhere in the model space and return the value of the scalar field 20 | and the orientation of the gradient of the scalar field. 21 | * Interprets scalar field for geology 22 | 23 | GeologicalFeatureBuilder 24 | ~~~~~~~~~~~~~~~~~~~~~~~~ 25 | 26 | StructuralFrame 27 | ~~~~~~~~~~~~~~~ 28 | * Curvilinear coordinate system made up of 3 geological features that relate to the same feature being modelled 29 | * 30 | 31 | StructuralFrameBuilder 32 | ~~~~~~~~~~~~~~~~~~~~~~ 33 | 34 | GeologicalInterpolator 35 | ~~~~~~~~~~~~~~~~~~~~~~ 36 | 37 | 38 | DiscreteInterpolator 39 | ~~~~~~~~~~~~~~~~~~~~ 40 | 41 | 42 | PiecewiseLinearInterpolator 43 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 44 | 45 | 46 | FiniteDifferenceInterpolator 47 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 48 | 49 | 50 | -------------------------------------------------------------------------------- /docs/source/_static/ubuntu_install.rst: -------------------------------------------------------------------------------- 1 | 2 | Linux 3 | ~~~~~~~~~~~~ 4 | .. container:: toggle 5 | 6 | .. container:: header 7 | 8 | **Development environment** 9 | 10 | 11 | LoopStructural can be easily installed using a Makefile once a few things are set up. Firstly, you need to add an 12 | environment variable to your system. :code:`LOOP_ENV`, this can be done by adding 13 | 14 | .. code-block:: 15 | 16 | export LOOP_ENV=$YOUR_PATH_TO_VIRTUAL_ENVIRONMENT 17 | 18 | to your :code:`.bashrc` file. 19 | Make sure the path is updated to a directory in your system where you want to save the python virtual environment. 20 | It could be for example where you clone this repository and a subfolder called venv or LoopStructural. 21 | 22 | Once you have the environment variable you can run the command :code:`make dependencies` (or :code:`make dependencies.fc` for Fedora) which will install the required dependencies for LoopStructural: 23 | 24 | Required dependencies for Ubuntu 25 | 26 | * python3 27 | * python3-dev 28 | * python3-venv 29 | * pybind11-dev 30 | * mesa-common-dev 31 | * mesa-utils 32 | * libgl1-mesa-dev 33 | * gcc 34 | * g++ 35 | 36 | .. code-block:: 37 | 38 | sudo apt-get install python3 python3-dev python3-venv pybind11-dev mesa-common-dev mesa-utils libgl1-mesa-dev gcc g++ 39 | 40 | Required dependencies for Fedora 41 | 42 | * python3 43 | * python3-devel 44 | * pybind11-devel 45 | * mesa-libGL-devel 46 | * gcc 47 | * g++ 48 | 49 | .. code-block:: 50 | 51 | sudo dnf install python3 python3-devel pybind11-devel mesa-libGL-devel gcc g++ 52 | 53 | Once these are installed you can run :code:`make venv` to create a new python virtual environment in the location you 54 | specified. If a python environment already exists then this will be used. 55 | 56 | :code:`make all` will install the required python dependencies for LoopStructural and then install and build the library. 57 | It just executes the following command: 58 | 59 | .. code-block:: 60 | 61 | pip3 install -r requirements.txt && python3 setup.py install build_ext --inplace 62 | 63 | If you want to use a jupyter notebook then you can launch a server by running :code:`make notebook`, alternatively you can 64 | run :code:`make notebookbuild` if you want to build the library before launching the server. 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/source/_static/windows_install.rst: -------------------------------------------------------------------------------- 1 | Windows using conda 2 | ~~~~~~~~~~~~~~~~~~~ 3 | .. container:: toggle 4 | 5 | .. container:: header 6 | 7 | **Installing a precompiled version** 8 | 9 | LoopStructural can then be installed into your python environment by running the command within your chosen python virtual environment. 10 | .. code-block:: 11 | pip install LoopStructural 12 | 13 | .. container:: toggle 14 | 15 | .. container:: header 16 | 17 | If you want to install LoopStructural from source to be able to modify the code you will need to have a working C++ compiler. 18 | This is best done using anaconda 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /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" %} 2 | 3 | {% block footer %} 4 | 14 | {% endblock %} -------------------------------------------------------------------------------- /docs/source/getting_started/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ====== 3 | LoopStructural is the 3D geological modelling library for Loop (Loop3d.org). 4 | The development of LoopStructural is lead by Lachlan Grose as an ARC (LP170100985) post-doc at Monash University. 5 | Laurent Ailleres and Gautier Laurent have made significant contributions to the conceptual design and integration of geological concepts into the geological modelling workflow. 6 | Roy Thomson and Yohan de Rose have contributed to the implementation and integration of LoopStructural into the Loop workflow. 7 | 8 | Background of Implicit Surface Modelling 9 | ----------------------------------------- 10 | The interpolation algorithms behind LoopStructural are implementations of research and published work from the RING team (formerly Gocad research group), Monah University and external libraries written by Michael Hillier. 11 | Within LoopStructural we have implemented three discrete interpolation algorithms for implicit 3D modelling: 12 | 13 | * A Piecewise Linear Interpolation algorithm where the interpolation is performed on a tetraheral mesh minimising the second derivative of the implicit funtion between neighbouring tetrahedron. 14 | This interpolation algorithm is an implementation of the discrete smooth interpolation algorithm included in Gocad-Skua, which is heavily based on the work of Frank et al., 2007 and Caumon et al., 2007. 15 | 16 | * This interpolation algorithm can also be framed by minimising the second derivatives using finite different on a regular cartesian grid which has been presented by Irakarama et al., 17 | 18 | * Within the Piecewise Linear framework we have also implemented additional constraints for modelling the geometry of folded surfaces. To do this we build a structural frame, where the structural frame characterises the axial foliation and fold axis of the fold. We modify the regularisation constraint for the folded surfaces so that the regularisation only occurs orthogonal to the fold axis and axial surface.We also use the geometry of the fold looking down plunge to add in additional constraints on the folded surface geometry. The fold constraints were first presented by Laurent et al., 2016 and the characterisation of the geometry from datasets was introduced by Grose et al., 2017 19 | 20 | 21 | References 22 | ---------- 23 | Caumon, G., Antoine, C. and Tertois, A.: Building 3D geological surfaces from field data using implicit surfaces Field data and the need for interpretation, , 1–6, 2007. 24 | Frank, T., Tertois, A.-L. L. and Mallet, J.-L. L.: 3D-reconstruction of complex geological interfaces from irregularly distributed and noisy point data, Comput. Geosci., 33(7), 932–943, doi:10.1016/j.cageo.2006.11.014, 2007. 25 | Grose, L., Laurent, G., Aillères, L., Armit, R., Jessell, M. and Caumon, G.: Structural data constraints for implicit modeling of folds, J. Struct. Geol., 104, 80–92, doi:10.1016/j.jsg.2017.09.013, 2017. 26 | Hillier, M. J., Schetselaar, E. M., de Kemp, E. A. and Perron, G.: Three-Dimensional Modelling of Geological Surfaces Using Generalized Interpolation with Radial Basis Functions, Math. Geosci., 46(8), 931–953, doi:10.1007/s11004-014-9540-3, 2014. 27 | Laurent, G., Ailleres, L., Grose, L., Caumon, G., Jessell, M. and Armit, R.: Implicit modeling of folds and overprinting deformation, Earth Planet. Sci. Lett., 456, 26–38, doi:10.1016/j.epsl.2016.09.040, 2016. 28 | Mallet, J.-L.: Elements of Mathematical Sedimentary Geology: the GeoChron Model, , 1–4, doi:10.3997/9789073834811, 2014. 29 | 30 | -------------------------------------------------------------------------------- /docs/source/getting_started/background.rst: -------------------------------------------------------------------------------- 1 | Background 2 | ========== 3 | LoopStructural is an opensource Python 3.9+ library for 3D geological modelling where geological objects are represented by an implicit function. 4 | Where the implicit function represents the distance or pseudodistance to a reference horizion. 5 | There is no analytical function that can be used to represent the geometry of geological objects, so the implicit function has to be approximated. 6 | The implicit function is approximated from the observations of the surface 7 | * location of the geological feature can be used to define the value of the implicit function at a location 8 | * orientation of the geological feature (e.g. strike and dip of a surface) can be used to constrain the gradient of the implicit function 9 | 10 | Complex features such as folds, faults and unconformities need additional information to be incorporated into the model. 11 | Using LoopStructural the overprinting relationships between folds can be incorporated by applying specific fold constraints. 12 | Faults require the fault displacement and fault slip direction vector to be known to incorporate these directly into the model. 13 | Unconformities are incorporated by specifying the value of an existing implicit function that defines the unconformity surface. 14 | 15 | Intepreting a map into model input 16 | ---------------------------------- 17 | To build a geological model the user needs to convert their observations into the appropriate format for LoopStructural. 18 | 19 | 20 | Automatic intepretation into a model using map2loop 21 | ---------------------------------------------------- 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/source/getting_started/index.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | .. toctree:: 5 | :caption: Getting Started 6 | 7 | installation 8 | background 9 | about 10 | contributors_guide 11 | loopstructural_design 12 | CHANGELOG 13 | -------------------------------------------------------------------------------- /docs/source/getting_started/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ==================== 3 | 4 | **Last updated:** 4/02/2025 5 | 6 | LoopStructural is supported and tested on Python 3.9+ and can be installed on Linux, Windows and Mac. 7 | We recommend installing LoopStructural into clean python environment. Either using anaconda or python virtual environments. 8 | There are three ways of installing LoopStructural onto your system: 9 | 10 | Installing from pip or conda 11 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | .. code-block:: 14 | 15 | pip install LoopStructural 16 | pip install LoopStructural[all] # to include all optional dependencies 17 | 18 | 19 | .. code-block:: 20 | 21 | conda install -c conda-forge -c loop3d loopstructural 22 | 23 | 24 | Compiling LoopStructural from source 25 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 26 | 27 | You can install the most recent version of LoopStructural by cloning it from GitHub. 28 | 29 | 30 | 31 | 32 | .. code-block:: 33 | 34 | git clone https://github.com/Loop3D/LoopStructural.git 35 | cd LoopStructural 36 | pip install -e . # -e installs as an editable package so you can modify the source code and see the changes immediately 37 | 38 | Dependencies 39 | ~~~~~~~~~~~~ 40 | 41 | Required dependencies: 42 | 43 | * numpy 44 | * pandas 45 | * scipy 46 | * scikit-image 47 | * scikit-learn 48 | 49 | Optional dependencies: 50 | 51 | * matplotlib, 2D/3D visualisation 52 | * pyvista, 3D visualisation 53 | * surfepy, radial basis interpolation 54 | * map2loop, generation of input datasets from regional Australian maps 55 | * geoh5py, export to gocad hdf5 format 56 | * pyevtk, export to vtk format 57 | * dill, serialisation of python objects 58 | * loopsolver, solving of inequalities 59 | * tqdm, progress bar 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/source/getting_started/todo.rst: -------------------------------------------------------------------------------- 1 | # Todo list for LoopStructural 2 | 3 | 1. Create interpolation API 4 | - use scikit learn style 5 | - allow interpolation to be used within scikit learn pipeline 6 | - generic api for all interpolators 7 | - 2D/3D 8 | 9 | 2. Modelling API 10 | - create api for using GeologicalModel class 11 | - 12 | 13 | 14 | 3. -------------------------------------------------------------------------------- /docs/source/images/edit_bashrc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/edit_bashrc.png -------------------------------------------------------------------------------- /docs/source/images/fault_frame_figure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/fault_frame_figure.png -------------------------------------------------------------------------------- /docs/source/images/githubwebsite_clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/githubwebsite_clone.png -------------------------------------------------------------------------------- /docs/source/images/githubwindows_clone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/githubwindows_clone.png -------------------------------------------------------------------------------- /docs/source/images/githubwindows_update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/githubwindows_update.png -------------------------------------------------------------------------------- /docs/source/images/image823.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/image823.png -------------------------------------------------------------------------------- /docs/source/images/jupyter_browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/jupyter_browser.png -------------------------------------------------------------------------------- /docs/source/images/loop-struct-foot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/loop-struct-foot.png -------------------------------------------------------------------------------- /docs/source/images/loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/loop.png -------------------------------------------------------------------------------- /docs/source/images/moba_xterm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/moba_xterm.png -------------------------------------------------------------------------------- /docs/source/images/powershell_enable_wls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/powershell_enable_wls.png -------------------------------------------------------------------------------- /docs/source/images/run_jupyter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/run_jupyter.png -------------------------------------------------------------------------------- /docs/source/images/solver_comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/solver_comparison.png -------------------------------------------------------------------------------- /docs/source/images/structural_frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/structural_frame.png -------------------------------------------------------------------------------- /docs/source/images/ubuntu_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/ubuntu_login.png -------------------------------------------------------------------------------- /docs/source/images/ubuntu_microsoft_store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/ubuntu_microsoft_store.png -------------------------------------------------------------------------------- /docs/source/images/wls_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/docs/source/images/wls_terminal.png -------------------------------------------------------------------------------- /docs/source/user_guide/fold_modelling.rst: -------------------------------------------------------------------------------- 1 | Fold modelling 2 | =============== 3 | 4 | A specific interpolator can be used for modelling folds geological features. The LoopStructural implements -------------------------------------------------------------------------------- /docs/source/user_guide/index.rst: -------------------------------------------------------------------------------- 1 | User Guide 2 | =========== 3 | This section of the documentation provides a detailed guide on how to use LoopStructural for users who may or may not be familiar with 4 | implicit 3D geological modelling, but are looking to utilise LoopStructural for their geological modelling needs. See the pane on the left 5 | for a list of topics covered in this section. 6 | 7 | 8 | 9 | .. toctree:: 10 | :caption: User Guide 11 | 12 | what_is_a_geological_model 13 | input_data 14 | geological_model 15 | interpolation_options 16 | fold_modelling 17 | fault_modelling 18 | visualisation 19 | debugging -------------------------------------------------------------------------------- /docs/source/user_guide/input_data.rst: -------------------------------------------------------------------------------- 1 | Input data 2 | ========== 3 | To create a geological model the data needs to be formatted in a table the following headings: 4 | 5 | * X - x component of the cartesian coordinates 6 | * Y - y component of the cartesian coordinates 7 | * Z - z component of the cartesian coordinates 8 | * feature_name - unique name of the geological feature being modelled 9 | * val - value observations of the scalar field 10 | * interface - unique identifier for an inteface containing similar scalar field values 11 | * nx - x component of the gradient norm 12 | * ny - y component of the gradient norm 13 | * nz - z component of the gradient norm 14 | * gx - x component of a gradient constraint 15 | * gy - y component of a gradient constraint 16 | * gz - z component of a gradient constraint 17 | * tx - x component of a gradient tangent constraint 18 | * ty - y component of a gradient tangent constraint 19 | * tz - z component of a gradient tangent constraint 20 | * coord - coordinate of the structural frame data point is used for 21 | 22 | Value constraints 23 | ------------------ 24 | Value constraints set the value of the implicit function to equal the value. 25 | This should represent the distance from the reference horizon. 26 | It is important to consider the range in value of this field and the coordinates of the model. 27 | If the range in value of the scalar field is very large relative to the model coordinates, it would be expected that the resulting model will be very steep. 28 | 29 | Interface constraints 30 | --------------------- 31 | Interface constraints are points which should have the same value of the implicit function. 32 | This is comparable to the way data are interpreted using the Lajaunnie approach. 33 | A warning, when using interface constraints there is no gradient information implicitly defined by the value of the points. 34 | This means that unless there are sufficient gradient normal constraints located between interfaces the scalar field may not appear as expected. 35 | Another consideration is that using discrete approaches the implicit function is solved in a least squares sense, this means that all of the constraints minimised equally. 36 | Due to the implementation of interface constraints, these tend to be weighted higher in the system of equations due to there being more pairs of points than points. 37 | As a result, it is recommended to increase the weighting of the normal constraints by at least the average number of points in an interface. 38 | 39 | Gradient norm constraints 40 | ------------------------- 41 | Sets the partial derivatives of the implicit function to equal the components of the gradient norm vector. 42 | This has a similar limitation as the value constraints, the magnitude of the vector needs to within the correct scale of the model. 43 | This means the normal vector should be scaled in the same way as the model coordinates. 44 | 45 | Gradient constraints 46 | -------------------- 47 | Gradient constraints find a pair of vectors that are orthogonal to the normal vector to the field. 48 | This this can be the strike vector and the dip vector. 49 | Two constraints are added into the implicit functions constraining the scalar field to be orthogonal to these vectors. 50 | This constraint will control the gradient of the scalar field without controlling the polarity or the length of the gradient norm. 51 | For this reason these constraints are not as strong as the gradient norm constraint. 52 | 53 | 54 | Tangent constraints 55 | ------------------- 56 | Constrains a vector to be orthogonal to the scalar field by adding the following constraint: 57 | 58 | .. math:: f(x,y,z) \cdot \textbf{t} = 0 59 | 60 | This constraint does not control the vaue of the scalar field and is quite weak. 61 | It is not recommended for use, however it could be used for integrating form lines, instead of using interface constraints. 62 | 63 | -------------------------------------------------------------------------------- /docs/source/user_guide/map2loop.rst: -------------------------------------------------------------------------------- 1 | The class method (GeologicalModel.from_map2loop_directory(m2l_directory, **kwargs)) creates an instance of a GeologicalModel from a root map2loop output directory. 2 | There are two steps to this function the first is a pre-processing step that prepares the extracted datasets for LoopStructural. This involves combining all observations (fault surface location, fault orientation, stratigraphic orientation and stratigraphic contacts) into the one pandas DataFrame. The cumulative thickness is calculated by iterating over the stratigraphic column for each group and starting with the base of the group as 0 and adding the thickness of the lower unit. This function returns a python dictionary which is structured as shown in Table 5. The processed data dictionary can then be passed to a utility function that: 3 | 1. initialises a GeologicalModel for the specified bounding box (map area chosen for map2loop and depth specified). 4 | 2. associates the pandas DataFrame to the model 5 | 3. iterate through the fault-stratigraphy table 6 | • adding stratigraphic packages that are associated with no faults 7 | • add faults in reverse order, fault intersections are manually, and fault geometries are only interpolated within their object aligned bounding box to reduce computational to ensure that 8 | • The base of each group is assigned as an unconformity for any older stratigraphic units or faults that occur below it 9 | 4. associate stratigraphic column to geological model 10 | 11 | A parameters dictionary can be passed to the GeologicalModel.from_map2loop_directory function which allows for the interpolator parameters (listed in Table 2) for the model to be specified. This can be passed using two python dictionaries: foliation_params contains the parameters for modelling any stratigraphic groups and fault_params contains parameters for the faults. 12 | -------------------------------------------------------------------------------- /docs/source/user_guide/preparing_data.rst: -------------------------------------------------------------------------------- 1 | From geological map to model 2 | ---------------------------- 3 | LoopStructural is an implicit geological modelling library with different interpolation algorithms (discrete and data supported). 4 | A geological model consists of multiple implicit functions that each define a distance or pseudo-distance within the geological object. 5 | The interaction between the implicit function is defined by the relative timing of the feature and the type of geological feature being modelled. 6 | Each implicit function is approximated to fit the observations of the geological feature. 7 | The geological observations need to be converted into mathematical constraints for the function defining the value of the implicit function and the gradient of the implicit function. 8 | 9 | This guide details the `modelling.input` module which provides the tools for converting from a geological dataset into a 3D modelling. 10 | 11 | Input dataset 12 | ------------- 13 | 14 | To build a geological model of the location and distribution of stratigraphic units the input dataset must include: 15 | 1. Location of the interface between stratigraphic units 16 | 2. Orientation of stratigraphy 17 | 3. Order of stratigraphic units 18 | 4. Stratigraphic thicknesses 19 | 5. Location of fault traces 20 | 6. Orientation of fault 21 | 7. Fault properties 22 | 8. Fault intersections 23 | 9. Fault stratigraphy intersections 24 | 10. Visualisation colours 25 | 26 | 27 | Requirements for modelling stratigraphy 28 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 29 | 30 | contacts 31 | ******** 32 | 33 | To build a model showing the distribution and location of stratigraphy the location of the basal interface between stratigraphic layers needs to recorded. 34 | This can be included as a DataFrame like object with the following columns: 35 | * `X` eastings of the contact location 36 | * `Y` northings of the contact Location 37 | * `Z` altitude of the contact location 38 | * `formation` a unique name defining the interface 39 | 40 | orientations 41 | ************ 42 | 43 | Structural observations can be included using a DataFrame like object with the following columns: 44 | * `X` eastings of the contact location 45 | * `Y` northings of the contact Location 46 | * `Z` altitude of the contact location 47 | * `strike` using the right hand thumb rule 48 | * `dip` angle between horizontal and contact 49 | 50 | stratigraphic_order 51 | ******************* 52 | 53 | The stratigraphic order defines the relative age of the stratigraphic interfaces. 54 | This can be included using a nested list containing the formation names. 55 | The lists are to be formated so that the first level of the list contains the youngest conformable layers. 56 | The next level contains the order of the formations within the conformable group, with them order youngest to oldest. 57 | In the example below, there are two groups the youngest group contains a,b,c and the older group contains d,e. 58 | 59 | .. code-block:: 60 | [[a,b,c],['d','e]] 61 | 62 | Note: If the stratigraphy can be interpreted as conformable, it is advisable to include all stratigraphy inside a single group. 63 | 64 | thicknesses 65 | *********** 66 | 67 | 68 | I 69 | should be incolumn 70 | 71 | ProcessInputData 72 | -------------------- 73 | 74 | The minimum requirements for building a geological model are: 75 | * contact location observations 76 | * contact orientation observations 77 | * stratigraphic order 78 | -------------------------------------------------------------------------------- /docs/source/user_guide/structural_frames.rst: -------------------------------------------------------------------------------- 1 | Structural Frames 2 | ================= 3 | 4 | Structural frames form the basis of many of the geological features that are built using LoopStructural. 5 | A structural frame is a curvilinear coordinate system with three coordinates: 6 | * Major structrural feature 7 | * Structural direction 8 | * Additional/Intermediate direction 9 | 10 | These coordinates correspond to the different elements of geological structures that are observed and recorded by geologists in the field. 11 | For example, when modelling faults the major structural feature is the fault surface and the structural direction is the fault slip direction. 12 | For folds, the major structural is the axial foliation and the structural direction is the fold axis. 13 | 14 | Each coordinate of the structural frame are represented by three implicit functions. 15 | The major structural feature is interpolated first as this is the field usually associated with more observations. 16 | The structural direction is interpolated using any associated observations, or conceptual knowledge (for example the expected fault slip direction or fold axis) and an additonal constraint to enforce orthogonality between the structural direction and the already interpolated major structural feature. 17 | -------------------------------------------------------------------------------- /examples/1_basic/README.rst: -------------------------------------------------------------------------------- 1 | 1. Basics 2 | --------- -------------------------------------------------------------------------------- /examples/1_basic/plot_3_multiple_groups.py: -------------------------------------------------------------------------------- 1 | """ 2 | 1c. Multiple groups 3 | =================== 4 | Creating a model with multiple geological features, dealing with unconformities. 5 | 6 | """ 7 | 8 | from LoopStructural import GeologicalModel 9 | from LoopStructural.datasets import load_claudius 10 | from LoopStructural.visualisation import Loop3DView 11 | 12 | 13 | data, bb = load_claudius() 14 | data = data.reset_index() 15 | 16 | data.loc[:, "val"] *= -1 17 | data.loc[:, ["nx", "ny", "nz"]] *= -1 18 | 19 | data.loc[792, "feature_name"] = "strati2" 20 | data.loc[792, ["nx", "ny", "nz"]] = [0, 0, 1] 21 | data.loc[792, "val"] = 0 22 | 23 | model = GeologicalModel(bb[0, :], bb[1, :]) 24 | model.set_model_data(data) 25 | 26 | strati2 = model.create_and_add_foliation( 27 | "strati2", 28 | interpolatortype="FDI", 29 | nelements=1e4, 30 | ) 31 | uc = model.add_unconformity(strati2, 1) 32 | 33 | strati = model.create_and_add_foliation( 34 | "strati", 35 | interpolatortype="FDI", 36 | nelements=1e4, 37 | ) 38 | 39 | viewer = Loop3DView(model) 40 | viewer.plot_surface( 41 | strati2, 42 | # nslices=5 43 | value=[2, 1.5, 1], 44 | ) 45 | viewer.plot_surface(strati, value=[0, -60, -250, -330], paint_with=strati) 46 | viewer.display() 47 | -------------------------------------------------------------------------------- /examples/1_basic/plot_4_using_stratigraphic_column.py: -------------------------------------------------------------------------------- 1 | """ 2 | 1d. Using Stratigraphic Columns 3 | =============================== 4 | We will use the previous example Creating a model with multiple geological features, dealing with unconformities. 5 | 6 | """ 7 | 8 | from LoopStructural import GeologicalModel 9 | from LoopStructural.datasets import load_claudius 10 | from LoopStructural.visualisation import Loop3DView 11 | 12 | import numpy as np 13 | 14 | data, bb = load_claudius() 15 | data = data.reset_index() 16 | 17 | data.loc[:, "val"] *= -1 18 | data.loc[:, ["nx", "ny", "nz"]] *= -1 19 | 20 | data.loc[792, "feature_name"] = "strati2" 21 | data.loc[792, ["nx", "ny", "nz"]] = [0, 0, 1] 22 | data.loc[792, "val"] = 0 23 | 24 | model = GeologicalModel(bb[0, :], bb[1, :]) 25 | model.set_model_data(data) 26 | 27 | strati2 = model.create_and_add_foliation( 28 | "strati2", 29 | interpolatortype="FDI", 30 | nelements=1e4, 31 | ) 32 | uc = model.add_unconformity(strati2, 1) 33 | 34 | strati = model.create_and_add_foliation( 35 | "strati", 36 | interpolatortype="FDI", 37 | nelements=1e4, 38 | ) 39 | 40 | ######################################################################## 41 | # Stratigraphic columns 42 | # ~~~~~~~~~~~~~~~~~~~~~~~ 43 | # We define the stratigraphic column using a nested dictionary 44 | 45 | stratigraphic_column = {} 46 | stratigraphic_column["strati2"] = {} 47 | stratigraphic_column["strati2"]["unit1"] = {"min": 1, "max": 10, "id": 0} 48 | stratigraphic_column["strati"] = {} 49 | stratigraphic_column["strati"]["unit2"] = {"min": -60, "max": 0, "id": 1} 50 | stratigraphic_column["strati"]["unit3"] = {"min": -250, "max": -60, "id": 2} 51 | stratigraphic_column["strati"]["unit4"] = {"min": -330, "max": -250, "id": 3} 52 | stratigraphic_column["strati"]["unit5"] = {"min": -np.inf, "max": -330, "id": 4} 53 | 54 | ######################################################## 55 | # Adding stratigraphic column to the model 56 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 57 | # The stratigraphic column can be added to the geological model. Allowing 58 | # for the `model.evaluate_model(xyz)` function to be called. 59 | 60 | model.set_stratigraphic_column(stratigraphic_column) 61 | 62 | viewer = Loop3DView(model) 63 | viewer.plot_block_model() 64 | viewer.display() 65 | -------------------------------------------------------------------------------- /examples/1_basic/plot_5_unconformities.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============================ 3 | 1h. Unconformities and fault 4 | ============================ 5 | This tutorial will demonstrate how to add unconformities to a mode using LoopStructural. 6 | 7 | """ 8 | 9 | import numpy as np 10 | import pandas as pd 11 | from LoopStructural import GeologicalModel 12 | import matplotlib.pyplot as plt 13 | 14 | data = pd.DataFrame( 15 | [ 16 | [100, 100, 150, 0.17, 0, 0.98, 0, "strati"], 17 | [100, 100, 170, 0, 0, 0.86, 0, "strati3"], 18 | [100, 100, 100, 0, 0, 1, 0, "strati2"], 19 | [100, 100, 50, 0, 0, 1, 0, "nconf"], 20 | [100, 100, 50, 0, 0, 1, 0, "strati4"], 21 | [700, 100, 190, 1, 0, 0, np.nan, "fault"], 22 | ], 23 | columns=["X", "Y", "Z", "nx", "ny", "nz", "val", "feature_name"], 24 | ) 25 | 26 | model = GeologicalModel(np.zeros(3), np.array([1000, 1000, 200])) 27 | model.data = data 28 | model.create_and_add_foliation("strati2", buffer=0.0) 29 | model.add_unconformity(model["strati2"], 0) 30 | model.create_and_add_fault( 31 | "fault", 32 | 50, 33 | minor_axis=300, 34 | major_axis=500, 35 | intermediate_axis=300, 36 | fault_center=[700, 500, 0], 37 | ) 38 | 39 | model.create_and_add_foliation("strati", buffer=0.0) 40 | model.add_unconformity(model["strati"], 0) 41 | model.create_and_add_foliation("strati3", buffer=0.0) 42 | model.create_and_add_foliation("nconf", buffer=0.0) 43 | model.add_onlap_unconformity(model["nconf"], 0) 44 | model.create_and_add_foliation("strati4") 45 | 46 | 47 | stratigraphic_columns = { 48 | "strati4": {"series4": {"min": -np.inf, "max": np.inf, "id": 5}}, 49 | "strati2": { 50 | "series1": {"min": 0.0, "max": 2.0, "id": 0, "colour": "red"}, 51 | "series2": {"min": 2.0, "max": 5.0, "id": 1, "colour": "red"}, 52 | "series3": {"min": 5.0, "max": 10.0, "id": 2, "colour": "red"}, 53 | }, 54 | "strati": { 55 | "series2": {"min": -np.inf, "max": -100, "id": 3, "colour": "blue"}, 56 | "series3": {"min": -100, "max": np.inf, "id": 4, "colour": "blue"}, 57 | }, 58 | } 59 | 60 | 61 | model.set_stratigraphic_column(stratigraphic_columns) 62 | 63 | xx, zz = np.meshgrid(np.linspace(0, 1000, 100), np.linspace(0, 200, 100)) 64 | yy = np.zeros_like(xx) + 500 65 | points = np.array([xx.flatten(), yy.flatten(), zz.flatten()]).T 66 | val = model["strati"].evaluate_value(points) 67 | val2 = model["strati2"].evaluate_value(points) 68 | val3 = model["strati3"].evaluate_value(points) 69 | val4 = model["strati4"].evaluate_value(points) 70 | uf = model["strati4"].regions[0](points) 71 | fval = model['fault'].evaluate_value(points) 72 | 73 | plt.contourf(val.reshape((100, 100)), extent=(0, 1000, 0, 200), cmap='viridis') 74 | plt.contourf(val2.reshape((100, 100)), extent=(0, 1000, 0, 200), cmap='Reds') 75 | plt.contourf(val3.reshape((100, 100)), extent=(0, 1000, 0, 200), cmap='Blues') 76 | plt.contourf(val4.reshape((100, 100)), extent=(0, 1000, 0, 200), cmap='Greens') 77 | plt.contour(fval.reshape((100, 100)), [0], extent=(0, 1000, 0, 200)) 78 | -------------------------------------------------------------------------------- /examples/1_basic/plot_7_exporting.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | 1j. Exporting models 4 | =============================== 5 | 6 | Models can be exported to vtk, gocad and geoh5 formats. 7 | """ 8 | 9 | from LoopStructural import GeologicalModel 10 | from LoopStructural.datasets import load_claudius 11 | 12 | data, bb = load_claudius() 13 | 14 | model = GeologicalModel(bb[0, :], bb[1, :]) 15 | model.data = data 16 | model.create_and_add_foliation("strati") 17 | 18 | 19 | ###################################################################### 20 | # Export surfaces to vtk 21 | # ~~~~~~~~~~~~~~~~~~~~~~ 22 | # Isosurfaces can be extracted from a geological feature by calling 23 | # the `.surfaces` method on the feature. The argument for this method 24 | # is the value, values or number of surfaces that are extracted. 25 | # This returns a list of `LoopStructural.datatypes.Surface` objects 26 | # These objects can be interrogated to return the triangles, vertices 27 | # and normals. Or can be exported into another format using the `save` 28 | # method. The supported file formats are `vtk`, `ts` and `geoh5`. 29 | # 30 | 31 | surfaces = model['strati'].surfaces(value=0.0) 32 | 33 | print(surfaces) 34 | 35 | print(surfaces[0].vtk) 36 | 37 | # surfaces[0].save('text.geoh5') 38 | 39 | ###################################################################### 40 | # Export the model to geoh5 41 | # ~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | # The entire model can be exported to a geoh5 file using the `save_model` 43 | # method. This will save all the data, foliations, faults and other objects 44 | # in the model to a geoh5 file. This file can be loaded into LoopStructural 45 | 46 | # model.save('model.geoh5') 47 | -------------------------------------------------------------------------------- /examples/2_fold/README.rst: -------------------------------------------------------------------------------- 1 | 2. Modelling Folds 2 | ------------------- -------------------------------------------------------------------------------- /examples/2_fold/_refolded_folds.py: -------------------------------------------------------------------------------- 1 | """ 2 | 2b. Refolded folds 3 | =================== 4 | 5 | 6 | """ 7 | 8 | from LoopStructural import GeologicalModel 9 | from LoopStructural.visualisation import Loop3DView, RotationAnglePlotter 10 | from LoopStructural.datasets import load_laurent2016 11 | import pandas as pd 12 | 13 | # logging.getLogger().setLevel(logging.INFO) 14 | 15 | # load in the data from the provided examples 16 | data, bb = load_laurent2016() 17 | # bb[1,2] = 10000 18 | 19 | data.head() 20 | 21 | newdata = pd.DataFrame( 22 | [[5923.504395, 4748.135254, 3588.621094, "s2", 1.0]], 23 | columns=["X", "Y", "Z", "feature_name", "val"], 24 | ) 25 | data = pd.concat([data, newdata], sort=False) 26 | 27 | rotation = [-69.11979675292969, 15.704944610595703, 6.00014591217041] 28 | 29 | 30 | ###################################################################### 31 | # Modelling S2 32 | # ~~~~~~~~~~~~ 33 | # 34 | 35 | model = GeologicalModel(bb[0, :], bb[1, :]) 36 | model.set_model_data(data) 37 | s2 = model.create_and_add_fold_frame("s2", nelements=10000, buffer=0.5, solver="lu", damp=True) 38 | viewer = Loop3DView(model) 39 | viewer.add_scalar_field(s2[0], cmap="prism") 40 | viewer.add_isosurface(s2[0], slices=[0, 1]) 41 | viewer.add_data(s2[0]) 42 | viewer.rotate(rotation) 43 | viewer.display() 44 | 45 | 46 | ###################################################################### 47 | # Modelling S1 48 | # ~~~~~~~~~~~~ 49 | # 50 | 51 | s1 = model.create_and_add_folded_fold_frame("s1", av_fold_axis=True, nelements=50000, buffer=0.3) 52 | 53 | 54 | viewer = Loop3DView(model) 55 | viewer.add_scalar_field(s1[0], cmap="prism") 56 | viewer.rotate([-69.11979675292969, 15.704944610595703, 6.00014591217041]) 57 | viewer.display() 58 | 59 | ###################################################################### 60 | # S2/S1 S-Plots 61 | # ~~~~~~~~~~~~~ 62 | # 63 | s2_s1_splot = RotationAnglePlotter(s1) 64 | s2_s1_splot.add_fold_limb_data() 65 | s2_s1_splot.add_fold_limb_curve() 66 | # fig, ax = plt.subplots(1,2,figsize=(10,5)) 67 | # x = np.linspace(s2[0].min(),s2[0].max(),1000) 68 | # ax[0].plot(x,s1['fold'].fold_limb_rotation(x)) 69 | # ax[0].plot(s1['fold'].fold_limb_rotation.fold_frame_coordinate,s1['fold'].fold_limb_rotation.rotation_angle,'bo') 70 | # ax[1].plot(s1['limb_svariogram'].lags,s1['limb_svariogram'].variogram,'bo') 71 | 72 | 73 | ###################################################################### 74 | # Modelling S0 75 | # ~~~~~~~~~~~~ 76 | # 77 | 78 | s0 = model.create_and_add_folded_foliation( 79 | "s0", 80 | av_fold_axis=True, 81 | nelements=50000, 82 | buffer=0.2, 83 | ) 84 | 85 | viewer = Loop3DView(model) 86 | viewer.add_scalar_field(s0, cmap="tab20") 87 | viewer.rotate([-69.11979675292969, 15.704944610595703, 6.00014591217041]) 88 | viewer.display() 89 | 90 | ###################################################################### 91 | # S1/S0 S-Plots 92 | # ~~~~~~~~~~~~~ 93 | # 94 | s1_s0_splot = RotationAnglePlotter(s0) 95 | s1_s0_splot.add_fold_limb_data() 96 | s1_s0_splot.add_fold_limb_curve() 97 | 98 | # fig, ax = plt.subplots(1,2,figsize=(10,5)) 99 | # x = np.linspace(s1[0].min(),s1[0].max(),1000) 100 | # ax[0].plot(x,s0['fold'].fold_limb_rotation(x)) 101 | # ax[0].plot(s0['fold'].fold_limb_rotation.fold_frame_coordinate,s0['fold'].fold_limb_rotation.rotation_angle,'bo') 102 | # ax[1].plot(s0['limb_svariogram'].lags,s1['limb_svariogram'].variogram,'bo') 103 | 104 | viewer = Loop3DView(model) 105 | viewer.add_isosurface(s0, nslices=10, paint_with=s0, cmap="tab20") 106 | # viewer.add_data(s0) 107 | # viewer.add_fold(s0['fold'],locations=s0['support'].barycentre[::80]) 108 | viewer.rotate([-69.11979675292969, 15.704944610595703, 6.00014591217041]) 109 | viewer.display() 110 | -------------------------------------------------------------------------------- /examples/3_fault/README.rst: -------------------------------------------------------------------------------- 1 | 3. Modelling Faults 2 | -------------------- -------------------------------------------------------------------------------- /examples/3_fault/_faulted_intrusion.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3a. Modelling faults using structural frames 3 | ======================================== 4 | 5 | """ 6 | 7 | from LoopStructural import GeologicalModel 8 | from LoopStructural.visualisation import Loop3DView 9 | from LoopStructural.datasets import load_intrusion 10 | import numpy as np 11 | import matplotlib.pyplot as plt 12 | 13 | data, bb = load_intrusion() 14 | 15 | 16 | ###################################################################### 17 | # Modelling faults using structural frames 18 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 19 | # 20 | # Standard implicit modelling techniques either treat faults as domain 21 | # boundaries or use a step function in the implicit function to capture 22 | # the displacement in the faulted surface. 23 | # 24 | # Adding faults into the implicit function using step functions is limited 25 | # because this does not capture the kinematics of the fault. It 26 | # effectively defines the fault displacement by adding a value to the 27 | # scalar field on the hanging wall of the fault. In the example below a 28 | # 2-D ellipsoidal function is combined with a step function to show how 29 | # the resulting geometry results in a shrinking shape. This would be 30 | # representative of modelling an intrusion. 31 | # 32 | 33 | intrusion = lambda x, y: (x * 2) ** 2 + (y**2) 34 | x = np.linspace(-10, 10, 100) 35 | y = np.linspace(-10, 10, 100) 36 | xx, yy = np.meshgrid(x, y) 37 | fault = np.zeros(xx.shape) 38 | fault[yy > 0] = 50 39 | val = intrusion(xx, yy) + fault 40 | 41 | 42 | plt.contourf(val) 43 | 44 | 45 | ###################################################################### 46 | # LoopStructural applies structural frames to the fault geometry to 47 | # capture the geometry and kinematics of the fault. A fault frame 48 | # consisting of the fault surface, fault slip direction and fault extent 49 | # are built from observations. The geometry of the deformed surface is 50 | # then interpolated by first restoring the observations by combining the 51 | # fault frame and an expected displacement model. 52 | # 53 | 54 | model = GeologicalModel(bb[0, :], bb[1, :]) 55 | model.set_model_data(data) 56 | fault = model.create_and_add_fault( 57 | "fault", 500, nelements=10000, steps=4, interpolatortype="FDI", buffer=0.3 58 | ) 59 | 60 | viewer = Loop3DView(model) 61 | viewer.add_isosurface( 62 | fault, 63 | isovalue=0, 64 | # slices=[0,1]#nslices=10 65 | ) 66 | xyz = model.data[model.data["feature_name"] == "strati"][["X", "Y", "Z"]].to_numpy() 67 | xyz = xyz[fault.evaluate(xyz).astype(bool), :] 68 | viewer.add_vector_field(fault, locations=xyz) 69 | viewer.add_points( 70 | model.rescale( 71 | model.data[model.data["feature_name"] == "strati"][["X", "Y", "Z"]], 72 | inplace=False, 73 | ), 74 | name="prefault", 75 | ) 76 | viewer.rotation = [-73.24819946289062, -86.82220458984375, -13.912878036499023] 77 | viewer.display() 78 | 79 | 80 | displacement = 400 # INSERT YOUR DISPLACEMENT NUMBER HERE BEFORE # 81 | 82 | model = GeologicalModel(bb[0, :], bb[1, :]) 83 | model.set_model_data(data) 84 | fault = model.create_and_add_fault( 85 | "fault", displacement, nelements=2000, steps=4, interpolatortype="PLI", buffer=2 86 | ) 87 | strati = model.create_and_add_foliation("strati", nelements=30000, interpolatortype="PLI", cgw=0.03) 88 | model.update() 89 | viewer = Loop3DView(model) 90 | viewer.add_isosurface(strati, isovalue=0) 91 | # viewer.add_data(model.features[0][0]) 92 | viewer.add_data(strati) 93 | viewer.add_isosurface( 94 | fault, 95 | isovalue=0, 96 | # slices=[0,1]#nslices=10 97 | ) 98 | viewer.add_points( 99 | model.rescale( 100 | model.data[model.data["feature_name"] == "strati"][["X", "Y", "Z"]], 101 | inplace=False, 102 | ), 103 | name="prefault", 104 | ) 105 | viewer.rotation = [-73.24819946289062, -86.82220458984375, -13.912878036499023] 106 | viewer.display() 107 | -------------------------------------------------------------------------------- /examples/4_advanced/_7_local_weights.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============================ 3 | 1f. Local data weighting 4 | ============================ 5 | LoopStructural primarily uses discrete interpolation methods (e.g. finite differences on a regular grid, 6 | or linear/quadratic on tetrahedral meshes). The interpolation is determined by combining a regularisation 7 | term and the data weights. The default behaviour is for every data point to be weighted equally, however 8 | it is also possible to vary these weights per datapoint. 9 | 10 | """ 11 | 12 | from LoopStructural import GeologicalModel 13 | from LoopStructural.datasets import load_claudius 14 | from LoopStructural.visualisation import Loop3DView 15 | 16 | ################################################################################################## 17 | # Use Cladius case study 18 | # ~~~~~~~~~~~~~~~~~~~~~~~~ 19 | # 20 | data, bb = load_claudius() 21 | data.head() 22 | ################################################################################################## 23 | # Build model with constant weighting 24 | # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 25 | # Build model with weight 1.0 for the control points (cpw) and gradient normal constraints (npw) 26 | model = GeologicalModel(bb[0, :], bb[1, :]) 27 | model.data = data 28 | model.create_and_add_foliation("strati", interpolatortype="FDI", cpw=1.0, npw=1.0) 29 | view = Loop3DView(model) 30 | view.add_isosurface(model["strati"], slices=data["val"].dropna().unique()) 31 | view.rotation = [84.24493408203125, -77.89686584472656, -171.99795532226562] 32 | view.display() 33 | ################################################################################################## 34 | # Change weights to 35 | 36 | model = GeologicalModel(bb[0, :], bb[1, :]) 37 | model.data = data 38 | model.create_and_add_foliation("strati", interpolatortype="FDI", cpw=10.0, npw=1.0) 39 | view = Loop3DView(model) 40 | view.add_isosurface(model["strati"], slices=data["val"].dropna().unique()) 41 | view.rotation = [84.24493408203125, -77.89686584472656, -171.99795532226562] 42 | view.display() 43 | 44 | ################################################################################################## 45 | # Locally vary weights 46 | # # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 47 | # Add a weight column to the dataframe and decrease the weighting of the points 48 | # in the North of the model. 49 | data, bb = load_claudius() 50 | data["w"] = 1.0 51 | data.loc[data["Y"] > (bb[1, 1] - bb[0, 1]) * 0.2 + bb[0, 1], "w"] = 0.01 52 | data.sample(10) 53 | 54 | # cpw/npw are multipliers for the weight column 55 | model.create_and_add_foliation("strati", cpw=1.0, npw=1) 56 | view = Loop3DView(model) 57 | view.add_isosurface(model["strati"], slices=data["val"].dropna().unique()) 58 | view.rotation = [84.24493408203125, -77.89686584472656, -171.99795532226562] 59 | view.display() 60 | -------------------------------------------------------------------------------- /examples/README.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | ======== -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": { 3 | "LoopStructural": { 4 | "release-type": "python" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scikit-learn 2 | scikit-image 3 | scipy 4 | numpy 5 | setuptools 6 | pandas 7 | pyevtk 8 | dill 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """See pyproject.toml for project metadata.""" 2 | 3 | from setuptools import setup 4 | import os 5 | 6 | package_root = os.path.abspath(os.path.dirname(__file__)) 7 | 8 | version = {} 9 | with open(os.path.join(package_root, "LoopStructural/version.py")) as fp: 10 | exec(fp.read(), version) 11 | version = version["__version__"] 12 | setup() 13 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # conftest.py 2 | 3 | from glob import glob 4 | 5 | 6 | def refactor(string: str) -> str: 7 | return string.replace("/", ".").replace("\\", ".").replace(".py", "") 8 | 9 | 10 | pytest_plugins = [ 11 | refactor(fixture) for fixture in glob("tests/fixtures/*.py") if "__" not in fixture 12 | ] 13 | print(pytest_plugins) 14 | -------------------------------------------------------------------------------- /tests/fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Loop3D/LoopStructural/760686432710c92ec2653cafe226012c64c24551/tests/fixtures/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | 5 | 6 | @pytest.fixture(params=[0, 1, 2]) 7 | def data(request): 8 | data_list = [] 9 | if request.param == 0: 10 | value = True 11 | gradient = False 12 | if request.param == 1: 13 | value = False 14 | gradient = True 15 | 16 | if request.param == 2: 17 | value = True 18 | gradient = True 19 | if value: 20 | xy = np.array(np.meshgrid(np.linspace(0, 1, 50), np.linspace(0, 1, 50))).T.reshape(-1, 2) 21 | xyz = np.hstack([xy, np.zeros((xy.shape[0], 1))]) 22 | data = pd.DataFrame(xyz, columns=["X", "Y", "Z"]) 23 | data["val"] = np.sin(data["X"]) 24 | data["w"] = 1 25 | data["feature_name"] = "strati" 26 | data_list.append(data) 27 | if gradient: 28 | data = pd.DataFrame( 29 | [[0.5, 0.5, 0.5, 0, 0, 1], [0.75, 0.5, 0.75, 0, 0, 1]], 30 | columns=["X", "Y", "Z", "nx", "ny", "nz"], 31 | ) 32 | data["w"] = 1 33 | data["feature_name"] = "strati" 34 | data_list.append(data) 35 | if "nx" not in data: 36 | data["nx"] = np.nan 37 | data["ny"] = np.nan 38 | data["nz"] = np.nan 39 | if "val" not in data: 40 | data["val"] = np.nan 41 | return pd.concat(data_list, ignore_index=True) 42 | -------------------------------------------------------------------------------- /tests/fixtures/horizontal_data.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import pytest 4 | 5 | 6 | @pytest.fixture() 7 | def horizontal_data(): 8 | 9 | xy = np.array(np.meshgrid(np.linspace(0, 1, 50), np.linspace(0, 1, 50))).T.reshape(-1, 2) 10 | df1 = pd.DataFrame(xy, columns=["X", "Y"]) 11 | df2 = pd.DataFrame(xy, columns=["X", "Y"]) 12 | df1["Z"] = 0.25 13 | df1["val"] = 0 14 | df2["Z"] = 0.55 15 | df2["val"] = 0.3 16 | data = pd.concat([df1, df2], ignore_index=True) 17 | data["w"] = 1 18 | data["feature_name"] = "strati" 19 | 20 | return data 21 | -------------------------------------------------------------------------------- /tests/fixtures/interpolator.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.interpolators import ( 2 | FiniteDifferenceInterpolator as FDI, 3 | PiecewiseLinearInterpolator as PLI, 4 | ) 5 | from LoopStructural.interpolators import StructuredGrid, TetMesh 6 | from LoopStructural.datatypes import BoundingBox 7 | import pytest 8 | import numpy as np 9 | 10 | 11 | @pytest.fixture(params=["FDI", "PLI"]) 12 | def interpolator(request): 13 | interpolator = request.param 14 | origin = np.array([-0.1, -0.1, -0.1]) 15 | maximum = np.array([1.1, 1.1, 1.1]) 16 | nsteps = np.array([20, 20, 20]) 17 | step_vector = (maximum - origin) / nsteps 18 | if interpolator == "FDI": 19 | grid = StructuredGrid(origin=origin, nsteps=nsteps, step_vector=step_vector) 20 | interpolator = FDI(grid) 21 | return interpolator 22 | elif interpolator == "PLI": 23 | grid = TetMesh(origin=origin, nsteps=nsteps, step_vector=step_vector) 24 | interpolator = PLI(grid) 25 | return interpolator 26 | else: 27 | raise ValueError(f"Invalid interpolator: {interpolator}") 28 | 29 | 30 | @pytest.fixture(params=["PLI", "FDI"]) 31 | def interpolatortype(request): 32 | return request.param 33 | 34 | 35 | @pytest.fixture(params=[1e3, 1e4, 4e4]) 36 | def nelements(request): 37 | nelements = request.param 38 | return nelements 39 | 40 | 41 | @pytest.fixture() 42 | def bounding_box(): 43 | return BoundingBox(np.array([0, 0, 0]), np.array([1, 1, 1])) 44 | 45 | 46 | @pytest.fixture(params=["PLI", "FDI"]) 47 | def interpolator_type(request): 48 | interpolator_type = request.param 49 | return interpolator_type 50 | 51 | 52 | @pytest.fixture(params=["grid", "tetra"]) 53 | def support(request): 54 | support_type = request.param 55 | if support_type == "grid": 56 | return StructuredGrid() 57 | if support_type == "tetra": 58 | return TetMesh() 59 | 60 | 61 | @pytest.fixture(params=["grid", "tetra"]) 62 | def support_class(request): 63 | support_type = request.param 64 | if support_type == "grid": 65 | return StructuredGrid 66 | if support_type == "tetra": 67 | return TetMesh 68 | 69 | 70 | @pytest.fixture(params=["everywhere", "restricted"]) 71 | def region_func(request): 72 | region_type = request.param 73 | 74 | if region_type == "restricted": 75 | return lambda xyz: xyz[:, 0] > 0.5 76 | if region_type == "everywhere": 77 | return lambda xyz: np.ones(xyz.shape[0], dtype=bool) 78 | -------------------------------------------------------------------------------- /tests/integration/test_fold_models.py: -------------------------------------------------------------------------------- 1 | from LoopStructural import GeologicalModel 2 | from LoopStructural.modelling.features import GeologicalFeature 3 | from LoopStructural.datasets import load_noddy_single_fold 4 | 5 | import pandas as pd 6 | import numpy as np 7 | 8 | data, boundary_points = load_noddy_single_fold() 9 | data.head() 10 | 11 | 12 | def test_average_fold_axis(): 13 | mdata = pd.concat([data[:100], data[data["feature_name"] == "s1"]]) 14 | model = GeologicalModel(boundary_points[0, :], boundary_points[1, :]) 15 | model.set_model_data(mdata) 16 | fold_frame = model.create_and_add_fold_frame("s1", nelements=10000) 17 | stratigraphy = model.create_and_add_folded_foliation( 18 | "s0", 19 | fold_frame=fold_frame, 20 | nelements=10000, 21 | av_fold_axis=True, 22 | # fold_axis=[-6.51626577e-06, -5.00013645e-01, -8.66017526e-01], 23 | # limb_wl=1 24 | ) 25 | model.update() 26 | 27 | assert np.isclose( 28 | stratigraphy.fold.fold_axis, 29 | np.array([-6.51626577e-06, -5.00013645e-01, -8.66017526e-01]), 30 | rtol=1e-3, 31 | atol=1e-3, 32 | ).all() 33 | 34 | 35 | def test_fixed_fold_axis(): 36 | mdata = pd.concat([data[:100], data[data["feature_name"] == "s1"]]) 37 | model = GeologicalModel(boundary_points[0, :], boundary_points[1, :]) 38 | model.set_model_data(mdata) 39 | fold_frame = model.create_and_add_fold_frame("s1", nelements=10000) 40 | stratigraphy = model.create_and_add_folded_foliation( 41 | "s0", 42 | fold_frame=fold_frame, 43 | nelements=10000, 44 | # av_fold_axis=True 45 | fold_axis=[-6.51626577e-06, -5.00013645e-01, -8.66017526e-01], 46 | # limb_wl=1 47 | ) 48 | model.update() 49 | assert np.isclose( 50 | stratigraphy.fold.fold_axis, np.array([-6.51626577e-06, -5.00013645e-01, -8.66017526e-01]) 51 | ).all() 52 | 53 | 54 | def test_fixed_wavelength(): 55 | mdata = pd.concat([data[:100], data[data["feature_name"] == "s1"]]) 56 | model = GeologicalModel(boundary_points[0, :], boundary_points[1, :]) 57 | model.set_model_data(mdata) 58 | fold_frame = model.create_and_add_fold_frame("s1", nelements=10000) 59 | stratigraphy = model.create_and_add_folded_foliation( 60 | "s0", 61 | fold_frame=fold_frame, 62 | nelements=10000, 63 | # av_fold_axis=True 64 | fold_axis=[-6.51626577e-06, -5.00013645e-01, -8.66017526e-01], 65 | limb_wl=1, 66 | ) 67 | model.update() 68 | assert isinstance(stratigraphy, GeologicalFeature) 69 | 70 | 71 | def test_no_fold_frame(): 72 | mdata = pd.concat([data[:100], data[data["feature_name"] == "s1"]]) 73 | model = GeologicalModel(boundary_points[0, :], boundary_points[1, :]) 74 | model.set_model_data(mdata) 75 | fold_frame = model.create_and_add_fold_frame("s1", nelements=10000) 76 | stratigraphy = model.create_and_add_folded_foliation( 77 | "s0", 78 | # fold_frame, 79 | nelements=10000, 80 | # av_fold_axis=True 81 | fold_axis=[-6.51626577e-06, -5.00013645e-01, -8.66017526e-01], 82 | limb_wl=1, 83 | ) 84 | model.update() 85 | assert isinstance(stratigraphy, GeologicalFeature) 86 | assert stratigraphy.fold.foldframe.name == "s1" 87 | assert fold_frame.name == "s1" 88 | -------------------------------------------------------------------------------- /tests/integration/test_refolded.py: -------------------------------------------------------------------------------- 1 | from LoopStructural import GeologicalModel 2 | from LoopStructural.modelling.features import StructuralFrame 3 | from LoopStructural.datasets import load_laurent2016 4 | 5 | 6 | def average_axis(): 7 | data, bb = load_laurent2016() 8 | 9 | model = GeologicalModel(bb[0, :], bb[1, :]) 10 | model.set_model_data(data) 11 | s2 = model.create_and_add_fold_frame("s2", nelements=10000) 12 | 13 | s1 = model.create_and_add_folded_fold_frame( 14 | "s1", limb_wl=0.4, av_fold_axis=True, nelements=50000 15 | ) 16 | 17 | s0 = model.create_and_add_folded_fold_frame( 18 | "s0", limb_wl=1.0, av_fold_axis=True, nelements=50000 19 | ) 20 | assert s1.fold.foldframe.name == 's2' 21 | assert s0.fold.foldframe.name == 's1' 22 | assert isinstance(s2, StructuralFrame) 23 | -------------------------------------------------------------------------------- /tests/unit/datatypes/test__structured_grid.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from LoopStructural.datatypes._structured_grid import StructuredGrid 4 | from LoopStructural.utils import rng 5 | 6 | 7 | def test_structured_grid_to_dict(): 8 | origin = np.array([0, 0, 0]) 9 | nsteps = np.array([10, 10, 10]) 10 | step_vector = np.array([1, 1, 1]) 11 | data = {'rng': rng.random(size=(10, 10, 10))} 12 | name = "grid_data" 13 | 14 | grid = StructuredGrid( 15 | origin=origin, 16 | step_vector=step_vector, 17 | nsteps=nsteps, 18 | properties=data, 19 | cell_properties={}, 20 | name=name, 21 | ) 22 | grid_dict = grid.to_dict() 23 | 24 | assert np.all(grid_dict["origin"] == origin) 25 | assert np.all(grid_dict["nsteps"] == nsteps) 26 | assert np.all(grid_dict["step_vector"] == step_vector) 27 | assert np.all(grid_dict["properties"] == data) 28 | assert grid_dict["name"] == name 29 | 30 | 31 | def test_structured_grid_maximum(): 32 | origin = np.array([0, 0, 0]) 33 | nsteps = np.array([10, 10, 10]) 34 | step_vector = np.array([1, 1, 1]) 35 | 36 | grid = StructuredGrid( 37 | origin=origin, 38 | step_vector=step_vector, 39 | nsteps=nsteps, 40 | cell_properties={}, 41 | properties={}, 42 | name=None, 43 | ) 44 | maximum = grid.maximum 45 | 46 | expected_maximum = origin + nsteps * step_vector 47 | assert np.array_equal(maximum, expected_maximum) 48 | 49 | 50 | def test_structured_grid_vtk(): 51 | try: 52 | import pyvista as pv 53 | except ImportError: 54 | pytest.skip("pyvista is required for vtk support") 55 | origin = np.array([0, 0, 0]) 56 | nsteps = np.array([10, 10, 10]) 57 | step_vector = np.array([1, 1, 1]) 58 | data = {'rng': rng.random(size=(10, 10, 10))} 59 | name = "grid_data" 60 | 61 | grid = StructuredGrid( 62 | origin=origin, 63 | step_vector=step_vector, 64 | nsteps=nsteps, 65 | properties=data, 66 | cell_properties={}, 67 | name=name, 68 | ) 69 | vtk_grid = grid.vtk() 70 | grid_points = vtk_grid.points 71 | grid_origin = np.min(grid_points, axis=0) 72 | 73 | # Add assertions to validate the generated vtk_grid 74 | assert isinstance(vtk_grid, pv.RectilinearGrid) 75 | assert np.array_equal(vtk_grid.dimensions, nsteps) 76 | assert np.array_equal(grid_origin, origin) 77 | assert np.array_equal(vtk_grid['rng'], data['rng'].flatten(order="F")) 78 | 79 | 80 | if __name__ == "__main__": 81 | test_structured_grid_to_dict() 82 | test_structured_grid_maximum() 83 | test_structured_grid_vtk() 84 | -------------------------------------------------------------------------------- /tests/unit/datatypes/test__surface.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from LoopStructural.datatypes._surface import Surface 4 | 5 | 6 | def test_surface_creation(): 7 | vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) 8 | triangles = np.array([[0, 1, 2]]) 9 | normals = np.array([[0, 0, 1]]) 10 | name = "surface1" 11 | values = np.array([1, 2, 3]) 12 | 13 | surface = Surface( 14 | vertices=vertices, triangles=triangles, normals=normals, name=name, values=values 15 | ) 16 | 17 | assert np.array_equal(surface.vertices, vertices) 18 | assert np.array_equal(surface.triangles, triangles) 19 | assert np.array_equal(surface.normals, normals) 20 | assert surface.name == name 21 | assert np.array_equal(surface.values, values) 22 | 23 | 24 | def test_surface_vtk(): 25 | import importlib.util 26 | 27 | if importlib.util.find_spec('pyvista') is None: 28 | pytest.skip("pyvista is required for vtk support") 29 | vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) 30 | triangles = np.array([[0, 1, 2]]) 31 | normals = np.array([[0, 0, 1]]) 32 | name = "surface1" 33 | values = np.array([1, 2, 3]) 34 | 35 | surface = Surface( 36 | vertices=vertices, triangles=triangles, normals=normals, name=name, values=values 37 | ) 38 | vtk_surface = surface.vtk() 39 | 40 | assert vtk_surface.n_points == len(vertices) 41 | assert vtk_surface.n_cells == len(triangles) 42 | assert np.array_equal(vtk_surface.point_data["values"], values) 43 | 44 | 45 | def test_surface_to_dict(): 46 | vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]]) 47 | triangles = np.array([[0, 1, 2]]) 48 | normals = np.array([[0, 0, 1]]) 49 | name = "surface1" 50 | values = np.array([1, 2, 3]) 51 | 52 | surface = Surface( 53 | vertices=vertices, triangles=triangles, normals=normals, name=name, values=values 54 | ) 55 | surface_dict = surface.to_dict() 56 | 57 | assert np.array_equal(surface_dict["vertices"], vertices) 58 | assert np.array_equal(surface_dict["triangles"], triangles) 59 | assert np.array_equal(surface_dict["normals"], normals) 60 | assert surface_dict["name"] == name 61 | assert np.array_equal(surface_dict["values"], values) 62 | -------------------------------------------------------------------------------- /tests/unit/input/test_data_processor.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.modelling import ProcessInputData 2 | from LoopStructural.utils import rng 3 | import pandas as pd 4 | import numpy as np 5 | 6 | 7 | def test_create_processor(): 8 | df = pd.DataFrame(rng.random(size=(10, 3)), columns=["X", "Y", "Z"]) 9 | df["name"] = ["unit_{}".format(name % 2) for name in range(10)] 10 | stratigraphic_order = [("sg", ["unit_0", "unit_1", "basement"])] 11 | thicknesses = {"unit_0": 1.0, "unit_1": 0.5} 12 | processor = ProcessInputData( 13 | contacts=df, stratigraphic_order=stratigraphic_order, thicknesses=thicknesses 14 | ) 15 | assert (processor.data["val"].unique() == np.array([0.5, 0])).all() 16 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_2d_discrete_support.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.interpolators import StructuredGrid2D 2 | import numpy as np 3 | 4 | 5 | ## structured grid 2d tests 6 | def test_create_structured_grid2d(): 7 | grid = StructuredGrid2D() 8 | assert isinstance(grid, StructuredGrid2D) 9 | 10 | 11 | def test_create_structured_grid2d_origin_nsteps(): 12 | grid = StructuredGrid2D(origin=np.zeros(2), nsteps=np.array([5, 5])) 13 | assert grid.n_nodes == 5 * 5 14 | assert np.sum(grid.maximum - np.ones(2) * 5) == 0 15 | 16 | 17 | def test_create_structured_grid2d_origin_nsteps_sv(): 18 | grid = StructuredGrid2D( 19 | origin=np.zeros(2), nsteps=np.array([10, 10]), step_vector=np.array([0.1, 0.1]) 20 | ) 21 | assert np.sum(grid.step_vector - np.array([0.1, 0.1])) == 0 22 | assert np.sum(grid.maximum - np.ones(2)) == 0 23 | 24 | 25 | def test_evaluate_value_2d(): 26 | grid = StructuredGrid2D() 27 | # grid.update_property("X", grid.nodes[:, 0]) 28 | assert ( 29 | np.sum(grid.barycentre[:, 0] - grid.evaluate_value(grid.barycentre, grid.nodes[:, 0])) == 0 30 | ) 31 | 32 | 33 | def test_evaluate_gradient_2d(): 34 | grid = StructuredGrid2D() 35 | # grid.update_property("Y", ) 36 | vector = np.mean(grid.evaluate_gradient(grid.barycentre, grid.nodes[:, 1]), axis=0) 37 | # vector/=np.linalg.norm(vector) 38 | assert np.sum(vector - np.array([0, grid.step_vector[1]])) == 0 39 | 40 | 41 | def test_get_element_2d(): 42 | grid = StructuredGrid2D() 43 | point = grid.barycentre[[0], :] 44 | idc, inside = grid.position_to_cell_corners(point) 45 | bary = np.mean(grid.nodes[idc, :], axis=0) 46 | assert np.sum(point - bary) == 0 47 | 48 | 49 | def test_global_to_local_coordinates2d(): 50 | grid = StructuredGrid2D() 51 | point = np.array([[1.2, 1.5, 1.7]]) 52 | local_coords = grid.position_to_local_coordinates(point) 53 | assert np.isclose(local_coords[0, 0], 0.2) 54 | assert np.isclose(local_coords[0, 1], 0.5) 55 | 56 | 57 | def test_get_element_outside2d(): 58 | grid = StructuredGrid2D() 59 | point = np.array([grid.origin - np.ones(2)]) 60 | idc, inside = grid.position_to_cell_corners(point) 61 | assert not inside[0] 62 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_discrete_interpolator.py: -------------------------------------------------------------------------------- 1 | def test_nx(interpolator, data): 2 | assert interpolator.nx == 21 * 21 * 21 3 | 4 | 5 | def test_region(interpolator, data, region_func): 6 | """Test to see whether restricting the interpolator to a region works""" 7 | # interpolator = generate_interpolator(interpolator) 8 | interpolator.set_value_constraints(data[["X", "Y", "Z", "val", "w"]].to_numpy()) 9 | interpolator.setup_interpolator() 10 | interpolator.set_region(region_func) 11 | # assert np.all(interpolator.region == region_func(interpolator.support.nodes)) 12 | interpolator.solve_system() 13 | 14 | 15 | def test_add_constraint_to_least_squares(interpolator): 16 | """make sure that when incorrect sized arrays are passed it doesn't get added""" 17 | pass 18 | 19 | 20 | def test_update_interpolator(): 21 | pass 22 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_geological_interpolator.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def test_get_data_locations(interpolator, data): 5 | interpolator.set_value_constraints( 6 | data.loc[~data["val"].isna(), ["X", "Y", "Z", "val", "w"]].to_numpy() 7 | ) 8 | interpolator.set_normal_constraints( 9 | data.loc[~data["nx"].isna(), ["X", "Y", "Z", "nx", "ny", "nz", "w"]].to_numpy() 10 | ) 11 | locations = interpolator.get_data_locations() 12 | assert np.sum(locations - data[["X", "Y", "Z"]].to_numpy()) == 0 13 | 14 | 15 | def test_get_value_constraints(interpolator, data): 16 | interpolator.set_value_constraints( 17 | data.loc[~data["val"].isna(), ["X", "Y", "Z", "val", "w"]].to_numpy() 18 | ) 19 | interpolator.set_normal_constraints( 20 | data.loc[~data["nx"].isna(), ["X", "Y", "Z", "nx", "ny", "nz", "w"]].to_numpy() 21 | ) 22 | val = interpolator.get_value_constraints() 23 | assert np.sum(val - data.loc[~data["val"].isna(), ["X", "Y", "Z", "val", "w"]].to_numpy()) == 0 24 | 25 | 26 | def test_get_norm_constraints(interpolator, data): 27 | interpolator.set_value_constraints( 28 | data.loc[~data["val"].isna(), ["X", "Y", "Z", "val", "w"]].to_numpy() 29 | ) 30 | interpolator.set_normal_constraints( 31 | data.loc[~data["nx"].isna(), ["X", "Y", "Z", "nx", "ny", "nz", "w"]].to_numpy() 32 | ) 33 | val = interpolator.get_norm_constraints() 34 | assert ( 35 | np.sum( 36 | val - data.loc[~data["nx"].isna(), ["X", "Y", "Z", "nx", "ny", "nz", "w"]].to_numpy() 37 | ) 38 | == 0 39 | ) 40 | 41 | 42 | def test_reset(interpolator, data): 43 | interpolator.set_value_constraints( 44 | data.loc[~data["val"].isna(), ["X", "Y", "Z", "val", "w"]].to_numpy() 45 | ) 46 | interpolator.set_normal_constraints( 47 | data.loc[~data["nx"].isna(), ["X", "Y", "Z", "nx", "ny", "nz", "w"]].to_numpy() 48 | ) 49 | interpolator.clean() 50 | assert interpolator.get_data_locations().shape[0] == 0 51 | assert not interpolator.up_to_date 52 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_interpolator_builder.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import numpy as np 3 | from LoopStructural.datatypes import BoundingBox 4 | from LoopStructural.interpolators._interpolator_builder import InterpolatorBuilder 5 | from LoopStructural.interpolators import InterpolatorType 6 | 7 | 8 | @pytest.fixture 9 | def setup_builder(): 10 | bounding_box = BoundingBox(np.array([0, 0, 0]), np.array([1, 1, 1])) 11 | interpolatortype = InterpolatorType.FINITE_DIFFERENCE 12 | nelements = 1000 13 | buffer = 0.2 14 | builder = InterpolatorBuilder( 15 | interpolatortype=interpolatortype, 16 | bounding_box=bounding_box, 17 | nelements=nelements, 18 | buffer=buffer, 19 | ) 20 | return builder 21 | 22 | 23 | def test_create_interpolator(setup_builder): 24 | builder = setup_builder 25 | builder.build() 26 | assert builder.interpolator is not None, "Interpolator should be created" 27 | 28 | 29 | def test_set_value_constraints(setup_builder): 30 | builder = setup_builder 31 | builder.build() 32 | value_constraints = np.array([[0.5, 0.5, 0.5, 1.0, 1.0]]) 33 | builder.add_value_constraints(value_constraints) 34 | assert np.array_equal( 35 | builder.interpolator.data["value"], value_constraints 36 | ), "Value constraints should be set correctly" 37 | 38 | 39 | def test_set_gradient_constraints(setup_builder): 40 | builder = setup_builder 41 | gradient_constraints = np.array([[0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0]]) 42 | builder.add_gradient_constraints(gradient_constraints) 43 | assert np.array_equal( 44 | builder.interpolator.data["gradient"], gradient_constraints 45 | ), "Gradient constraints should be set correctly" 46 | 47 | 48 | def test_set_normal_constraints(setup_builder): 49 | builder = setup_builder 50 | normal_constraints = np.array([[0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0]]) 51 | builder.add_normal_constraints(normal_constraints) 52 | assert np.array_equal( 53 | builder.interpolator.data["normal"], normal_constraints 54 | ), "Normal constraints should be set correctly" 55 | 56 | 57 | def test_setup_interpolator(setup_builder): 58 | builder = setup_builder 59 | builder.build() 60 | value_constraints = np.array([[0.5, 0.5, 0.5, 1.0, 1.0]]) 61 | interpolator = builder.add_value_constraints(value_constraints).setup_interpolator().build() 62 | assert interpolator is not None, "Interpolator should be set up" 63 | assert np.array_equal( 64 | interpolator.data["value"], value_constraints 65 | ), "Value constraints should be set correctly after setup" 66 | 67 | 68 | def test_evaluate_scalar_value(setup_builder): 69 | builder = setup_builder 70 | builder.build() 71 | value_constraints = np.array([[0.5, 0.5, 0.5, 1.0]]) 72 | interpolator = builder.add_value_constraints(value_constraints).setup_interpolator().build() 73 | locations = np.array([[0.5, 0.5, 0.5]]) 74 | values = interpolator.evaluate_value(locations) 75 | assert values is not None, "Evaluation should return values" 76 | assert values.shape == (1,), "Evaluation should return correct shape" 77 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_normal_magnitude_interpolators.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from LoopStructural import GeologicalModel 4 | 5 | @pytest.mark.parametrize("interpolator_type", ["PLI", "FDI"]) 6 | @pytest.mark.parametrize("magnitude", [0.1, 0.5, 1.0, 2.0, 5.0]) 7 | @pytest.mark.parametrize("normal_direction", [ 8 | [1, 0, 0], # x-axis 9 | [0, 1, 0], # y-axis 10 | [0, 0, 1], # z-axis 11 | [1, 1, 0], # xy diagonal 12 | [0, 1, 1], # yz diagonal 13 | [1, 0, 1], # xz diagonal 14 | [1, 1, 1], # xyz diagonal 15 | [-1, 1, 0], # negative x 16 | [0, -1, 1], # negative y 17 | [1, 0, -1], # negative z 18 | [2, 1, 3], # arbitrary non-axis 19 | [-2, 2, 1], # arbitrary non-axis 20 | [0.5, -1.5, 2], # arbitrary non-axis 21 | [1, 2, -2], # arbitrary non-axis 22 | [-1, -1, 2], # arbitrary non-axis 23 | ]) 24 | def test_gradient_magnitude_with_normal_constraint(interpolator_type, magnitude, normal_direction): 25 | # Create a simple model domain 26 | origin = np.zeros(3) 27 | maximum = np.ones(3) 28 | model = GeologicalModel(origin, maximum) 29 | 30 | # Set up a single normal constraint at the center 31 | center = np.array([[0.5, 0.5, 0.5]]) 32 | normal = np.array([normal_direction], dtype=float) 33 | normal = normal / np.linalg.norm(normal) * magnitude 34 | data = np.hstack([center, normal, [[np.nan]]]) 35 | import pandas as pd 36 | df = pd.DataFrame(data, columns=["X", "Y", "Z", "nx", "ny", "nz", "val" ]) 37 | df["feature_name"] = "strati" 38 | model.data = df 39 | model.create_and_add_foliation("strati", interpolatortype=interpolator_type) 40 | model.update() 41 | 42 | # Evaluate the gradient at the constraint location 43 | grad = model["strati"].evaluate_gradient(center)[0] 44 | grad_mag = np.linalg.norm(grad) 45 | # The direction should match, and the magnitude should be close to the input magnitude 46 | assert np.allclose(grad / grad_mag, normal[0] / magnitude, atol=1e-2) 47 | assert np.isclose(grad_mag, magnitude, atol=0.2) 48 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_outside_box.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | from LoopStructural import GeologicalModel 4 | 5 | 6 | def test_outside_box_normal(): 7 | for interpolator in ["PLI", "FDI"]: 8 | print(f"Running test for {interpolator} with normal constraints") 9 | model = GeologicalModel(np.zeros(3), np.ones(3)) 10 | data = pd.DataFrame( 11 | [ 12 | [0.5, 0.5, 0.5, 0, 1.0, 0.0, np.nan, "strati"], 13 | [1.5, 0.5, 0.5, 0, 1.0, 0.0, np.nan, "strati"], 14 | [0.5, 1.5, 1.5, 0, 1.0, 0.0, 1.0, "strati"], 15 | ], 16 | columns=["X", "Y", "Z", "nx", "ny", "nz", "val", "feature_name"], 17 | ) 18 | model.data = data 19 | model.create_and_add_foliation("strati", interpolatortype=interpolator) 20 | model.update() 21 | 22 | 23 | def test_outside_box_value(): 24 | for interpolator in ["PLI", "FDI"]: 25 | print(f"Running test for {interpolator} with normal constraints") 26 | 27 | model = GeologicalModel(np.zeros(3), np.ones(3)) 28 | data = pd.DataFrame( 29 | [ 30 | [0.5, 0.5, 0.5, 0, "strati"], 31 | [1.5, 0.5, 0.5, 0, "strati"], 32 | [0.5, 1.5, 1.5, 0, "strati"], 33 | ], 34 | columns=["X", "Y", "Z", "val", "feature_name"], 35 | ) 36 | model.data = data 37 | model.create_and_add_foliation("strati", interpolatortype=interpolator) 38 | model.update() 39 | 40 | 41 | if __name__ == "__main__": 42 | test_outside_box_normal() 43 | test_outside_box_value() 44 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_solvers.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/unit/interpolator/test_unstructured_supports.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from LoopStructural.interpolators import UnStructuredTetMesh 3 | from LoopStructural.utils import rng 4 | from os.path import dirname 5 | 6 | file_path = dirname(__file__) 7 | 8 | 9 | def test_get_elements(): 10 | nodes = np.loadtxt("{}/nodes.txt".format(file_path)) 11 | elements = np.loadtxt("{}/elements.txt".format(file_path)) 12 | elements = np.array(elements, dtype="int64") 13 | neighbours = np.loadtxt("{}/neighbours.txt".format(file_path)) 14 | 15 | mesh = UnStructuredTetMesh(nodes, elements, neighbours) 16 | points = rng.random((100, 3)) 17 | verts, c, tetra, inside = mesh.get_element_for_location(points) 18 | 19 | vertices = nodes[elements, :] 20 | pos = points[:, :] 21 | vap = pos[:, None, :] - vertices[None, :, 0, :] 22 | vbp = pos[:, None, :] - vertices[None, :, 1, :] 23 | # # vcp = p - points[:, 2, :] 24 | # # vdp = p - points[:, 3, :] 25 | vab = vertices[None, :, 1, :] - vertices[None, :, 0, :] 26 | vac = vertices[None, :, 2, :] - vertices[None, :, 0, :] 27 | vad = vertices[None, :, 3, :] - vertices[None, :, 0, :] 28 | vbc = vertices[None, :, 2, :] - vertices[None, :, 1, :] 29 | vbd = vertices[None, :, 3, :] - vertices[None, :, 1, :] 30 | 31 | va = np.einsum("ikj, ikj->ik", vbp, np.cross(vbd, vbc, axisa=2, axisb=2)) / 6.0 32 | vb = np.einsum("ikj, ikj->ik", vap, np.cross(vac, vad, axisa=2, axisb=2)) / 6.0 33 | vc = np.einsum("ikj, ikj->ik", vap, np.cross(vad, vab, axisa=2, axisb=2)) / 6.0 34 | vd = np.einsum("ikj, ikj->ik", vap, np.cross(vab, vac, axisa=2, axisb=2)) / 6.0 35 | v = np.einsum("ikj, ikj->ik", vab, np.cross(vac, vad, axisa=2, axisb=2)) / 6.0 36 | # c = np.zeros((va.shape[0], 4)) 37 | # c[:, 0] = va / v 38 | # c[:, 1] = vb / v 39 | # c[:, 2] = vc / v 40 | # c[:, 3] = vd / v 41 | c = np.zeros((pos.shape[0], va.shape[1], 4)) 42 | c[:, :, 0] = va / v 43 | c[:, :, 1] = vb / v 44 | c[:, :, 2] = vc / v 45 | 46 | c[:, :, 3] = vd / v 47 | row, col = np.where(np.all(c >= 0, axis=2)) 48 | 49 | tetra_idx = col 50 | 51 | # check if the calculated tetra from the mesh method using aabb 52 | # is the same as using the barycentric coordinates on all elelemts for 53 | # all points 54 | print(elements[tetra_idx] - elements[tetra]) 55 | assert np.all(elements[tetra_idx] - elements[tetra] == 0) 56 | -------------------------------------------------------------------------------- /tests/unit/modelling/test__bounding_box.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from LoopStructural.datatypes._bounding_box import BoundingBox 4 | 5 | 6 | def test_bounding_box_creation(): 7 | origin = np.array([0, 0, 0]) 8 | maximum = np.array([1, 1, 1]) 9 | nsteps = np.array([10, 10, 10]) 10 | step_vector = (maximum - origin) / nsteps 11 | 12 | bbox = BoundingBox(origin=origin, maximum=maximum, nsteps=nsteps, step_vector=step_vector) 13 | assert np.all(np.isclose(bbox.origin, origin)) 14 | assert np.all(np.isclose(bbox.maximum, maximum)) 15 | assert np.all(np.isclose(bbox.nsteps, nsteps)) 16 | assert np.all(np.isclose(bbox.step_vector, step_vector)) 17 | 18 | 19 | def test_bounding_box_fit(): 20 | locations = np.array([[0.5, 0.5, 0.5], [0.2, 0.3, 0.4], [0.8, 0.9, 1.0]]) 21 | expected_origin = np.array([0.2, 0.3, 0.4]) 22 | expected_maximum = np.array([0.8, 0.9, 1.0]) 23 | 24 | bbox = BoundingBox() 25 | bbox.fit(locations) 26 | assert np.all(np.isclose(bbox.origin, expected_origin)) 27 | assert np.all(np.isclose(bbox.maximum, expected_maximum)) 28 | assert np.all(np.isclose(bbox.maximum, expected_maximum)) 29 | assert np.all(np.isclose(bbox.global_origin, np.zeros(3))) 30 | bbox.fit(locations, local_coordinate=True) 31 | assert np.all(np.isclose(bbox.origin, np.zeros(3))) 32 | assert np.all(np.isclose(bbox.maximum, expected_maximum - expected_origin)) 33 | assert np.all(np.isclose(bbox.global_origin, expected_origin)) 34 | 35 | 36 | def test_bounding_box_volume(): 37 | origin = np.array([0, 0, 0]) 38 | maximum = np.array([1, 1, 1]) 39 | nsteps = np.array([10, 10, 10]) 40 | step_vector = (maximum - origin) / nsteps 41 | 42 | bbox = BoundingBox(origin=origin, maximum=maximum, nsteps=nsteps, step_vector=step_vector) 43 | 44 | expected_volume = 1.0 45 | assert bbox.volume == expected_volume 46 | 47 | 48 | def test_bounding_box_is_inside(): 49 | origin = np.array([0, 0, 0]) 50 | maximum = np.array([1, 1, 1]) 51 | nsteps = np.array([10, 10, 10]) 52 | step_vector = (maximum - origin) / nsteps 53 | 54 | bbox = BoundingBox(origin=origin, maximum=maximum, nsteps=nsteps, step_vector=step_vector) 55 | 56 | inside_points = np.array([[0.5, 0.5, 0.5], [0.2, 0.3, 0.4]]) 57 | outside_points = np.array([[1.5, 1.5, 1.5], [-0.1, -0.2, -0.3]]) 58 | 59 | assert np.all(bbox.is_inside(inside_points)) 60 | assert not np.any(bbox.is_inside(outside_points)) 61 | 62 | 63 | def test_local_and_global_origin(): 64 | origin = np.array([0, 0, 0]) 65 | maximum = np.array([1, 1, 1]) 66 | nsteps = np.array([10, 10, 10]) 67 | step_vector = (maximum - origin) / nsteps 68 | 69 | bbox = BoundingBox(origin=origin, maximum=maximum, nsteps=nsteps, step_vector=step_vector) 70 | assert np.all(np.isclose(bbox.global_origin, origin)) 71 | assert np.all(np.isclose(bbox.global_maximum, maximum)) 72 | assert np.all(np.isclose(bbox.origin, np.zeros(3))) 73 | assert np.all(np.isclose(bbox.maximum, maximum - origin)) 74 | 75 | 76 | def test_buffer(): 77 | origin = np.array([0, 0, 0]) 78 | maximum = np.array([1, 1, 1]) 79 | nsteps = np.array([10, 10, 10]) 80 | step_vector = (maximum - origin) / nsteps 81 | 82 | bbox = BoundingBox(origin=origin, maximum=maximum, nsteps=nsteps, step_vector=step_vector) 83 | bbox2 = bbox.with_buffer(0.1) 84 | assert np.all(np.isclose(bbox2.origin, np.array([-0.1, -0.1, -0.1]))) 85 | assert np.all(np.isclose(bbox2.maximum, np.array([1.1, 1.1, 1.1]))) 86 | 87 | 88 | if __name__ == "__main__": 89 | pytest.main([__file__]) 90 | -------------------------------------------------------------------------------- /tests/unit/modelling/test_faults_segment.py: -------------------------------------------------------------------------------- 1 | from LoopStructural import GeologicalModel 2 | from LoopStructural.modelling.features.fault import FaultSegment 3 | import pandas as pd 4 | 5 | 6 | def test_create_and_add_fault(): 7 | model = GeologicalModel([0, 0, 0], [1, 1, 1]) 8 | data = pd.DataFrame( 9 | [ 10 | [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 11 | # [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 12 | [0.5, 0.5, 0.5, 1, 0, 0, 1, "fault", 0], 13 | [0.5, 0.5, 0.5, 0, 0, 1, 2, "fault", 0], 14 | ], 15 | columns=["X", "Y", "Z", "nx", "ny", "nz", "coord", "feature_name", "val"], 16 | ) 17 | model.data = data 18 | model.create_and_add_fault( 19 | "fault", 20 | 1, 21 | nelements=1e4, 22 | # force_mesh_geometry=True 23 | ) 24 | assert isinstance(model["fault"], FaultSegment) 25 | 26 | 27 | def test_fault_displacement(): 28 | model = GeologicalModel([0, 0, 0], [1, 1, 1]) 29 | data = pd.DataFrame( 30 | [ 31 | [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 32 | # [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 33 | [0.5, 0.5, 0.5, 1, 0, 0, 1, "fault", 0], 34 | [0.5, 0.5, 0.5, 0, 0, 1, 2, "fault", 0], 35 | ], 36 | columns=["X", "Y", "Z", "nx", "ny", "nz", "coord", "feature_name", "val"], 37 | ) 38 | model.data = data 39 | model.create_and_add_fault( 40 | "fault", 41 | 1, 42 | nelements=1e4, 43 | # force_mesh_geometry=True 44 | ) 45 | assert isinstance(model["fault"], FaultSegment) 46 | 47 | 48 | def test_fault_evaluate(): 49 | model = GeologicalModel([0, 0, 0], [1, 1, 1]) 50 | data = pd.DataFrame( 51 | [ 52 | [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 53 | # [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 54 | [0.5, 0.5, 0.5, 1, 0, 0, 1, "fault", 0], 55 | [0.5, 0.5, 0.5, 0, 0, 1, 2, "fault", 0], 56 | ], 57 | columns=["X", "Y", "Z", "nx", "ny", "nz", "coord", "feature_name", "val"], 58 | ) 59 | model.data = data 60 | model.create_and_add_fault( 61 | "fault", 62 | 1, 63 | nelements=1e4, 64 | # force_mesh_geometry=True 65 | ) 66 | assert isinstance(model["fault"], FaultSegment) 67 | 68 | 69 | def test_fault_inside_volume(): 70 | model = GeologicalModel([0, 0, 0], [1, 1, 1]) 71 | data = pd.DataFrame( 72 | [ 73 | [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 74 | # [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 75 | [0.5, 0.5, 0.5, 1, 0, 0, 1, "fault", 0], 76 | [0.5, 0.5, 0.5, 0, 0, 1, 2, "fault", 0], 77 | ], 78 | columns=["X", "Y", "Z", "nx", "ny", "nz", "coord", "feature_name", "val"], 79 | ) 80 | model.data = data 81 | model.create_and_add_fault( 82 | "fault", 83 | 1, 84 | nelements=1e4, 85 | # force_mesh_geometry=True 86 | ) 87 | assert isinstance(model["fault"], FaultSegment) 88 | 89 | 90 | def test_fault_add_abutting(): 91 | model = GeologicalModel([0, 0, 0], [1, 1, 1]) 92 | data = pd.DataFrame( 93 | [ 94 | [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 95 | # [0.5, 0.5, 0.5, 0, 1, 0, 0, "fault", 0], 96 | [0.5, 0.5, 0.5, 1, 0, 0, 1, "fault", 0], 97 | [0.5, 0.5, 0.5, 0, 0, 1, 2, "fault", 0], 98 | ], 99 | columns=["X", "Y", "Z", "nx", "ny", "nz", "coord", "feature_name", "val"], 100 | ) 101 | model.data = data 102 | model.create_and_add_fault( 103 | "fault", 104 | 1, 105 | nelements=1e4, 106 | # force_mesh_geometry=True 107 | ) 108 | assert isinstance(model["fault"], FaultSegment) 109 | 110 | 111 | if __name__ == "__main__": 112 | test_create_and_add_fault() 113 | -------------------------------------------------------------------------------- /tests/unit/modelling/test_fold_event.py: -------------------------------------------------------------------------------- 1 | def test_constructor(): 2 | 3 | pass 4 | 5 | 6 | def test_constant_fold_axis(): 7 | pass 8 | 9 | 10 | def test_rotation_fold_axis(): 11 | pass 12 | -------------------------------------------------------------------------------- /tests/unit/modelling/test_geological_feature.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.modelling.features import ( 2 | GeologicalFeature, 3 | AnalyticalGeologicalFeature, 4 | FeatureType, 5 | ) 6 | import numpy as np 7 | 8 | 9 | def test_constructors(): 10 | # test constructors work and that the types are set correctly 11 | # base_feature = GeologicalFeature("test", None, [], [], None) 12 | # assert base_feature.type == FeatureType.BASE 13 | # assert base_feature.name == "test" 14 | feature = GeologicalFeature("test", None, [], [], None) 15 | assert feature.type == FeatureType.INTERPOLATED 16 | assert feature.name == "test" 17 | feature = AnalyticalGeologicalFeature("test", [0, 0, 1], [0, 0, 0], [], [], None, None) 18 | # for analytical feature check that the evaluated value is correct. 19 | # this should be the distance from origin to the point in the direction 20 | # of the direction vector 21 | assert feature.type == FeatureType.ANALYTICAL 22 | assert feature.name == "test" 23 | assert feature.evaluate_value([0, 0, 0]) == 0 24 | assert np.all(feature.evaluate_gradient([0, 0, 0]) - np.array([0, 0, 1]) == 0) 25 | assert feature.evaluate_value([0, 0, 1]) == 1 26 | assert feature.evaluate_value([0, 0, -1]) == -1 27 | assert feature.evaluate_value([0, 1, 0]) == 0 28 | 29 | 30 | def test_toggle_faults(): 31 | base_feature = GeologicalFeature("test", None, [], [], None) 32 | assert base_feature.faults_enabled is True 33 | base_feature.toggle_faults() 34 | assert base_feature.faults_enabled is False 35 | base_feature.toggle_faults() 36 | assert base_feature.faults_enabled is True 37 | 38 | 39 | def test_tojson(): 40 | base_feature = GeologicalFeature("test", None, [], [], None) 41 | import json 42 | from LoopStructural.utils import LoopJSONEncoder 43 | 44 | json.dumps(base_feature, cls=LoopJSONEncoder) 45 | 46 | 47 | if __name__ == "__main__": 48 | test_constructors() 49 | test_toggle_faults() 50 | test_tojson() 51 | print("All tests passed") 52 | exit(0) 53 | -------------------------------------------------------------------------------- /tests/unit/modelling/test_geological_feature_builder.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_geological_feature_builder_constructor( 4 | interpolatortype, bounding_box, nelements 5 | ): 6 | # builder = GeologicalFeatureBuilder(interpolatortype, bounding_box, nelements) 7 | # assert builder.interpolator == interpolator 8 | pass 9 | 10 | 11 | def test_get_interpolator(): 12 | pass 13 | 14 | 15 | def test_add_data_to_interpolator(interpolator, horizontal_data): 16 | # builder = GeologicalFeatureBuilder(interpolator) 17 | # builder.add_data_from_data_frame(horizontal_data) 18 | # assert builder.data.shape = 19 | pass 20 | 21 | 22 | def test_install_gradient_constraints(): 23 | pass 24 | 25 | 26 | def test_get_value_constraints(): 27 | pass 28 | 29 | 30 | def test_get_gradient_constraints(): 31 | pass 32 | 33 | 34 | def test_get_tangent_constraints(): 35 | pass 36 | 37 | 38 | def test_norm_constraints(): 39 | pass 40 | 41 | 42 | def get_orientation_constraints(): 43 | pass 44 | 45 | 46 | def get_data_locations(): 47 | pass 48 | 49 | 50 | def test_test_interpolation_geometry(): 51 | pass 52 | 53 | 54 | def test_not_up_to_date(): 55 | """test to make sure that the feature 56 | isn't interpolated when everything is set up 57 | """ 58 | pass 59 | 60 | 61 | def test_get_feature(): 62 | pass 63 | 64 | 65 | def test_change_up_to_date(): 66 | pass 67 | -------------------------------------------------------------------------------- /tests/unit/modelling/test_geological_model.py: -------------------------------------------------------------------------------- 1 | from LoopStructural import GeologicalModel 2 | from LoopStructural.datasets import load_claudius 3 | import numpy as np 4 | import pytest 5 | 6 | @pytest.mark.parametrize("origin, maximum", [([0,0,0],[5,5,5]), ([10,10,10],[15,15,15])]) 7 | def test_create_geological_model(origin, maximum): 8 | model = GeologicalModel(origin, maximum) 9 | assert (model.bounding_box.global_origin - np.array(origin)).sum() == 0 10 | assert (model.bounding_box.global_maximum - np.array(maximum)).sum() == 0 11 | assert (model.bounding_box.origin - np.zeros(3)).sum() == 0 12 | assert (model.bounding_box.maximum - np.ones(3)*5).sum() == 0 13 | 14 | def test_rescale_model_data(): 15 | data, bb = load_claudius() 16 | model = GeologicalModel(bb[0, :], bb[1, :]) 17 | model.set_model_data(data) 18 | # Check that the model data is rescaled to local coordinates 19 | expected = data[['X', 'Y', 'Z']].values - bb[None, 0, :] 20 | actual = model.data[['X', 'Y', 'Z']].values 21 | assert np.allclose(actual, expected, atol=1e-6) 22 | def test_access_feature_model(): 23 | data, bb = load_claudius() 24 | model = GeologicalModel(bb[0, :], bb[1, :]) 25 | model.set_model_data(data) 26 | s0 = model.create_and_add_foliation("strati") 27 | assert s0 == model["strati"] 28 | 29 | if __name__ == "__main__": 30 | test_rescale_model_data() -------------------------------------------------------------------------------- /tests/unit/test_imports.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | 3 | 4 | def test_import_model(): 5 | 6 | success = True 7 | if importlib.util.find_spec("LoopStructural", 'GeologicalModel') is None: 8 | success = False 9 | assert success 10 | 11 | 12 | # def test_import_visualisation(): 13 | # success = True 14 | # try: 15 | # from LoopStructural.visualisation import Loop3DView 16 | # except ImportError: 17 | # success = False 18 | # assert success == True 19 | 20 | # def test_import_rotation_angle_plotter(): 21 | # success = True 22 | # try: 23 | # from LoopStructural.visualisation import RotationAnglePlotter 24 | # except ImportError: 25 | # success = False 26 | # assert success == True 27 | 28 | 29 | # def test_import_geological_feature(): 30 | 31 | # success = True 32 | # if importlib.util.find_spec('LoopStructural.modelling.features', ''): 33 | # success = False 34 | # assert success 35 | -------------------------------------------------------------------------------- /tests/unit/utils/test_conversions.py: -------------------------------------------------------------------------------- 1 | from LoopStructural.utils import strikedip2vector, plungeazimuth2vector 2 | import numpy as np 3 | 4 | 5 | def test_strikedip2vector(): 6 | strike = [0, 45, 90] 7 | dip = [30, 60, 90] 8 | expected_result = np.array( 9 | [ 10 | [0.5, 0.0, 0.8660254], 11 | [0.61237, -0.61237, 0.5], 12 | [0.0, -1.0, 0.0], 13 | ] 14 | ) 15 | 16 | result = strikedip2vector(strike, dip) 17 | print(result - expected_result) 18 | assert np.allclose(result, expected_result, atol=1e-3) 19 | 20 | 21 | # import numpy as np 22 | # from LoopStructural.utils.maths import plungeazimuth2vector 23 | 24 | 25 | def test_plungeazimuth2vector_single_values(): 26 | plunge = 0 27 | plunge_dir = 90 28 | expected_result = np.array([[1, 0, 0]]) 29 | result = plungeazimuth2vector(plunge, plunge_dir) 30 | assert np.allclose(result, expected_result) 31 | 32 | 33 | def test_plungeazimuth2vector_array_values(): 34 | plunge = [0, 90, 0] 35 | plunge_dir = [90, 90, 0] 36 | expected_result = np.array( 37 | [ 38 | [1, 0, 0], 39 | [0.0, 0.0, -1.0], 40 | [0, 1, 0], 41 | ] 42 | ) 43 | result = plungeazimuth2vector(plunge, plunge_dir) 44 | assert np.allclose(result, expected_result) 45 | 46 | 47 | def test_plungeazimuth2vector_empty_arrays(): 48 | plunge = [] 49 | plunge_dir = [] 50 | result = plungeazimuth2vector(plunge, plunge_dir) 51 | assert result.shape[0] == 0 52 | -------------------------------------------------------------------------------- /tests/unit/utils/test_math.py: -------------------------------------------------------------------------------- 1 | # from LoopStructural.utils import rotate 2 | # import numpy as np 3 | 4 | 5 | # def test_rotate(): 6 | --------------------------------------------------------------------------------