├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── lint.yaml │ ├── test-coverage.yaml │ └── tests.yaml ├── .gitignore ├── LICENSE ├── README.md ├── docs └── README.assets │ └── pytissue-demo-banenr.jpg ├── pyproject.toml └── pytissueoptics ├── __init__.py ├── __main__.py ├── examples ├── __init__.py ├── benchmarks │ ├── cube60.py │ ├── cubesph60b.py │ ├── env.py │ ├── skinvessel.py │ └── sphshells.py ├── rayscattering │ ├── env.py │ ├── ex01.py │ ├── ex02.py │ ├── ex03.py │ ├── ex04.py │ └── ex05.py └── scene │ ├── droid.obj │ ├── env.py │ ├── example0.py │ ├── example1.py │ ├── example2.py │ ├── example3.py │ └── example4.py ├── rayscattering ├── __init__.py ├── display │ ├── __init__.py │ ├── profiles │ │ ├── __init__.py │ │ ├── profile1D.py │ │ └── profileFactory.py │ ├── utils │ │ ├── __init__.py │ │ └── direction.py │ ├── viewer.py │ └── views │ │ ├── __init__.py │ │ ├── defaultViews.py │ │ ├── view2D.py │ │ └── viewFactory.py ├── energyLogging │ ├── __init__.py │ ├── energyLogger.py │ ├── energyType.py │ ├── pointCloud.py │ └── pointCloudFactory.py ├── fresnel.py ├── materials │ ├── __init__.py │ └── scatteringMaterial.py ├── opencl │ ├── CLPhotons.py │ ├── CLProgram.py │ ├── CLScene.py │ ├── __init__.py │ ├── buffers │ │ ├── CLObject.py │ │ ├── __init__.py │ │ ├── dataPointCL.py │ │ ├── materialCL.py │ │ ├── photonCL.py │ │ ├── seedCL.py │ │ ├── solidCL.py │ │ ├── solidCandidateCL.py │ │ ├── surfaceCL.py │ │ ├── triangleCL.py │ │ └── vertexCL.py │ ├── config │ │ ├── CLConfig.py │ │ ├── IPPTable.py │ │ └── __init__.py │ ├── src │ │ ├── fresnel.c │ │ ├── intersection.c │ │ ├── propagation.c │ │ ├── random.c │ │ ├── scatteringMaterial.c │ │ └── vectorOperators.c │ └── utils │ │ ├── CLKeyLog.py │ │ ├── CLParameters.py │ │ ├── __init__.py │ │ ├── batchTiming.py │ │ └── optimalWorkUnits.py ├── photon.py ├── samples │ ├── __init__.py │ ├── infiniteTissue.py │ └── phantomTissue.py ├── scatteringScene.py ├── source.py ├── statistics │ ├── __init__.py │ └── statistics.py ├── tests │ ├── __init__.py │ ├── display │ │ ├── __init__.py │ │ ├── testImages │ │ │ ├── profile1D.png │ │ │ └── viewXPOS_uYPOS_vZNEG.png │ │ ├── testProfile1D.py │ │ ├── testProfileFactory.py │ │ ├── testView2D.py │ │ ├── testViewFactory.py │ │ └── testViewer.py │ ├── energyLogging │ │ ├── __init__.py │ │ ├── testEnergyLogger.py │ │ ├── testPointCloudFactory.py │ │ └── testPointcloud.py │ ├── materials │ │ ├── __init__.py │ │ └── testScatteringMaterial.py │ ├── opencl │ │ ├── __init__.py │ │ ├── config │ │ │ ├── __init__.py │ │ │ ├── testCLConfig.py │ │ │ └── testIPPTable.py │ │ ├── src │ │ │ ├── CLObjects.py │ │ │ ├── __init__.py │ │ │ ├── testCLFresnel.py │ │ │ ├── testCLIntersection.py │ │ │ ├── testCLPhoton.py │ │ │ ├── testCLRandom.py │ │ │ ├── testCLScatteringMaterial.py │ │ │ ├── testCLSmoothing.py │ │ │ └── testCLVectorOperators.py │ │ ├── testCLKeyLog.py │ │ └── testCLPhotons.py │ ├── statistics │ │ ├── __init__.py │ │ └── testStats.py │ ├── testFresnel.py │ ├── testPhoton.py │ ├── testScatteringScene.py │ ├── testSource.py │ ├── testSourceAccelerated.py │ └── testUtils.py └── utils.py ├── scene ├── __init__.py ├── geometry │ ├── __init__.py │ ├── bbox.py │ ├── polygon.py │ ├── primitives.py │ ├── quad.py │ ├── rotation.py │ ├── surfaceCollection.py │ ├── triangle.py │ ├── utils.py │ ├── vector.py │ └── vertex.py ├── intersection │ ├── __init__.py │ ├── bboxIntersect.py │ ├── intersectionFinder.py │ ├── mollerTrumboreIntersect.py │ ├── ray.py │ └── raySource.py ├── loader │ ├── __init__.py │ ├── loadSolid.py │ ├── loader.py │ └── parsers │ │ ├── __init__.py │ │ ├── obj │ │ ├── __init__.py │ │ └── objParser.py │ │ ├── parsedObject.py │ │ ├── parsedSurface.py │ │ └── parser.py ├── logger │ ├── __init__.py │ ├── listArrayContainer.py │ └── logger.py ├── material.py ├── scene │ ├── __init__.py │ └── scene.py ├── shader │ ├── __init__.py │ └── utils.py ├── solids │ ├── __init__.py │ ├── cone.py │ ├── cube.py │ ├── cuboid.py │ ├── cylinder.py │ ├── ellipsoid.py │ ├── lens.py │ ├── solid.py │ ├── solidFactory.py │ ├── sphere.py │ └── stack │ │ ├── __init__.py │ │ ├── cuboidStacker.py │ │ └── stackResult.py ├── tests │ ├── __init__.py │ ├── geometry │ │ ├── __init__.py │ │ ├── testBoundingBox.py │ │ ├── testPolygon.py │ │ ├── testRotation.py │ │ ├── testSurfaceCollection.py │ │ ├── testTriangle.py │ │ ├── testUtils.py │ │ └── testVector.py │ ├── intersection │ │ ├── __init__.py │ │ ├── benchmarkIntersectionFinder.py │ │ ├── testBoundingBoxIntersect.py │ │ ├── testIntersectionFinder.py │ │ ├── testPolygonIntersect.py │ │ ├── testRay.py │ │ └── testUniformRaySource.py │ ├── loader │ │ ├── __init__.py │ │ ├── parsers │ │ │ ├── __init__.py │ │ │ ├── objFiles │ │ │ │ ├── test.wrongExtension │ │ │ │ ├── testCubeQuads.obj │ │ │ │ ├── testCubeQuadsNoObject.obj │ │ │ │ ├── testCubeQuadsNoObjectName.obj │ │ │ │ ├── testCubeQuadsNoSurface.obj │ │ │ │ ├── testCubeQuadsNoSurfaceName.obj │ │ │ │ ├── testCubeQuadsRepeatingSurface.obj │ │ │ │ ├── testCubeQuadsTexture.obj │ │ │ │ ├── testCubeTriangles.obj │ │ │ │ └── testCubeTrianglesMulti.obj │ │ │ └── testOBJParser.py │ │ ├── testLoadSolid.py │ │ └── testLoader.py │ ├── logger │ │ ├── __init__.py │ │ ├── testListArrayContainer.py │ │ └── testLogger.py │ ├── scene │ │ ├── __init__.py │ │ ├── benchmarkScenes.py │ │ └── testScene.py │ ├── shader │ │ ├── __init__.py │ │ └── testSmoothing.py │ ├── solids │ │ ├── __init__.py │ │ ├── testCone.py │ │ ├── testCuboid.py │ │ ├── testCylinder.py │ │ ├── testEllipsoid.py │ │ ├── testSolid.py │ │ ├── testSolidGroup.py │ │ ├── testSphere.py │ │ └── testThickLens.py │ ├── tree │ │ ├── __init__.py │ │ ├── testSpacePartition.py │ │ └── treeConstructor │ │ │ ├── __init__.py │ │ │ └── binary │ │ │ ├── __init__.py │ │ │ ├── testNoSplitConstructor.py │ │ │ └── testSplitConstructor.py │ ├── utils │ │ ├── __init__.py │ │ └── testNoProgressBar.py │ └── viewer │ │ ├── __init__.py │ │ ├── testImages │ │ ├── images.png │ │ ├── logger_natural.png │ │ ├── scene_natural.png │ │ ├── solid_natural_front.png │ │ ├── solid_optics.png │ │ └── sphere_normals.png │ │ ├── testMayavi3DViewer.py │ │ └── testMayaviSolid.py ├── tree │ ├── __init__.py │ ├── node.py │ ├── spacePartition.py │ └── treeConstructor │ │ ├── __init__.py │ │ ├── binary │ │ ├── __init__.py │ │ ├── noSplitOneAxisConstructor.py │ │ ├── noSplitThreeAxesConstructor.py │ │ ├── sahSearchResult.py │ │ └── splitTreeAxesConstructor.py │ │ ├── splitNodeResult.py │ │ └── treeConstructor.py ├── utils │ ├── __init__.py │ └── progressBar.py └── viewer │ ├── __init__.py │ ├── abstract3DViewer.py │ ├── displayable.py │ ├── mayavi │ ├── __init__.py │ ├── mayavi3DViewer.py │ ├── mayaviNormals.py │ ├── mayaviSolid.py │ ├── mayaviTriangleMesh.py │ └── mayaviVolumeSlicer.py │ ├── null3DViewer.py │ ├── provider.py │ └── viewPoint.py └── testExamples.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Python Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 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/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Python 3.11 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.11 22 | 23 | - name: Install Ruff 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install ruff 27 | 28 | - name: Lint check 29 | run: ruff check pytissueoptics 30 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Linux packages for Qt5/Qt6 support and start Xvfb 19 | uses: pyvista/setup-headless-display-action@v3 20 | with: 21 | qt: true 22 | 23 | - name: Linux OpenCL support 24 | run: | 25 | sudo apt-get update 26 | sudo apt-get install -y pocl-opencl-icd ocl-icd-opencl-dev gcc clinfo 27 | clinfo 28 | 29 | - name: Set up Python 3.11 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: 3.11 33 | 34 | - name: Install dependencies 35 | run: | 36 | python -m pip install --upgrade pip 37 | pip install -e .[dev] 38 | 39 | - name: Run tests with coverage 40 | env: 41 | PTO_CI_MODE: 1 42 | run: | 43 | pytest -v --cov=pytissueoptics --cov-report=xml --cov-report=html 44 | 45 | - name: Upload coverage report 46 | uses: codecov/codecov-action@v4 47 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Test all platforms 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ ubuntu-latest, macos-13, macos-latest, windows-latest ] 16 | python-version: [ "3.9", "3.10", "3.11", "3.12", "3.13" ] 17 | 18 | runs-on: ${{ matrix.os }} 19 | 20 | steps: 21 | - name: Checkout code 22 | uses: actions/checkout@v4 23 | 24 | - name: Linux packages for Qt5/Qt6 support and start Xvfb 25 | if: runner.os == 'Linux' 26 | uses: pyvista/setup-headless-display-action@v3 27 | with: 28 | qt: true 29 | 30 | - name: Linux OpenCL support 31 | if: runner.os == 'Linux' 32 | run: | 33 | sudo apt-get update 34 | sudo apt-get install -y pocl-opencl-icd ocl-icd-opencl-dev gcc clinfo 35 | clinfo 36 | 37 | - name : Windows OpenCL support 38 | if: runner.os == 'Windows' 39 | run: | 40 | choco install opencl-intel-cpu-runtime --no-progress --yes 41 | 42 | - name: Set up Python ${{ matrix.python-version }} 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: ${{ matrix.python-version }} 46 | 47 | - name: Install dependencies 48 | run: | 49 | python -m pip install --upgrade pip 50 | python -m pip install -e .[dev] 51 | python -m pip freeze --all 52 | 53 | - name: Validate OpenCL 54 | if: matrix.os != 'macos-latest' # no OpenCL device in macOS runners since macos-14 55 | run: | 56 | python -c "from pytissueoptics.rayscattering.opencl import OPENCL_OK; assert OPENCL_OK" 57 | 58 | - name: Run tests 59 | env: 60 | PTO_CI_MODE: 1 61 | run: | 62 | python -m pytest -v 63 | -------------------------------------------------------------------------------- /.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 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # celery beat schedule file 95 | celerybeat-schedule 96 | 97 | # SageMath parsed files 98 | *.sage.py 99 | 100 | # Environments 101 | .env 102 | .venv 103 | env*/ 104 | venv*/ 105 | ENV/ 106 | env.bak/ 107 | venv.bak/ 108 | 109 | # Spyder project settings 110 | .spyderproject 111 | .spyproject 112 | 113 | # Rope project settings 114 | .ropeproject 115 | 116 | # mkdocs documentation 117 | /site 118 | 119 | # mypy 120 | .mypy_cache/ 121 | .dmypy.json 122 | dmypy.json 123 | 124 | # Pyre type checker 125 | .pyre/ 126 | output.json 127 | test.py 128 | 129 | .idea/workspace.xml 130 | 131 | .idea/ 132 | 133 | pytissueoptics/scene/examples/brain-simple-2.obj 134 | 135 | pytissueoptics/scene/tests/tree/treeConstructor/profilerTreeConstructor.py 136 | pytissueoptics/rayscattering/tests/_trial_temp/_trial_marker 137 | pytissueoptics/rayscattering/opencl/config.json 138 | pytissueoptics/rayscattering/opencl/ipp.json 139 | .vscode/settings.json 140 | 141 | .DS_Store 142 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Daniel 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 | -------------------------------------------------------------------------------- /docs/README.assets/pytissue-demo-banenr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/docs/README.assets/pytissue-demo-banenr.jpg -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=77.0","setuptools_scm>=7"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project.urls] 6 | GitHub = "https://github.com/DCC-Lab/PyTissueOptics" 7 | 8 | [project] 9 | name = "pytissueoptics" 10 | description = "Python module for 3D Monte Carlo Simulation of Light Propagation" 11 | readme = "README.md" 12 | authors = [ 13 | { name = "Ludovick Begin", email = "ludovick.begin@gmail.com" }, 14 | { name = "Marc-Andre Vigneault", email = "marc-andre.vigneault.2@ulaval.ca" }, 15 | { name = "Daniel Cote", email = "dccote@cervo.ulaval.ca" } 16 | ] 17 | license = "MIT" 18 | license-files = ["LICENSE"] 19 | requires-python = ">=3.9" 20 | dynamic = ["version"] 21 | dependencies = [ 22 | "numpy>=2.0.0", 23 | "matplotlib", 24 | "tqdm", 25 | "psutil", 26 | "configobj", 27 | "Pygments", 28 | "siphash24; python_version < '3.13'", 29 | "pyopencl", 30 | "vtk>=9.4", 31 | "mayavi-dev", 32 | "pyqt5", 33 | ] 34 | 35 | [project.optional-dependencies] 36 | dev = [ 37 | "mockito", 38 | "pytest", 39 | "pytest-cov", 40 | "ruff", 41 | ] 42 | 43 | [tool.setuptools] 44 | packages = ["pytissueoptics"] 45 | 46 | [tool.setuptools.package-data] 47 | "pytissueoptics" = ["rayscattering/opencl/src/*.c", "**/*.obj", "examples/*.py"] 48 | 49 | [tool.setuptools_scm] 50 | version_scheme = "guess-next-dev" 51 | local_scheme = "node-and-date" 52 | 53 | [tool.pytest.ini_options] 54 | testpaths = ["pytissueoptics"] 55 | python_files = ["test*.py"] 56 | 57 | [tool.ruff] 58 | line-length = 120 59 | target-version = "py39" 60 | 61 | [tool.ruff.lint] 62 | ignore = [ 63 | "PT009", # unittest-style asserts 64 | "F405", # Name defined from star imports 65 | ] 66 | 67 | [tool.ruff.format] 68 | quote-style = "double" 69 | indent-style = "space" 70 | -------------------------------------------------------------------------------- /pytissueoptics/__init__.py: -------------------------------------------------------------------------------- 1 | from importlib.metadata import version 2 | 3 | from .rayscattering import * # noqa: F403 4 | from .scene import * # noqa: F403 5 | 6 | __version__ = version("pytissueoptics") 7 | -------------------------------------------------------------------------------- /pytissueoptics/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from pytissueoptics import __version__ 6 | from pytissueoptics.examples import loadExamples 7 | 8 | ap = argparse.ArgumentParser(prog="python -m pytissueoptics", description="Run PyTissueOptics examples. ") 9 | ap.add_argument("-v", "--version", action="version", version=f"PyTissueOptics {__version__}") 10 | ap.add_argument( 11 | "-e", "--examples", required=False, default="all", help="Run specific examples by number, e.g. -e 1,2,3. " 12 | ) 13 | ap.add_argument("-l", "--list", required=False, action="store_true", help="List available examples. ") 14 | ap.add_argument("-t", "--tests", required=False, action="store_true", help="Run unit tests. ") 15 | 16 | args = vars(ap.parse_args()) 17 | runExamples = args["examples"] 18 | runTests = args["tests"] 19 | listExamples = args["list"] 20 | allExamples = loadExamples() 21 | 22 | if listExamples: 23 | print("Available examples:") 24 | for i, example in enumerate(allExamples): 25 | print(f"{i + 1}. {example.name}.py {example.title}") 26 | sys.exit() 27 | 28 | if runTests: 29 | moduleDir = os.path.dirname(__file__) 30 | err = os.system(f"python -m unittest discover -s {moduleDir} -p 'test*.py'") 31 | sys.exit(err) 32 | 33 | if runExamples == "all": 34 | runExamples = list(range(1, len(allExamples) + 1)) 35 | elif runExamples: 36 | runExamples = [int(i) for i in runExamples.split(",")] 37 | else: 38 | print("No examples specified. Use -e 1,2,3 to run specific examples. ") 39 | sys.exit() 40 | 41 | print(f"Running examples {runExamples}...") 42 | for i in runExamples: 43 | example = allExamples[i - 1] 44 | print(f"\nExample {i}: {example.name}.py") 45 | print(f"TITLE: {example.title}") 46 | print(f"DESCRIPTION: {example.description}") 47 | print("\n--------------- Begin source code ---------------") 48 | print(example.sourceCode) 49 | print("---------------- End source code ----------------") 50 | print("\n----------------- Begin output ------------------") 51 | example.func() 52 | print("------------------ End output -------------------") 53 | -------------------------------------------------------------------------------- /pytissueoptics/examples/__init__.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import re 4 | import sys 5 | from dataclasses import dataclass 6 | from typing import List 7 | 8 | from pygments import highlight 9 | from pygments.formatters import TerminalFormatter 10 | from pygments.lexers import PythonLexer 11 | 12 | EXAMPLE_MODULE = "rayscattering" 13 | EXAMPLE_FILE_PATTERN = r"^(ex\d+)\.py$" 14 | EXAMPLE_DIR = os.path.join(os.path.dirname(__file__), EXAMPLE_MODULE) 15 | EXAMPLE_FILES = [file for file in os.listdir(EXAMPLE_DIR) if re.match(EXAMPLE_FILE_PATTERN, file)] 16 | EXAMPLE_FILES.sort() 17 | 18 | sys.path.insert(0, EXAMPLE_DIR) 19 | 20 | 21 | @dataclass 22 | class Example: 23 | name: str 24 | title: str 25 | description: str 26 | func: callable 27 | sourceCode: str 28 | 29 | 30 | def loadExamples() -> List[Example]: 31 | allExamples = [] 32 | for file in EXAMPLE_FILES: 33 | name = re.match(EXAMPLE_FILE_PATTERN, file).group(1) 34 | module = importlib.import_module(f"pytissueoptics.examples.{EXAMPLE_MODULE}.{name}") 35 | with open(os.path.join(EXAMPLE_DIR, file), "r") as f: 36 | srcCode = f.read() 37 | pattern = r"def exampleCode\(\):\s*(.*?)\s*if __name__ == \"__main__\":" 38 | srcCode = re.search(pattern, srcCode, re.DOTALL).group(1) 39 | srcCode = re.sub(r"^ ", "", srcCode, flags=re.MULTILINE) 40 | srcCode = "from pytissueoptics import *\n" + srcCode 41 | srcCode = highlight(srcCode, PythonLexer(), TerminalFormatter()) 42 | allExamples.append(Example(name, module.TITLE, module.DESCRIPTION, module.exampleCode, srcCode)) 43 | return allExamples 44 | -------------------------------------------------------------------------------- /pytissueoptics/examples/benchmarks/cube60.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "MCX Homogeneous cube" 6 | 7 | DESCRIPTION = """ Pencil source propagation through a homogeneous cube of size 60x60x60 mm. """ 8 | 9 | 10 | def exampleCode(): 11 | N = 100000 if hardwareAccelerationIsAvailable() else 1000 12 | 13 | tissue = ScatteringScene([Cube(60, material=ScatteringMaterial(mu_a=0.005, mu_s=1, g=0.01, n=1))]) 14 | logger = EnergyLogger(tissue, defaultBinSize=0.1) 15 | source = PencilPointSource(position=Vector(0, 0, -29.99), direction=Vector(0, 0, 1), N=N, displaySize=1) 16 | 17 | source.propagate(tissue, logger=logger) 18 | 19 | viewer = Viewer(tissue, source, logger) 20 | viewer.reportStats() 21 | 22 | viewer.show2D(View2DProjectionX()) 23 | viewer.show1D(Direction.Z_POS) 24 | viewer.show3D(pointCloudStyle=PointCloudStyle(showSolidPoints=False)) 25 | viewer.show3D() 26 | 27 | 28 | if __name__ == "__main__": 29 | exampleCode() 30 | -------------------------------------------------------------------------------- /pytissueoptics/examples/benchmarks/cubesph60b.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "MCX Sphere" 6 | 7 | DESCRIPTION = """ Pencil source propagation through a homogeneous cube of size 60x60x60 mm with a spherical absorbing inclusion in the center. """ 8 | 9 | 10 | def exampleCode(): 11 | N = 100000 if hardwareAccelerationIsAvailable() else 1000 12 | 13 | cube = Cube(60, material=ScatteringMaterial(mu_a=0.005, mu_s=0, g=0.01, n=1.37)) 14 | sphere = Sphere(15, order=3, material=ScatteringMaterial(mu_a=0.002, mu_s=5, g=0.9, n=1)) 15 | tissue = ScatteringScene([cube, sphere]) 16 | 17 | logger = EnergyLogger(tissue, defaultBinSize=0.1) 18 | source = PencilPointSource(position=Vector(0, 0, -29.99), direction=Vector(0, 0, 1), N=N, displaySize=1) 19 | 20 | source.propagate(tissue, logger=logger) 21 | 22 | viewer = Viewer(tissue, source, logger) 23 | viewer.reportStats() 24 | 25 | viewer.show2D(View2DProjectionX()) 26 | viewer.show2D(View2DProjectionZ()) 27 | viewer.show1D(Direction.Z_POS) 28 | viewer.show3D(visibility=Visibility.DEFAULT_3D | Visibility.VIEWS) 29 | 30 | 31 | if __name__ == "__main__": 32 | exampleCode() 33 | -------------------------------------------------------------------------------- /pytissueoptics/examples/benchmarks/env.py: -------------------------------------------------------------------------------- 1 | # We set up the environment so we do not have to install PyTissueOptics to run the examples. 2 | # By adjusting the path, we use the current version in development. 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) 8 | -------------------------------------------------------------------------------- /pytissueoptics/examples/benchmarks/skinvessel.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "Skin vessel" 6 | 7 | DESCRIPTION = """ Adapted MCX built-in example - a 3-layer domain with a cylindrical vessel inclusion 8 | to simulate skin/vessel measurements. This benchmark was first constructed by Dr. Steve Jacques in his mcxyz software 9 | """ 10 | 11 | 12 | def exampleCode(): 13 | N = 1000000 if hardwareAccelerationIsAvailable() else 100 14 | 15 | # Units in mm and mm-1 16 | waterLayer = Cuboid(1, 0.1, 1, material=ScatteringMaterial(mu_a=0.00004, mu_s=1, g=1, n=1.33), label="water") 17 | epidermisLayer = Cuboid( 18 | 1, 0.06, 1, material=ScatteringMaterial(mu_a=1.65724, mu_s=37.59398, g=0.9, n=1.44), label="epidermis" 19 | ) 20 | dermisLayer = Cuboid( 21 | 1, 0.84, 1, material=ScatteringMaterial(mu_a=0.04585, mu_s=35.65406, g=0.9, n=1.38), label="dermis" 22 | ) 23 | zStack = waterLayer.stack(epidermisLayer).stack(dermisLayer) 24 | zStack.translateTo(Vector(0, 0, 0)) 25 | bloodVessel = Cylinder( 26 | 0.1, 0.99, material=ScatteringMaterial(mu_a=23.05427, mu_s=9.3985, g=0.9, n=1.361), label="blood" 27 | ) 28 | 29 | scene = ScatteringScene([zStack, bloodVessel]) 30 | 31 | source = DirectionalSource( 32 | position=Vector(0, -0.399, 0), direction=Vector(0, 1, 0), N=N, diameter=0.6, displaySize=0.06 33 | ) 34 | logger = EnergyLogger(scene, defaultBinSize=0.001) 35 | source.propagate(scene, logger=logger) 36 | 37 | viewer = Viewer(scene, source, logger) 38 | viewer.reportStats() 39 | viewer.show2D(View2DProjectionX()) 40 | viewer.show2D(View2DProjectionZ()) 41 | viewer.show1D(Direction.Z_POS) 42 | viewer.show1D(Direction.Y_POS) 43 | 44 | # Displaying only the energy that crossed surfaces to save memory. 45 | viewer.show3D(pointCloudStyle=PointCloudStyle(showSolidPoints=False)) 46 | 47 | viewer.show3DVolumeSlicer(0.005) 48 | 49 | 50 | if __name__ == "__main__": 51 | exampleCode() 52 | -------------------------------------------------------------------------------- /pytissueoptics/examples/benchmarks/sphshells.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "MCX Spherical shells" 6 | 7 | DESCRIPTION = """ Adapted MCX built-in example - a 4-layer heterogeneous domain including a thin spherical-shell with 8 | low-scattering/low absorption material to simulate CSF-like tissue in the brain. """ 9 | 10 | 11 | def exampleCode(): 12 | N = 100000 if hardwareAccelerationIsAvailable() else 100 13 | 14 | outerShell = Sphere(2.5, order=2, material=ScatteringMaterial(mu_a=0.04, mu_s=0.09, g=0.89, n=1.37), label="outer") 15 | innerShell = Sphere(2.3, order=2, material=ScatteringMaterial(mu_a=0.2, mu_s=90, g=0.89, n=1.37), label="inner") 16 | core = Sphere(1.0, order=2, material=ScatteringMaterial(mu_a=0.5, mu_s=1e-6, g=1, n=1.37), label="core") 17 | grid = Cuboid(6, 6, 6, material=ScatteringMaterial(mu_a=0.2, mu_s=70, g=0.89, n=1.37)) 18 | tissue = ScatteringScene([core, innerShell, outerShell, grid]) 19 | logger = EnergyLogger(tissue, defaultBinSize=0.1) 20 | source = PencilPointSource(position=Vector(0, 0.01, -3), direction=Vector(0, 0, 1), N=N, displaySize=2) 21 | 22 | source.propagate(tissue, logger=logger) 23 | 24 | viewer = Viewer(tissue, source, logger) 25 | viewer.reportStats() 26 | 27 | viewer.show2D(View2DProjectionX()) 28 | viewer.show2D(View2DProjectionZ()) 29 | viewer.show2D(View2DProjectionX(solidLabel="outer")) 30 | viewer.show2D(View2DProjectionX(solidLabel="inner")) 31 | viewer.show2D(View2DProjectionX(solidLabel="core")) 32 | 33 | viewer.show2D(View2DSurfaceZ(solidLabel="outer", surfaceLabel="ellipsoid")) 34 | viewer.show1D(Direction.Z_POS) 35 | viewer.show3D(pointCloudStyle=PointCloudStyle(showSolidPoints=False)) 36 | viewer.show3D() 37 | 38 | 39 | if __name__ == "__main__": 40 | exampleCode() 41 | -------------------------------------------------------------------------------- /pytissueoptics/examples/rayscattering/env.py: -------------------------------------------------------------------------------- 1 | # We set up the environment so we do not have to install PyTissueOptics to run the examples. 2 | # By adjusting the path, we use the current version in development. 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) 8 | -------------------------------------------------------------------------------- /pytissueoptics/examples/rayscattering/ex01.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "Divergent source propagation through a multi-layered tissue" 6 | 7 | DESCRIPTION = """ Propagation of a divergent source through a sample tissue called PhantomTissue. This tissue is composed 8 | of a stacked cuboid made of 3 layers of different material. """ 9 | 10 | 11 | def exampleCode(): 12 | N = 100000 if hardwareAccelerationIsAvailable() else 1000 13 | 14 | tissue = samples.PhantomTissue() 15 | logger = EnergyLogger(tissue) 16 | source = DivergentSource( 17 | position=Vector(0, 0, -0.2), direction=Vector(0, 0, 1), N=N, diameter=0.1, divergence=0.4, displaySize=0.2 18 | ) 19 | 20 | tissue.show(source=source) 21 | 22 | source.propagate(tissue, logger=logger) 23 | 24 | viewer = Viewer(tissue, source, logger) 25 | viewer.reportStats() 26 | 27 | viewer.show2D(View2DProjectionX()) 28 | viewer.show2D(View2DProjectionX(energyType=EnergyType.FLUENCE_RATE)) 29 | viewer.show2D(View2DProjectionX(solidLabel="middleLayer")) 30 | viewer.show2D(View2DSurfaceZ(solidLabel="middleLayer", surfaceLabel="interface1", surfaceEnergyLeaving=False)) 31 | viewer.show1D(Direction.Z_POS) 32 | viewer.show3D() 33 | viewer.show3D(pointCloudStyle=PointCloudStyle(showSolidPoints=False)) 34 | 35 | 36 | if __name__ == "__main__": 37 | exampleCode() 38 | -------------------------------------------------------------------------------- /pytissueoptics/examples/rayscattering/ex02.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "Propagation in an Infinite Medium" 6 | 7 | DESCRIPTION = """ Propagation of a divergent uniform source through an InfiniteTissue. The optical properties of the 8 | tissue are set to mimic a typical biological tissue. It is not recommended to try to visualize the data in 3D without 9 | binning as this generates a lot of data. """ 10 | 11 | 12 | def exampleCode(): 13 | import math 14 | 15 | N = 10000 if hardwareAccelerationIsAvailable() else 25 16 | 17 | myMaterial = ScatteringMaterial(mu_s=30.0, mu_a=0.1, g=0.9) 18 | tissue = samples.InfiniteTissue(myMaterial) 19 | 20 | logger = EnergyLogger(tissue) 21 | source = DivergentSource( 22 | position=Vector(0, 0, 0), direction=Vector(0, 0, 1), N=N, diameter=0.2, divergence=math.pi / 4 23 | ) 24 | 25 | source.propagate(tissue, logger=logger) 26 | 27 | viewer = Viewer(tissue, source, logger) 28 | viewer.reportStats() 29 | viewer.show2D(View2DProjectionX()) 30 | viewer.show2D(View2DProjectionX(limits=((0, 2), (-1, 1)))) 31 | 32 | 33 | if __name__ == "__main__": 34 | exampleCode() 35 | -------------------------------------------------------------------------------- /pytissueoptics/examples/rayscattering/ex03.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = ( 6 | "Propagate in a in a non-scattering custom scene with an optical lens." 7 | "Learn to save and load your data so you don't have to simulate again." 8 | ) 9 | 10 | DESCRIPTION = """ 11 | Thin Cuboid solids are used as screens for visualization, and a SymmetricLens() as a lens. They all go into a 12 | RayScatteringScene which takes a list of solid. We can display our scene before propagation. Then, we repeat the 13 | usual steps of propagation. You can experiment with different focal lengths and material. The symmetric thick lens will 14 | automatically compute the required surface curvatures to achieve the desired focal length. 15 | 16 | The logger can save data to a file. You can then use logger.load(filepath) or Logger(filepath) to load the data. 17 | At that point you can comment out the line ‘source.propagate()‘ if you don't want to simulate again. You can explore 18 | the different views and information the object Stats provides. 19 | """ 20 | 21 | 22 | def exampleCode(): 23 | N = 1000000 if hardwareAccelerationIsAvailable() else 2000 24 | glassMaterial = ScatteringMaterial(n=1.50) 25 | vacuum = ScatteringMaterial() 26 | screen1 = Cuboid(a=40, b=40, c=0.1, position=Vector(0, 0, 30), material=vacuum, label="Screen1") 27 | screen2 = Cuboid(a=40, b=40, c=0.1, position=Vector(0, 0, 70), material=vacuum, label="Screen2") 28 | screen3 = Cuboid(a=40, b=40, c=0.1, position=Vector(0, 0, 100.05), material=vacuum, label="Screen3") 29 | 30 | lens = SymmetricLens(f=100, diameter=25.4, thickness=3.6, material=glassMaterial, position=Vector(0, 0, 0)) 31 | myCustomScene = ScatteringScene([screen1, screen2, screen3, lens]) 32 | source = DirectionalSource(position=Vector(0, 0, -20), direction=Vector(0, 0, 1), diameter=20.0, N=N, displaySize=5) 33 | 34 | myCustomScene.show(source) 35 | 36 | logger = EnergyLogger(myCustomScene, "ex03.log") 37 | # The following line can be commented out if you only want to load the previously saved data. 38 | # Or leave it in if you want to continue the simulation. 39 | source.propagate(myCustomScene, logger) 40 | 41 | viewer = Viewer(myCustomScene, source, logger) 42 | viewer.reportStats() 43 | 44 | viewer.show3D() 45 | viewer.show2D(View2DSurfaceZ("Screen1", "front", surfaceEnergyLeaving=False)) 46 | viewer.show2D(View2DSurfaceZ("Screen2", "front", surfaceEnergyLeaving=False)) 47 | viewer.show2D(View2DSurfaceZ("Screen3", "front", surfaceEnergyLeaving=False)) 48 | 49 | 50 | if __name__ == "__main__": 51 | exampleCode() 52 | -------------------------------------------------------------------------------- /pytissueoptics/examples/rayscattering/ex04.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "Custom layer stack" 6 | 7 | DESCRIPTION = """ This example shows how to make a layer stack. You initialize multiple Cuboid() and stack them in a 8 | layer stack. You can do this using cuboid.stack(secondCuboid). The stacked face need to have the same size on both 9 | cuboids. """ 10 | 11 | 12 | def exampleCode(): 13 | N = 100000 if hardwareAccelerationIsAvailable() else 500 14 | materialLayer1 = ScatteringMaterial(mu_s=2, mu_a=0.5, g=0.7, n=1.3) 15 | materialLayer2 = ScatteringMaterial(mu_s=5, mu_a=0.8, g=0.8, n=1.4) 16 | materialLayer3 = ScatteringMaterial(mu_s=50, mu_a=2.5, g=0.9, n=1.5) 17 | 18 | layer1 = Cuboid(a=10, b=10, c=1, position=Vector(0, 0, 0), material=materialLayer1, label="Layer 1") 19 | layer2 = Cuboid(a=10, b=10, c=1, position=Vector(0, 0, 0), material=materialLayer2, label="Layer 2") 20 | layer3 = Cuboid(a=10, b=10, c=1, position=Vector(0, 0, 0), material=materialLayer3, label="Layer 3") 21 | stack1 = layer1.stack(layer2, "back") 22 | stackedTissue = stack1.stack(layer3, "back") 23 | 24 | tissue = ScatteringScene([stackedTissue]) 25 | 26 | logger = EnergyLogger(tissue) 27 | source = PencilPointSource(position=Vector(0, 0, -2), direction=Vector(0, 0, 1), N=N, displaySize=0.5) 28 | source.propagate(tissue, logger) 29 | 30 | viewer = Viewer(tissue, source, logger) 31 | viewer.reportStats() 32 | viewer.show3D() 33 | 34 | 35 | if __name__ == "__main__": 36 | exampleCode() 37 | -------------------------------------------------------------------------------- /pytissueoptics/examples/rayscattering/ex05.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | from pytissueoptics import * # noqa: F403 4 | 5 | TITLE = "Sphere inside a cube" 6 | 7 | DESCRIPTION = """ Propagation of a directional light source through a highly scattering medium scene composed of a 8 | sphere inside a cube. """ 9 | 10 | 11 | def exampleCode(): 12 | N = 10000 if hardwareAccelerationIsAvailable() else 200 13 | 14 | material1 = ScatteringMaterial(mu_s=20, mu_a=0.1, g=0.9, n=1.4) 15 | material2 = ScatteringMaterial(mu_s=30, mu_a=0.2, g=0.9, n=1.7) 16 | 17 | cube = Cuboid(a=3, b=3, c=3, position=Vector(0, 0, 0), material=material1, label="cube") 18 | sphere = Sphere(radius=1, order=3, position=Vector(0, 0, 0), material=material2, label="sphere", smooth=True) 19 | scene = ScatteringScene([cube, sphere]) 20 | 21 | logger = EnergyLogger(scene) 22 | source = DirectionalSource( 23 | position=Vector(0, 0, -2), direction=Vector(0, 0, 1), N=N, diameter=0.5, displaySize=0.25 24 | ) 25 | 26 | source.propagate(scene, logger) 27 | 28 | viewer = Viewer(scene, source, logger) 29 | viewer.reportStats() 30 | 31 | viewer.show2D(View2DProjectionY()) 32 | viewer.show2D(View2DProjectionY(solidLabel="sphere")) 33 | viewer.show3D() 34 | 35 | 36 | if __name__ == "__main__": 37 | exampleCode() 38 | -------------------------------------------------------------------------------- /pytissueoptics/examples/scene/env.py: -------------------------------------------------------------------------------- 1 | # We set up the environment so we do not have to install PyTissueOptics to run the examples. 2 | # By adjusting the path, we use the current version in development. 3 | 4 | import os 5 | import sys 6 | 7 | sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) 8 | -------------------------------------------------------------------------------- /pytissueoptics/examples/scene/example0.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | TITLE = "Explore different Shapes" 4 | 5 | DESCRIPTION = """ 6 | Spawning a Cuboid() of side lengths = 1,3,1 at (1,0,0) 7 | Sphere() of radius 0.5 at (0,0,0). 8 | Ellipsoid() of eccentricity 3,1,1 at (-2, 0, 0) 9 | A Mayavi Viewer allows for the display of those solids.""" 10 | 11 | 12 | def exampleCode(): 13 | from pytissueoptics.scene import Cuboid, Ellipsoid, Sphere, Vector, get3DViewer 14 | 15 | cuboid = Cuboid(a=1, b=3, c=1, position=Vector(1, 0, 0)) 16 | sphere = Sphere(radius=0.5, position=Vector(0, 0, 0)) 17 | ellipsoid = Ellipsoid(a=1.5, b=1, c=1, position=Vector(-2, 0, 0)) 18 | 19 | viewer = get3DViewer() 20 | viewer.add(cuboid, sphere, ellipsoid, representation="surface") 21 | viewer.show() 22 | 23 | 24 | if __name__ == "__main__": 25 | exampleCode() 26 | -------------------------------------------------------------------------------- /pytissueoptics/examples/scene/example1.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | TITLE = "Transforms on a Solid" 4 | 5 | DESCRIPTION = """ Translation Transform and Rotation Transform can be applied on Solids. 6 | Rotation parameters are in degrees and represent the angle of rotation around each cartesian axis. 7 | Here a cube is translated, another is rotated.""" 8 | 9 | 10 | def exampleCode(): 11 | from pytissueoptics.scene import Cuboid, Vector, get3DViewer 12 | 13 | centerCube = Cuboid(a=1, b=1, c=1, position=Vector(0, 0, 0)) 14 | topCube = Cuboid(a=1, b=1, c=1, position=Vector(0, 2, 0)) 15 | bottomCube = Cuboid(a=1, b=1, c=1, position=Vector(0, 0, 0)) 16 | 17 | bottomCube.translateTo(Vector(0, -2, 0)) 18 | centerCube.rotate(0, 30, 30) 19 | 20 | viewer = get3DViewer() 21 | viewer.add(centerCube, topCube, bottomCube, representation="surface") 22 | viewer.show() 23 | 24 | 25 | if __name__ == "__main__": 26 | exampleCode() 27 | -------------------------------------------------------------------------------- /pytissueoptics/examples/scene/example2.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | TITLE = "Stacking Cuboids" 4 | 5 | DESCRIPTION = """ It is possible to stack multiple cuboids together, which will manage the interface materials. 6 | To be stackable in a particular axis, the cuboids must have the same size in that axis.""" 7 | 8 | 9 | def exampleCode(): 10 | from pytissueoptics.scene import Cuboid, Vector, get3DViewer 11 | 12 | cuboid1 = Cuboid(1, 1, 1, position=Vector(2, 0, 0)) 13 | cuboid2 = Cuboid(2, 1, 1, position=Vector(0, 2, 0)) 14 | cuboid3 = Cuboid(3, 1, 1, position=Vector(0, 0, 2)) 15 | 16 | viewer = get3DViewer() 17 | viewer.add(cuboid1, cuboid2, cuboid3, representation="wireframe", lineWidth=5) 18 | viewer.show() 19 | 20 | cuboidStack = cuboid1.stack(cuboid2, onSurface="right") 21 | cuboidStack = cuboidStack.stack(cuboid3, onSurface="top") 22 | 23 | viewer = get3DViewer() 24 | viewer.add(cuboidStack, representation="wireframe", lineWidth=5) 25 | viewer.show() 26 | 27 | 28 | if __name__ == "__main__": 29 | exampleCode() 30 | -------------------------------------------------------------------------------- /pytissueoptics/examples/scene/example3.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | TITLE = "Load a .obj wavefront file" 4 | 5 | DESCRIPTION = """ """ 6 | 7 | 8 | def exampleCode(): 9 | from pytissueoptics.scene import get3DViewer, loadSolid 10 | 11 | solid = loadSolid("pytissueoptics/examples/scene/droid.obj") 12 | 13 | viewer = get3DViewer() 14 | viewer.add(solid, representation="surface", showNormals=True, normalLength=0.2) 15 | viewer.show() 16 | 17 | 18 | if __name__ == "__main__": 19 | exampleCode() 20 | -------------------------------------------------------------------------------- /pytissueoptics/examples/scene/example4.py: -------------------------------------------------------------------------------- 1 | import env # noqa: F401 2 | 3 | TITLE = "Lenses" 4 | 5 | DESCRIPTION = """Explore different types of lens-shaped solids.""" 6 | 7 | 8 | def exampleCode(): 9 | from pytissueoptics.scene import ( 10 | PlanoConcaveLens, 11 | PlanoConvexLens, 12 | RefractiveMaterial, 13 | SymmetricLens, 14 | ThickLens, 15 | Vector, 16 | get3DViewer, 17 | ) 18 | 19 | material = RefractiveMaterial(refractiveIndex=1.44) 20 | lens1 = ThickLens(30, 60, diameter=25.4, thickness=4, material=material, position=Vector(0, 0, 0)) 21 | lens3 = SymmetricLens(f=60, diameter=25.4, thickness=4, material=material, position=Vector(0, 0, 10)) 22 | lens2 = PlanoConvexLens(f=-60, diameter=25.4, thickness=4, material=material, position=Vector(0, 0, 20)) 23 | lens4 = PlanoConcaveLens(f=60, diameter=25.4, thickness=4, material=material, position=Vector(0, 0, 30)) 24 | 25 | viewer = get3DViewer() 26 | viewer.add(lens1, lens2, lens3, lens4, representation="surface", colormap="viridis", showNormals=False) 27 | viewer.show() 28 | 29 | 30 | if __name__ == "__main__": 31 | exampleCode() 32 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/__init__.py: -------------------------------------------------------------------------------- 1 | from .display.viewer import Direction, PointCloudStyle, Viewer, ViewGroup, Visibility 2 | from .display.views import ( 3 | View2DProjection, 4 | View2DProjectionX, 5 | View2DProjectionY, 6 | View2DProjectionZ, 7 | View2DSlice, 8 | View2DSliceX, 9 | View2DSliceY, 10 | View2DSliceZ, 11 | View2DSurface, 12 | View2DSurfaceX, 13 | View2DSurfaceY, 14 | View2DSurfaceZ, 15 | ) 16 | from .energyLogging import EnergyLogger, EnergyType 17 | from .materials import ScatteringMaterial 18 | from .opencl import CONFIG, disableOpenCL, hardwareAccelerationIsAvailable 19 | from .photon import Photon 20 | from .scatteringScene import ScatteringScene 21 | from .source import DirectionalSource, DivergentSource, IsotropicPointSource, PencilPointSource 22 | from .statistics import Stats 23 | 24 | __all__ = [ 25 | "Photon", 26 | "ScatteringMaterial", 27 | "PencilPointSource", 28 | "IsotropicPointSource", 29 | "DirectionalSource", 30 | "DivergentSource", 31 | "EnergyLogger", 32 | "EnergyType", 33 | "ScatteringScene", 34 | "Viewer", 35 | "PointCloudStyle", 36 | "Visibility", 37 | "ViewGroup", 38 | "Direction", 39 | "View2DProjection", 40 | "View2DProjectionX", 41 | "View2DProjectionY", 42 | "View2DProjectionZ", 43 | "View2DSurface", 44 | "View2DSurfaceX", 45 | "View2DSurfaceY", 46 | "View2DSurfaceZ", 47 | "View2DSlice", 48 | "View2DSliceX", 49 | "View2DSliceY", 50 | "View2DSliceZ", 51 | "samples", 52 | "Stats", 53 | "disableOpenCL", 54 | "hardwareAccelerationIsAvailable", 55 | "CONFIG", 56 | ] 57 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/display/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/display/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/display/profiles/__init__.py: -------------------------------------------------------------------------------- 1 | from .profile1D import Profile1D 2 | from .profileFactory import ProfileFactory 3 | 4 | __all__ = ["Profile1D", "ProfileFactory"] 5 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/display/profiles/profile1D.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import numpy as np 4 | from matplotlib import pyplot as plt 5 | 6 | from pytissueoptics.rayscattering.display.utils import Direction 7 | from pytissueoptics.rayscattering.energyLogging import EnergyType 8 | 9 | 10 | class Profile1D: 11 | """ 12 | Since 1D profiles are easily generated from existing 2D views or 3D data, this class is only used as a small 13 | dataclass. Only used internally Profile1DFactory when Viewer.show1D() is called. The user should only use the 14 | endpoint Viewer.show1D() which doesn't require creating a Profile1D object. 15 | """ 16 | 17 | def __init__( 18 | self, 19 | data: np.ndarray, 20 | horizontalDirection: Direction, 21 | limits: Tuple[float, float], 22 | name: str = None, 23 | energyType=EnergyType.DEPOSITION, 24 | ): 25 | self.data = data 26 | self.limits = limits 27 | self.horizontalDirection = horizontalDirection 28 | self.name = name 29 | self.energyType = energyType 30 | 31 | def show(self, logScale: bool = True): 32 | limits = sorted(self.limits) 33 | if self.horizontalDirection.isNegative: 34 | self.data = np.flip(self.data, axis=0) 35 | limits = (limits[1], limits[0]) 36 | bins = np.linspace(limits[0], limits[1], self.data.size + 1)[:-1] 37 | 38 | plt.bar(bins, self.data, width=np.diff(bins)[0], align="edge") 39 | 40 | if logScale: 41 | plt.yscale("log") 42 | plt.title(self.name) 43 | plt.xlim(*limits) 44 | plt.xlabel("xyz"[self.horizontalDirection.axis]) 45 | plt.ylabel("Deposited energy" if self.energyType == EnergyType.DEPOSITION else "Fluence rate") 46 | plt.show() 47 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/display/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .direction import DEFAULT_X_VIEW_DIRECTIONS, DEFAULT_Y_VIEW_DIRECTIONS, DEFAULT_Z_VIEW_DIRECTIONS, Direction 2 | 3 | __all__ = [ 4 | "DEFAULT_X_VIEW_DIRECTIONS", 5 | "DEFAULT_Y_VIEW_DIRECTIONS", 6 | "DEFAULT_Z_VIEW_DIRECTIONS", 7 | "Direction", 8 | ] 9 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/display/utils/direction.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class Direction(Enum): 5 | X_POS = 0 6 | Y_POS = 1 7 | Z_POS = 2 8 | X_NEG = 3 9 | Y_NEG = 4 10 | Z_NEG = 5 11 | 12 | def isSameAxisAs(self, other) -> bool: 13 | return self.value % 3 == other.value % 3 14 | 15 | @property 16 | def axis(self) -> int: 17 | """Returns an integer between 0 and 2 representing the x, y, or z axis, ignoring direction sign.""" 18 | return self.value % 3 19 | 20 | @property 21 | def isNegative(self) -> bool: 22 | return self.value >= 3 23 | 24 | @property 25 | def isPositive(self) -> bool: 26 | return not self.isNegative 27 | 28 | @property 29 | def sign(self) -> int: 30 | return 1 if self.isPositive else -1 31 | 32 | 33 | DEFAULT_X_VIEW_DIRECTIONS = (Direction.X_POS, Direction.Z_POS) 34 | DEFAULT_Y_VIEW_DIRECTIONS = (Direction.Y_NEG, Direction.Z_POS) 35 | DEFAULT_Z_VIEW_DIRECTIONS = (Direction.Z_POS, Direction.X_NEG) 36 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/display/views/__init__.py: -------------------------------------------------------------------------------- 1 | from .defaultViews import ( 2 | View2DProjection, 3 | View2DProjectionX, 4 | View2DProjectionY, 5 | View2DProjectionZ, 6 | View2DSlice, 7 | View2DSliceX, 8 | View2DSliceY, 9 | View2DSliceZ, 10 | View2DSurface, 11 | View2DSurfaceX, 12 | View2DSurfaceY, 13 | View2DSurfaceZ, 14 | ) 15 | from .view2D import View2D, ViewGroup 16 | from .viewFactory import ViewFactory 17 | 18 | __all__ = [ 19 | "View2D", 20 | "ViewGroup", 21 | "ViewFactory", 22 | "View2DProjection", 23 | "View2DProjectionX", 24 | "View2DProjectionY", 25 | "View2DProjectionZ", 26 | "View2DSlice", 27 | "View2DSliceX", 28 | "View2DSliceY", 29 | "View2DSliceZ", 30 | "View2DSurface", 31 | "View2DSurfaceX", 32 | "View2DSurfaceY", 33 | "View2DSurfaceZ", 34 | ] 35 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/energyLogging/__init__.py: -------------------------------------------------------------------------------- 1 | from .energyLogger import EnergyLogger 2 | from .energyType import EnergyType 3 | from .pointCloud import PointCloud 4 | from .pointCloudFactory import PointCloudFactory 5 | 6 | __all__ = [ 7 | "EnergyLogger", 8 | "EnergyType", 9 | "PointCloud", 10 | "PointCloudFactory", 11 | ] 12 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/energyLogging/energyType.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class EnergyType(Enum): 5 | """ 6 | Type of volumetric energy: either as the deposited energy in the solid (absorption) or as the fluence rate. 7 | """ 8 | 9 | DEPOSITION = auto() 10 | FLUENCE_RATE = auto() 11 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/energyLogging/pointCloud.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import numpy as np 4 | 5 | 6 | class PointCloud: 7 | """Each point is an array of the form (weight, x, y, z) and the resulting point cloud array is of shape (n, 4).""" 8 | 9 | def __init__(self, solidPoints: Optional[np.ndarray] = None, surfacePoints: Optional[np.ndarray] = None): 10 | self.solidPoints = solidPoints 11 | self.surfacePoints = surfacePoints 12 | 13 | @property 14 | def leavingSurfacePoints(self) -> Optional[np.ndarray]: 15 | if self.surfacePoints is None: 16 | return None 17 | return self.surfacePoints[np.where(self.surfacePoints[:, 0] >= 0)[0]] 18 | 19 | @property 20 | def enteringSurfacePoints(self) -> Optional[np.ndarray]: 21 | if self.surfacePoints is None: 22 | return None 23 | return self.surfacePoints[np.where(self.surfacePoints[:, 0] < 0)[0]] 24 | 25 | @property 26 | def enteringSurfacePointsPositive(self) -> Optional[np.ndarray]: 27 | if self.surfacePoints is None: 28 | return None 29 | points = self.enteringSurfacePoints 30 | points[:, 0] = np.negative(points[:, 0]) 31 | return points 32 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/energyLogging/pointCloudFactory.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from pytissueoptics.scene.logger import InteractionKey 4 | 5 | from .energyLogger import EnergyLogger 6 | from .energyType import EnergyType 7 | from .pointCloud import PointCloud 8 | 9 | 10 | class PointCloudFactory: 11 | def __init__(self, logger: EnergyLogger): 12 | self._logger = logger 13 | 14 | def getPointCloud( 15 | self, solidLabel: str = None, surfaceLabel: str = None, energyType=EnergyType.DEPOSITION 16 | ) -> PointCloud: 17 | if not solidLabel and not surfaceLabel: 18 | return PointCloud( 19 | self.getPointCloudOfSolids(energyType).solidPoints, self.getPointCloudOfSurfaces().surfacePoints 20 | ) 21 | points = self._logger.getDataPoints(InteractionKey(solidLabel, surfaceLabel), energyType=energyType) 22 | if surfaceLabel: 23 | return PointCloud(None, points) 24 | return PointCloud(points, None) 25 | 26 | def getPointCloudOfSolids(self, energyType=EnergyType.DEPOSITION) -> PointCloud: 27 | points = [] 28 | for solidLabel in self._logger.getStoredSolidLabels(): 29 | solidPoints = self.getPointCloud(solidLabel, energyType=energyType).solidPoints 30 | if solidPoints is not None: 31 | points.append(solidPoints) 32 | if len(points) == 0: 33 | return PointCloud(None, None) 34 | return PointCloud(np.concatenate(points, axis=0), None) 35 | 36 | def getPointCloudOfSurfaces(self, solidLabel: str = None) -> PointCloud: 37 | points = [] 38 | solidLabels = ( 39 | [solidLabel] if solidLabel else [_solidLabel for _solidLabel in self._logger.getStoredSolidLabels()] 40 | ) 41 | for _solidLabel in solidLabels: 42 | for surfaceLabel in self._logger.getStoredSurfaceLabels(_solidLabel): 43 | points.append(self.getPointCloud(_solidLabel, surfaceLabel).surfacePoints) 44 | 45 | if len(points) == 0: 46 | return PointCloud(None, None) 47 | return PointCloud(None, np.concatenate(points, axis=0)) 48 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/fresnel.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | from dataclasses import dataclass 4 | 5 | from pytissueoptics.scene.geometry import Environment, Vector 6 | from pytissueoptics.scene.intersection import Intersection 7 | 8 | 9 | @dataclass 10 | class FresnelIntersection: 11 | nextEnvironment: Environment 12 | incidencePlane: Vector 13 | isReflected: bool 14 | angleDeflection: float 15 | 16 | 17 | class FresnelIntersect: 18 | _indexIn: float 19 | _indexOut: float 20 | _thetaIn: float 21 | 22 | def compute(self, rayDirection: Vector, intersection: Intersection) -> FresnelIntersection: 23 | rayDirection = rayDirection 24 | normal = intersection.normal.copy() 25 | 26 | goingInside = rayDirection.dot(normal) < 0 27 | if goingInside: 28 | normal.multiply(-1) 29 | self._indexIn = intersection.outsideEnvironment.material.n 30 | self._indexOut = intersection.insideEnvironment.material.n 31 | nextEnvironment = intersection.insideEnvironment 32 | else: 33 | self._indexIn = intersection.insideEnvironment.material.n 34 | self._indexOut = intersection.outsideEnvironment.material.n 35 | nextEnvironment = intersection.outsideEnvironment 36 | 37 | incidencePlane = rayDirection.cross(normal) 38 | if incidencePlane.getNorm() < 1e-7: 39 | incidencePlane = rayDirection.getAnyOrthogonal() 40 | incidencePlane.normalize() 41 | 42 | dot = normal.dot(rayDirection) 43 | dot = max(min(dot, 1), -1) 44 | self._thetaIn = math.acos(dot) 45 | 46 | return self._create(nextEnvironment, incidencePlane) 47 | 48 | def _create(self, nextEnvironment, incidencePlane) -> FresnelIntersection: 49 | reflected = self._getIsReflected() 50 | if reflected: 51 | angleDeflection = self._getReflectionDeflection() 52 | else: 53 | angleDeflection = self._getRefractionDeflection() 54 | 55 | return FresnelIntersection(nextEnvironment, incidencePlane, reflected, angleDeflection) 56 | 57 | def _getIsReflected(self) -> bool: 58 | R = self._getReflectionCoefficient() 59 | if random.random() <= R: 60 | return True 61 | return False 62 | 63 | def _getReflectionCoefficient(self) -> float: 64 | """Fresnel reflection coefficient, directly from MCML code in 65 | Wang, L-H, S.L. Jacques, L-Q Zheng: 66 | MCML - Monte Carlo modeling of photon transport in multi-layered 67 | tissues. Computer Methods and Programs in Biomedicine 47:131-146, 1995. 68 | """ 69 | 70 | n1 = self._indexIn 71 | n2 = self._indexOut 72 | 73 | if n1 == n2: 74 | return 0 75 | 76 | if self._thetaIn == 0: 77 | R = (n2 - n1) / (n2 + n1) 78 | return R * R 79 | 80 | sa1 = math.sin(self._thetaIn) 81 | 82 | sa2 = sa1 * n1 / n2 83 | if sa2 >= 1: 84 | return 1 85 | 86 | ca1 = math.sqrt(1 - sa1 * sa1) 87 | ca2 = math.sqrt(1 - sa2 * sa2) 88 | 89 | cap = ca1 * ca2 - sa1 * sa2 # c+ = cc - ss. 90 | cam = ca1 * ca2 + sa1 * sa2 # c- = cc + ss. 91 | sap = sa1 * ca2 + ca1 * sa2 # s+ = sc + cs. 92 | sam = sa1 * ca2 - ca1 * sa2 # s- = sc - cs. 93 | r = 0.5 * sam * sam * (cam * cam + cap * cap) / (sap * sap * cam * cam) 94 | return r 95 | 96 | def _getReflectionDeflection(self) -> float: 97 | return 2 * self._thetaIn - math.pi 98 | 99 | def _getRefractionDeflection(self) -> float: 100 | sinThetaOut = self._indexIn * math.sin(self._thetaIn) / self._indexOut 101 | thetaOut = math.asin(sinThetaOut) 102 | return self._thetaIn - thetaOut 103 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/materials/__init__.py: -------------------------------------------------------------------------------- 1 | from .scatteringMaterial import ScatteringMaterial as ScatteringMaterial 2 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/materials/scatteringMaterial.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | 4 | import numpy as np 5 | 6 | from pytissueoptics.scene.material import RefractiveMaterial 7 | 8 | 9 | class ScatteringMaterial(RefractiveMaterial): 10 | def __init__(self, mu_s=0, mu_a=0, g=0, n=1.0): 11 | if mu_s < 0 or mu_a < 0: 12 | raise ValueError("Scattering and absorption coefficients must be positive.") 13 | if mu_s != 0 and mu_a == 0: 14 | raise ValueError("Scattering cannot occur without absorption.") 15 | self.mu_s = mu_s 16 | self.mu_a = mu_a 17 | self.mu_t = self.mu_a + self.mu_s 18 | 19 | if self.mu_t != 0: 20 | self._albedo = self.mu_a / self.mu_t 21 | else: 22 | self._albedo = 0 23 | 24 | self.g = g 25 | super().__init__(n) 26 | 27 | def getAlbedo(self): 28 | return self._albedo 29 | 30 | def getScatteringDistance(self): 31 | if self.mu_t == 0: 32 | return math.inf 33 | 34 | rnd = 0 35 | while rnd == 0: 36 | rnd = random.random() 37 | return -np.log(rnd) / self.mu_t 38 | 39 | def getScatteringAngles(self): 40 | phi = random.random() * 2 * np.pi 41 | g = self.g 42 | if g == 0: 43 | cost = 2 * random.random() - 1 44 | else: 45 | temp = (1 - g * g) / (1 - g + 2 * g * random.random()) 46 | cost = (1 + g * g - temp * temp) / (2 * g) 47 | return np.arccos(cost), phi 48 | 49 | def __hash__(self): 50 | return hash((self.mu_s, self.mu_a, self.g, self.n)) 51 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytissueoptics.rayscattering.opencl.config.CLConfig import OPENCL_AVAILABLE, WEIGHT_THRESHOLD, CLConfig, warnings 4 | from pytissueoptics.rayscattering.opencl.config.IPPTable import IPPTable 5 | 6 | OPENCL_OK = True 7 | 8 | if OPENCL_AVAILABLE: 9 | try: 10 | CONFIG = CLConfig() 11 | except Exception as e: 12 | warnings.warn("Error creating OpenCL config: " + str(e)) 13 | OPENCL_OK = False 14 | CONFIG = None 15 | else: 16 | CONFIG = None 17 | 18 | 19 | def disableOpenCL(): 20 | os.environ["PTO_DISABLE_OPENCL"] = "1" 21 | print("You can define PTO_DISABLE_OPENCL=1 in your profile to avoid this call.") 22 | 23 | 24 | def validateOpenCL() -> bool: 25 | notAvailableMessage = "Error: Hardware acceleration not available. Falling back to CPU. " 26 | 27 | if os.environ.get("PTO_DISABLE_OPENCL", "0") == "1": 28 | warnings.warn("User requested not to use OpenCL with environment variable 'PTO_DISABLE_OPENCL'=1.") 29 | return False 30 | if not OPENCL_AVAILABLE: 31 | warnings.warn(notAvailableMessage + "Please install pyopencl.") 32 | return False 33 | if not OPENCL_OK: 34 | warnings.warn(notAvailableMessage + "Please fix OpenCL error above.") 35 | return False 36 | 37 | CONFIG.validate() 38 | return True 39 | 40 | 41 | def hardwareAccelerationIsAvailable() -> bool: 42 | OPENCL_DISABLED = os.environ.get("PTO_DISABLE_OPENCL", "0") == "1" 43 | return OPENCL_AVAILABLE and OPENCL_OK and not OPENCL_DISABLED 44 | 45 | 46 | __all__ = ["IPPTable", "WEIGHT_THRESHOLD"] 47 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/__init__.py: -------------------------------------------------------------------------------- 1 | from .CLObject import BufferOf, CLObject, EmptyBuffer, RandomBuffer 2 | from .dataPointCL import DataPointCL 3 | from .materialCL import MaterialCL 4 | from .photonCL import PhotonCL 5 | from .seedCL import SeedCL 6 | from .solidCandidateCL import SolidCandidateCL 7 | from .solidCL import SolidCL, SolidCLInfo 8 | from .surfaceCL import SurfaceCL, SurfaceCLInfo 9 | from .triangleCL import TriangleCL, TriangleCLInfo 10 | from .vertexCL import VertexCL 11 | 12 | __all__ = [ 13 | "BufferOf", 14 | "CLObject", 15 | "EmptyBuffer", 16 | "RandomBuffer", 17 | "DataPointCL", 18 | "MaterialCL", 19 | "PhotonCL", 20 | "SeedCL", 21 | "SolidCandidateCL", 22 | "SolidCL", 23 | "SolidCLInfo", 24 | "SurfaceCL", 25 | "SurfaceCLInfo", 26 | "TriangleCL", 27 | "TriangleCLInfo", 28 | "VertexCL", 29 | ] 30 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/dataPointCL.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .CLObject import CLObject, cl 4 | 5 | 6 | class DataPointCL(CLObject): 7 | STRUCT_NAME = "DataPoint" 8 | 9 | STRUCT_DTYPE = np.dtype( 10 | [ 11 | ("delta_weight", cl.cltypes.float), 12 | ("x", cl.cltypes.float), 13 | ("y", cl.cltypes.float), 14 | ("z", cl.cltypes.float), 15 | ("solidID", cl.cltypes.int), 16 | ("surfaceID", cl.cltypes.int), 17 | ] 18 | ) 19 | 20 | def __init__(self, size: int): 21 | self._size = size 22 | super().__init__() 23 | 24 | def _getInitialHostBuffer(self) -> np.ndarray: 25 | return np.zeros(self._size, dtype=self._dtype) 26 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/materialCL.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.rayscattering.materials.scatteringMaterial import ScatteringMaterial 6 | 7 | from .CLObject import CLObject, cl 8 | 9 | 10 | class MaterialCL(CLObject): 11 | STRUCT_NAME = "Material" 12 | STRUCT_DTYPE = np.dtype( 13 | [ 14 | ("mu_s", cl.cltypes.float), 15 | ("mu_a", cl.cltypes.float), 16 | ("mu_t", cl.cltypes.float), 17 | ("g", cl.cltypes.float), 18 | ("n", cl.cltypes.float), 19 | ("albedo", cl.cltypes.float), 20 | ] 21 | ) 22 | 23 | def __init__(self, materials: List[ScatteringMaterial]): 24 | self._materials = materials 25 | super().__init__(buildOnce=True) 26 | 27 | def _getInitialHostBuffer(self) -> np.ndarray: 28 | buffer = np.empty(len(self._materials), dtype=self._dtype) 29 | for i, material in enumerate(self._materials): 30 | buffer[i]["mu_s"] = np.float32(material.mu_s) 31 | buffer[i]["mu_a"] = np.float32(material.mu_a) 32 | buffer[i]["mu_t"] = np.float32(material.mu_t) 33 | buffer[i]["g"] = np.float32(material.g) 34 | buffer[i]["n"] = np.float32(material.n) 35 | buffer[i]["albedo"] = np.float32(material.getAlbedo()) 36 | return buffer 37 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/photonCL.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.lib import recfunctions as rfn 3 | 4 | from .CLObject import CLObject, cl 5 | 6 | 7 | class PhotonCL(CLObject): 8 | STRUCT_NAME = "Photon" 9 | STRUCT_DTYPE = np.dtype( 10 | [ 11 | ("position", cl.cltypes.float3), 12 | ("direction", cl.cltypes.float3), 13 | ("er", cl.cltypes.float3), 14 | ("weight", cl.cltypes.float), 15 | ("materialID", cl.cltypes.uint), 16 | ("solidID", cl.cltypes.int), 17 | ] 18 | ) 19 | 20 | def __init__(self, positions: np.ndarray, directions: np.ndarray, materialID: int, solidID: int, weight=1.0): 21 | self._positions = positions 22 | self._directions = directions 23 | self._N = positions.shape[0] 24 | self._materialID = materialID 25 | self._solidID = solidID 26 | self._weight = weight 27 | 28 | super().__init__() 29 | 30 | def _getInitialHostBuffer(self) -> np.ndarray: 31 | buffer = np.zeros(self._N, dtype=self._dtype) 32 | buffer = rfn.structured_to_unstructured(buffer) 33 | buffer[:, 0:3] = self._positions 34 | buffer[:, 4:7] = self._directions 35 | buffer = rfn.unstructured_to_structured(buffer, self._dtype) 36 | buffer["weight"] = self._weight 37 | buffer["materialID"] = self._materialID 38 | buffer["solidID"] = self._solidID 39 | return buffer 40 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/seedCL.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .CLObject import CLObject, cl 4 | 5 | 6 | class SeedCL(CLObject): 7 | def __init__(self, size: int): 8 | self._size = size 9 | super().__init__(buildOnce=True) 10 | 11 | def _getInitialHostBuffer(self) -> np.ndarray: 12 | return np.random.randint(low=0, high=2**32 - 1, size=self._size, dtype=cl.cltypes.uint) 13 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/solidCL.py: -------------------------------------------------------------------------------- 1 | from typing import List, NamedTuple 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.scene.geometry import BoundingBox 6 | 7 | from .CLObject import CLObject, cl 8 | 9 | SolidCLInfo = NamedTuple("SolidInfo", [("bbox", BoundingBox), ("firstSurfaceID", int), ("lastSurfaceID", int)]) 10 | 11 | 12 | class SolidCL(CLObject): 13 | STRUCT_NAME = "Solid" 14 | STRUCT_DTYPE = np.dtype( 15 | [ 16 | ("bbox_min", cl.cltypes.float3), 17 | ("bbox_max", cl.cltypes.float3), 18 | ("firstSurfaceID", cl.cltypes.uint), 19 | ("lastSurfaceID", cl.cltypes.uint), 20 | ] 21 | ) 22 | 23 | def __init__(self, solidsInfo: List[SolidCLInfo]): 24 | self._solidsInfo = solidsInfo 25 | super().__init__(buildOnce=True) 26 | 27 | def _getInitialHostBuffer(self) -> np.ndarray: 28 | bufferSize = max(len(self._solidsInfo), 1) 29 | buffer = np.empty(bufferSize, dtype=self._dtype) 30 | for i, solidInfo in enumerate(self._solidsInfo): 31 | buffer[i]["bbox_min"][0] = np.float32(solidInfo.bbox.xMin) 32 | buffer[i]["bbox_min"][1] = np.float32(solidInfo.bbox.yMin) 33 | buffer[i]["bbox_min"][2] = np.float32(solidInfo.bbox.zMin) 34 | buffer[i]["bbox_max"][0] = np.float32(solidInfo.bbox.xMax) 35 | buffer[i]["bbox_max"][1] = np.float32(solidInfo.bbox.yMax) 36 | buffer[i]["bbox_max"][2] = np.float32(solidInfo.bbox.zMax) 37 | buffer[i]["firstSurfaceID"] = np.uint32(solidInfo.firstSurfaceID) 38 | buffer[i]["lastSurfaceID"] = np.uint32(solidInfo.lastSurfaceID) 39 | return buffer 40 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/solidCandidateCL.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from .CLObject import CLObject, cl 4 | 5 | 6 | class SolidCandidateCL(CLObject): 7 | STRUCT_NAME = "SolidCandidate" 8 | STRUCT_DTYPE = np.dtype([("distance", cl.cltypes.float), ("solidID", cl.cltypes.uint)]) 9 | 10 | def __init__(self, nWorkUnits: int, nSolids: int): 11 | self._size = nWorkUnits * nSolids 12 | super().__init__(buildOnce=True) 13 | 14 | def _getInitialHostBuffer(self) -> np.ndarray: 15 | bufferSize = max(self._size, 1) 16 | buffer = np.empty(bufferSize, dtype=self._dtype) 17 | buffer["distance"] = -1 18 | buffer["solidID"] = 0 19 | return buffer 20 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/surfaceCL.py: -------------------------------------------------------------------------------- 1 | from typing import List, NamedTuple 2 | 3 | import numpy as np 4 | 5 | from .CLObject import CLObject, cl 6 | 7 | SurfaceCLInfo = NamedTuple( 8 | "SurfaceInfo", 9 | [ 10 | ("firstPolygonID", int), 11 | ("lastPolygonID", int), 12 | ("insideMaterialID", int), 13 | ("outsideMaterialID", int), 14 | ("insideSolidID", int), 15 | ("outsideSolidID", int), 16 | ("toSmooth", bool), 17 | ], 18 | ) 19 | 20 | 21 | class SurfaceCL(CLObject): 22 | STRUCT_NAME = "Surface" 23 | STRUCT_DTYPE = np.dtype( 24 | [ 25 | ("firstPolygonID", cl.cltypes.uint), 26 | ("lastPolygonID", cl.cltypes.uint), 27 | ("insideMaterialID", cl.cltypes.uint), 28 | ("outsideMaterialID", cl.cltypes.uint), 29 | ("insideSolidID", cl.cltypes.int), 30 | ("outsideSolidID", cl.cltypes.int), 31 | ("toSmooth", cl.cltypes.uint), 32 | ] 33 | ) 34 | 35 | def __init__(self, surfacesInfo: List[SurfaceCLInfo]): 36 | self._surfacesInfo = surfacesInfo 37 | super().__init__(buildOnce=True) 38 | 39 | def _getInitialHostBuffer(self) -> np.ndarray: 40 | bufferSize = max(len(self._surfacesInfo), 1) 41 | buffer = np.empty(bufferSize, dtype=self._dtype) 42 | for i, surfaceInfo in enumerate(self._surfacesInfo): 43 | buffer[i]["firstPolygonID"] = np.uint32(surfaceInfo.firstPolygonID) 44 | buffer[i]["lastPolygonID"] = np.uint32(surfaceInfo.lastPolygonID) 45 | buffer[i]["insideMaterialID"] = np.uint32(surfaceInfo.insideMaterialID) 46 | buffer[i]["outsideMaterialID"] = np.uint32(surfaceInfo.outsideMaterialID) 47 | buffer[i]["insideSolidID"] = np.int32(surfaceInfo.insideSolidID) 48 | buffer[i]["outsideSolidID"] = np.int32(surfaceInfo.outsideSolidID) 49 | buffer[i]["toSmooth"] = np.uint32(surfaceInfo.toSmooth) 50 | return buffer 51 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/triangleCL.py: -------------------------------------------------------------------------------- 1 | from typing import List, NamedTuple 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.scene.geometry import Vector 6 | 7 | from .CLObject import CLObject, cl 8 | 9 | TriangleCLInfo = NamedTuple("TriangleInfo", [("vertexIDs", list), ("normal", Vector)]) 10 | 11 | 12 | class TriangleCL(CLObject): 13 | STRUCT_NAME = "Triangle" 14 | STRUCT_DTYPE = np.dtype([("vertexIDs", cl.cltypes.uint, 3), ("normal", cl.cltypes.float3)]) 15 | 16 | def __init__(self, trianglesInfo: List[TriangleCLInfo]): 17 | self._trianglesInfo = trianglesInfo 18 | super().__init__(buildOnce=True) 19 | 20 | def _getInitialHostBuffer(self) -> np.ndarray: 21 | bufferSize = max(len(self._trianglesInfo), 1) 22 | buffer = np.empty(bufferSize, dtype=self._dtype) 23 | for i, triangleInfo in enumerate(self._trianglesInfo): 24 | buffer[i]["vertexIDs"][0] = np.uint32(triangleInfo.vertexIDs[0]) 25 | buffer[i]["vertexIDs"][1] = np.uint32(triangleInfo.vertexIDs[1]) 26 | buffer[i]["vertexIDs"][2] = np.uint32(triangleInfo.vertexIDs[2]) 27 | buffer[i]["normal"][0] = np.float32(triangleInfo.normal.x) 28 | buffer[i]["normal"][1] = np.float32(triangleInfo.normal.y) 29 | buffer[i]["normal"][2] = np.float32(triangleInfo.normal.z) 30 | return buffer 31 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/buffers/vertexCL.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.scene.geometry import Vertex 6 | 7 | from .CLObject import CLObject, cl 8 | 9 | 10 | class VertexCL(CLObject): 11 | STRUCT_NAME = "Vertex" 12 | STRUCT_DTYPE = np.dtype([("position", cl.cltypes.float3), ("normal", cl.cltypes.float3)]) 13 | 14 | def __init__(self, vertices: List[Vertex]): 15 | self._vertices = vertices 16 | super().__init__(buildOnce=True) 17 | 18 | def _getInitialHostBuffer(self) -> np.ndarray: 19 | bufferSize = max(len(self._vertices), 1) 20 | buffer = np.empty(bufferSize, dtype=self._dtype) 21 | for i, vertex in enumerate(self._vertices): 22 | buffer[i]["position"][0] = np.float32(vertex.x) 23 | buffer[i]["position"][1] = np.float32(vertex.y) 24 | buffer[i]["position"][2] = np.float32(vertex.z) 25 | if vertex.normal is not None: 26 | buffer[i]["normal"][0] = np.float32(vertex.normal.x) 27 | buffer[i]["normal"][1] = np.float32(vertex.normal.y) 28 | buffer[i]["normal"][2] = np.float32(vertex.normal.z) 29 | return buffer 30 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/config/IPPTable.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Optional 4 | 5 | 6 | class IPPTable: 7 | TABLE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ipp.json") 8 | 9 | def __init__(self): 10 | self._assertExists() 11 | 12 | with open(self.TABLE_PATH, "r") as f: 13 | self._table = json.load(f) 14 | 15 | def getIPP(self, experimentHash: int) -> Optional[float]: 16 | if str(experimentHash) not in self._table: 17 | return None 18 | return self._table[str(experimentHash)][1] 19 | 20 | def updateIPP(self, experimentHash: int, photonCount: int, IPP: float): 21 | if str(experimentHash) not in self._table: 22 | self._table[str(experimentHash)] = [photonCount, IPP] 23 | else: 24 | oldN, oldIPP = self._table[str(experimentHash)] 25 | newN = oldN + photonCount 26 | newIPP = (oldN * oldIPP + photonCount * IPP) / newN 27 | self._table[str(experimentHash)] = [newN, round(newIPP, 3)] 28 | self._save() 29 | 30 | def _save(self): 31 | with open(self.TABLE_PATH, "w") as f: 32 | json.dump(self._table, f, indent=4) 33 | 34 | def __contains__(self, experimentHash: int): 35 | return str(experimentHash) in self._table 36 | 37 | def _assertExists(self): 38 | if not os.path.exists(self.TABLE_PATH): 39 | self._table = DEFAULT_IPP 40 | self._save() 41 | 42 | 43 | DEFAULT_IPP = { 44 | "-6887487125664597075": [40000, 144.571], 45 | "2325526903167451785": [11601000, 17.43], 46 | "-6919696058704085231": [3901000, 17.915], 47 | "-2058547611842612924": [11000, 2843.68], 48 | "3321215562015198964": [223000, 17.973], 49 | "-7640516670101814241": [21000, 2846.064], 50 | "6019286260145673908": [246000, 23.237], 51 | "7995554833896372811": [256000, 86.142], 52 | "-181984282493345035": [11812662, 41.95], 53 | } 54 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/opencl/config/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/src/random.c: -------------------------------------------------------------------------------- 1 | 2 | uint wangHash(uint seed){ 3 | seed = (seed ^ 61) ^ (seed >> 16); 4 | seed *= 9; 5 | seed = seed ^ (seed >> 4); 6 | seed *= 0x27d4eb2d; 7 | seed = seed ^ (seed >> 15); 8 | return seed; 9 | } 10 | 11 | float getRandomFloatValue(__global unsigned int *seeds, unsigned int id){ 12 | float result = 0.0f; 13 | while(result == 0.0f){ 14 | uint rnd_seed = wangHash(seeds[id]); 15 | seeds[id] = rnd_seed; 16 | result = (float)rnd_seed / (float)UINT_MAX; 17 | } 18 | return result; 19 | } 20 | 21 | // ----------------- TEST KERNELS ----------------- 22 | 23 | __kernel void fillRandomFloatBuffer(__global unsigned int *seeds, __global float *randomNumbers){ 24 | int id = get_global_id(0); 25 | randomNumbers[id] = getRandomFloatValue(seeds, id); 26 | } 27 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/src/scatteringMaterial.c: -------------------------------------------------------------------------------- 1 | struct ScatteringAngles { 2 | float phi, theta; 3 | }; 4 | 5 | typedef struct ScatteringAngles ScatteringAngles; 6 | 7 | float getScatteringDistance(float mu_t, float randomNumber){ 8 | return -log(randomNumber) / mu_t; 9 | } 10 | 11 | float getScatteringAnglePhi(float randomNumber){ 12 | float phi = 2.0f * M_PI * randomNumber; 13 | return phi; 14 | } 15 | 16 | float getScatteringAngleTheta(float g, float randomNumber){ 17 | if (g == 0){ 18 | return acos(2.0f * randomNumber - 1.0f); 19 | } 20 | else{ 21 | float temp = (1.0f - g * g) / (1 - g + 2 * g * randomNumber); 22 | return acos((1.0f + g * g - temp * temp) / (2 * g)); 23 | } 24 | } 25 | 26 | ScatteringAngles getScatteringAngles(float rndPhi, float rndTheta,__global Photon *photons, 27 | __constant Material *materials, uint photonID) 28 | { 29 | ScatteringAngles angles; 30 | float g = materials[photons[photonID].materialID].g; 31 | angles.phi = getScatteringAnglePhi(rndPhi); 32 | angles.theta = getScatteringAngleTheta(g, rndTheta); 33 | return angles; 34 | } 35 | 36 | // ----------- Test kernels ----------- 37 | 38 | __kernel void getScatteringDistanceKernel(__global float *distanceBuffer, __global float *randomNumbers, float mu_t){ 39 | uint gid = get_global_id(0); 40 | distanceBuffer[gid] = getScatteringDistance(mu_t, randomNumbers[gid]); 41 | } 42 | 43 | __kernel void getScatteringAnglePhiKernel(__global float *angleBuffer, __global float *randomNumbers){ 44 | uint gid = get_global_id(0); 45 | angleBuffer[gid] = getScatteringAnglePhi(randomNumbers[gid]); 46 | } 47 | 48 | __kernel void getScatteringAngleThetaKernel(__global float *angleBuffer, __global float *randomNumbers, float g){ 49 | uint gid = get_global_id(0); 50 | angleBuffer[gid] = getScatteringAngleTheta(g, randomNumbers[gid]); 51 | } 52 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/utils/CLParameters.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import psutil 3 | 4 | from pytissueoptics.rayscattering.opencl import CONFIG, warnings 5 | from pytissueoptics.rayscattering.opencl.buffers import DataPointCL 6 | 7 | DATAPOINT_SIZE = DataPointCL.getItemSize() 8 | 9 | 10 | class CLParameters: 11 | def __init__(self, N, AVG_IT_PER_PHOTON): 12 | nBatch = 1 / CONFIG.BATCH_LOAD_FACTOR 13 | avgPhotonsPerBatch = int(np.ceil(N / min(nBatch, CONFIG.N_WORK_UNITS))) 14 | self._maxLoggerMemory = self._calculateAverageBatchMemorySize(avgPhotonsPerBatch, AVG_IT_PER_PHOTON) 15 | self._maxPhotonsPerBatch = min(2 * avgPhotonsPerBatch, N) 16 | self._workItemAmount = CONFIG.N_WORK_UNITS 17 | 18 | self._assertEnoughRAM() 19 | 20 | def _calculateAverageBatchMemorySize(self, avgPhotonsPerBatch: int, avgInteractionsPerPhoton: float) -> int: 21 | """ 22 | Calculates the required number of bytes to allocate for each batch when expecting the given average number of 23 | interactions per photon. Note that each work unit requires a minimum of 2 available log entries to operate. 24 | """ 25 | avgInteractions = avgPhotonsPerBatch * avgInteractionsPerPhoton 26 | minInteractions = 2 * CONFIG.N_WORK_UNITS 27 | batchSize = max(avgInteractions, minInteractions) * DATAPOINT_SIZE 28 | maxSize = CONFIG.MAX_MEMORY_MB * 1024**2 29 | return min(batchSize, maxSize) 30 | 31 | @property 32 | def workItemAmount(self): 33 | return np.int32(self._workItemAmount) 34 | 35 | @property 36 | def maxPhotonsPerBatch(self): 37 | return np.int32(self._maxPhotonsPerBatch) 38 | 39 | @maxPhotonsPerBatch.setter 40 | def maxPhotonsPerBatch(self, value: int): 41 | if value < self._workItemAmount: 42 | self._workItemAmount = value 43 | self._maxPhotonsPerBatch = value 44 | 45 | @property 46 | def maxLoggableInteractions(self): 47 | return np.int32(self._maxLoggerMemory / DATAPOINT_SIZE) 48 | 49 | @property 50 | def maxLoggableInteractionsPerWorkItem(self): 51 | return np.int32(self.maxLoggableInteractions / self._workItemAmount) 52 | 53 | @property 54 | def photonsPerWorkItem(self): 55 | return np.int32(np.floor(self._maxPhotonsPerBatch / self._workItemAmount)) 56 | 57 | @property 58 | def requiredRAMBytes(self) -> float: 59 | averageNBatches = 1.4 * (1 / CONFIG.BATCH_LOAD_FACTOR) 60 | overHead = 1.15 61 | return overHead * averageNBatches * self._maxLoggerMemory 62 | 63 | def _assertEnoughRAM(self): 64 | freeSystemRAM = psutil.virtual_memory().available 65 | if self.requiredRAMBytes > 0.8 * freeSystemRAM: 66 | warnings.warn( 67 | f"WARNING: Available system RAM might not be enough for the simulation. " 68 | f"Estimated requirement: {self.requiredRAMBytes // 1024**2} MB, " 69 | f"Available: {freeSystemRAM // 1024**2} MB." 70 | ) 71 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .batchTiming import BatchTiming 2 | from .CLKeyLog import CLKeyLog 3 | from .CLParameters import CLParameters 4 | 5 | __all__ = ["BatchTiming", "CLKeyLog", "CLParameters"] 6 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/utils/batchTiming.py: -------------------------------------------------------------------------------- 1 | class BatchTiming: 2 | """ 3 | Used to record and display the progress of a batched photon propagation. 4 | """ 5 | 6 | def __init__(self, totalPhotons: int): 7 | self._photonCount = 0 8 | self._totalPhotons = totalPhotons 9 | self._batchCount = 0 10 | 11 | self._propagationTime = 0 12 | self._dataTransferTime = 0 13 | self._dataConversionTime = 0 14 | self._totalTime = 0 15 | 16 | self._title = "SIMULATION PROGRESS" 17 | self._header = ["BATCH #", "PHOTON COUNT", "SPEED (ph/ms)", "TIME ELAPSED", "TIME LEFT"] 18 | 19 | self._columnWidths = [len(value) + 3 for value in self._header] 20 | photonCountWidth = len(str(self._totalPhotons)) + 11 21 | self._columnWidths[1] = max(self._columnWidths[1], photonCountWidth) 22 | self._width = sum(self._columnWidths) + 3 * (len(self._columnWidths) - 1) 23 | 24 | self._printHeader() 25 | 26 | def recordBatch( 27 | self, 28 | photonCount: int, 29 | propagationTime: float, 30 | dataTransferTime: float, 31 | dataConversionTime: float, 32 | totalTime: float, 33 | ): 34 | """ 35 | Photon count is the number of photons that were propagated in the batch. The other times are in nanoseconds. 36 | Propagation time is the time it took to run the propagation kernel. Data transfer time is the time it took to 37 | transfer the raw 3D data from the GPU. Data conversion time is the time it took to sort and convert the 38 | interactions IDs into proper InteractionKey points. 39 | """ 40 | self._photonCount += photonCount 41 | self._propagationTime += propagationTime 42 | self._dataTransferTime += dataTransferTime 43 | self._dataConversionTime += dataConversionTime 44 | self._totalTime += totalTime 45 | self._batchCount += 1 46 | 47 | self._printProgress() 48 | if self._photonCount == self._totalPhotons: 49 | self._printFooter() 50 | 51 | def _printHeader(self): 52 | halfBanner = "=" * ((self._width - len(self._title) - 1) // 2) 53 | print(f"\n{halfBanner} {self._title} {halfBanner}") 54 | formatted_header = [f"[{title}] ".center(width) for title, width in zip(self._header, self._columnWidths)] 55 | print(":: ".join(formatted_header)) 56 | 57 | def _printProgress(self): 58 | progress = self._photonCount / self._totalPhotons 59 | speed = self._photonCount / (self._totalTime / 1e6) 60 | timeElapsed = self._totalTime / 1e9 61 | timeLeft = timeElapsed * (self._totalPhotons / self._photonCount) - timeElapsed 62 | 63 | progressString = f"{self._photonCount} ({progress * 100:.1f}%)" 64 | speedString = f"{speed:.2f}" 65 | timeElapsedString = f"{timeElapsed:.2f} s" 66 | timeLeftString = f"{timeLeft:.2f} s" 67 | 68 | formatted_values = [ 69 | f" {value} ".center(width) 70 | for value, width in zip( 71 | [self._batchCount, progressString, speedString, timeElapsedString, timeLeftString], self._columnWidths 72 | ) 73 | ] 74 | print(":: ".join(formatted_values)) 75 | 76 | def _printFooter(self): 77 | print("\nComputation splits:") 78 | splits = { 79 | "Propagation": self._propagationTime, 80 | "Data transfer": self._dataTransferTime, 81 | "Data conversion": self._dataConversionTime, 82 | } 83 | for key, value in splits.items(): 84 | print(f"\t{key}: {value / self._totalTime * 100:.1f}%") 85 | print("".join(["=" * self._width]) + "\n") 86 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/opencl/utils/optimalWorkUnits.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | 7 | from pytissueoptics import Cuboid, DirectionalSource, EnergyLogger, ScatteringMaterial, ScatteringScene, Sphere, Vector 8 | from pytissueoptics.rayscattering.opencl import CONFIG 9 | 10 | MAX_SECONDS_PER_TEST = 5 11 | 12 | 13 | def computeOptimalNWorkUnits() -> int: 14 | """ 15 | Find the optimal amount of work units for the OpenCL kernel (hardware-specific). 16 | """ 17 | 18 | AVERAGING = 3 19 | MIN_N = 128 20 | MAX_N = 32768 21 | 22 | material1 = ScatteringMaterial(mu_s=5, mu_a=0.8, g=0.9, n=1.4) 23 | material2 = ScatteringMaterial(mu_s=10, mu_a=0.8, g=0.9, n=1.7) 24 | cube = Cuboid(a=3, b=3, c=3, position=Vector(0, 0, 0), material=material1, label="Cube") 25 | sphere = Sphere(radius=1, order=2, position=Vector(0, 0, 0), material=material2, label="Sphere", smooth=True) 26 | scene = ScatteringScene([cube, sphere]) 27 | 28 | arr_workUnits, arr_speed = [], [] 29 | MIN_bits = int(math.log(MIN_N, math.sqrt(2))) + 1 30 | MAX_bits = int(math.log(MAX_N, math.sqrt(2))) + 1 31 | for i in range(MIN_bits, MAX_bits + 1): 32 | CONFIG.N_WORK_UNITS = int(np.sqrt(2) ** i) 33 | 34 | N = CONFIG.N_WORK_UNITS * 5 35 | 36 | timePerPhoton = 0 37 | totalTime = 0 38 | timedOut = False 39 | for _ in range(AVERAGING): 40 | source = DirectionalSource( 41 | position=Vector(0, 0, -2), direction=Vector(0, 0, 1), N=N, useHardwareAcceleration=True, diameter=0.5 42 | ) 43 | logger = EnergyLogger(scene) 44 | 45 | t0 = time.time() 46 | source.propagate(scene, logger=logger, showProgress=False) 47 | elapsedTime = time.time() - t0 48 | 49 | if elapsedTime > MAX_SECONDS_PER_TEST: 50 | print( 51 | f"... [{i + 1 - MIN_bits}/{MAX_bits + 1 - MIN_bits}] {CONFIG.N_WORK_UNITS} \t units : " 52 | f"Test is getting too slow on this hardware. Aborting." 53 | ) 54 | timedOut = True 55 | break 56 | 57 | totalTime += elapsedTime 58 | timePerPhoton += elapsedTime / N 59 | 60 | if timedOut: 61 | break 62 | timePerPhoton /= AVERAGING 63 | totalTime /= AVERAGING 64 | print( 65 | f"... [{i + 1 - MIN_bits}/{MAX_bits + 1 - MIN_bits}] {CONFIG.N_WORK_UNITS} \t units : {timePerPhoton:.6f} s/p [{AVERAGING}x {totalTime:.2f}s]" 66 | ) 67 | 68 | arr_workUnits.append(CONFIG.N_WORK_UNITS) 69 | arr_speed.append(timePerPhoton * 10**6) 70 | 71 | CONFIG.N_WORK_UNITS = None 72 | 73 | plt.plot(arr_workUnits, arr_speed, "o") 74 | plt.xlabel("N_WORK_UNITS") 75 | plt.ylabel("Time per photon (us)") 76 | plt.semilogy() 77 | 78 | print( 79 | f"Found an optimal N_WORK_UNITS of {arr_workUnits[np.argmin(arr_speed)]}. \nPlease analyze and close the " 80 | f"plot to continue." 81 | ) 82 | plt.show() 83 | 84 | return arr_workUnits[np.argmin(arr_speed)] 85 | 86 | 87 | if __name__ == "__main__": 88 | CONFIG.AUTO_SAVE = False 89 | optimalNWorkUnits = computeOptimalNWorkUnits() 90 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/samples/__init__.py: -------------------------------------------------------------------------------- 1 | from .infiniteTissue import InfiniteTissue 2 | from .phantomTissue import PhantomTissue 3 | 4 | __all__ = ["InfiniteTissue", "PhantomTissue"] 5 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/samples/infiniteTissue.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.rayscattering.materials import ScatteringMaterial 2 | from pytissueoptics.rayscattering.scatteringScene import ScatteringScene 3 | 4 | 5 | class InfiniteTissue(ScatteringScene): 6 | """An infinite tissue with a single material.""" 7 | 8 | def __init__(self, material: ScatteringMaterial = ScatteringMaterial(mu_s=2, mu_a=0.1, g=0.8, n=1.4)): 9 | super().__init__([], worldMaterial=material) 10 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/samples/phantomTissue.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.rayscattering.materials import ScatteringMaterial 2 | from pytissueoptics.rayscattering.scatteringScene import ScatteringScene 3 | from pytissueoptics.scene import Cuboid, Vector 4 | 5 | 6 | class PhantomTissue(ScatteringScene): 7 | """Phantom tissue consisting of 3 layers with various optical properties.""" 8 | 9 | TISSUE = [] 10 | 11 | def __init__(self, worldMaterial=ScatteringMaterial()): 12 | self._create() 13 | super().__init__(self.TISSUE, worldMaterial) 14 | 15 | def _create(self): 16 | n = [1.4, 1.7, 1.4] 17 | mu_s = [2, 3, 2] 18 | mu_a = [1, 1, 2] 19 | g = 0.8 20 | 21 | w = 3 22 | t = [0.75, 0.5, 0.75] 23 | 24 | frontLayer = Cuboid(w, w, t[0], material=ScatteringMaterial(mu_s[0], mu_a[0], g, n[0]), label="frontLayer") 25 | middleLayer = Cuboid(w, w, t[1], material=ScatteringMaterial(mu_s[1], mu_a[1], g, n[1]), label="middleLayer") 26 | backLayer = Cuboid(w, w, t[2], material=ScatteringMaterial(mu_s[2], mu_a[2], g, n[2]), label="backLayer") 27 | layerStack = backLayer.stack(middleLayer, "front").stack(frontLayer, "front") 28 | layerStack.translateTo(Vector(0, 0, sum(t) / 2)) 29 | 30 | self.TISSUE = [layerStack] 31 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/scatteringScene.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.rayscattering.materials import ScatteringMaterial 6 | from pytissueoptics.scene import Scene, Vector, get3DViewer 7 | from pytissueoptics.scene.solids import Solid 8 | from pytissueoptics.scene.viewer.displayable import Displayable 9 | 10 | 11 | class ScatteringScene(Scene): 12 | def __init__(self, solids: List[Solid], worldMaterial=ScatteringMaterial(), ignoreIntersections: bool = False): 13 | super().__init__(solids, worldMaterial=worldMaterial, ignoreIntersections=ignoreIntersections) 14 | 15 | def add(self, solid: Solid, position: Vector = None): 16 | polygonSample = solid.getPolygons()[0] 17 | if not isinstance(polygonSample.insideEnvironment.material, ScatteringMaterial): 18 | raise Exception( 19 | f"Solid '{solid.getLabel()}' has no ScatteringMaterial defined. " 20 | f"This is required for any RayScatteringScene. " 21 | ) 22 | super().add(solid, position) 23 | 24 | def show(self, source: Displayable = None, opacity=0.8, colormap="cool", **kwargs): 25 | viewer = get3DViewer() 26 | self.addToViewer(viewer, opacity=opacity, colormap=colormap, **kwargs) 27 | if source: 28 | source.addToViewer(viewer) 29 | viewer.show() 30 | 31 | def getEstimatedIPP(self, weightThreshold: float) -> float: 32 | """ 33 | Get the estimated number of interactions per photon. This gross estimation is done by assuming an infinite 34 | medium of mean scene albedo. Used as a starting point for the OpenCL kernel optimization. 35 | """ 36 | materials = self.getMaterials() 37 | averageAlbedo = sum([mat.getAlbedo() for mat in materials]) / len(materials) 38 | estimatedIPP = -np.log(weightThreshold) / averageAlbedo 39 | return estimatedIPP 40 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/statistics/__init__.py: -------------------------------------------------------------------------------- 1 | from .statistics import Stats as Stats 2 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/__init__.py: -------------------------------------------------------------------------------- 1 | SHOW_VISUAL_TESTS = False 2 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/display/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/display/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/display/testImages/profile1D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/display/testImages/profile1D.png -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/display/testImages/viewXPOS_uYPOS_vZNEG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/display/testImages/viewXPOS_uYPOS_vZNEG.png -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/display/testProfile1D.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import unittest 4 | from unittest.mock import patch 5 | 6 | import numpy as np 7 | from matplotlib import pyplot as plt 8 | 9 | from pytissueoptics import Direction 10 | from pytissueoptics.rayscattering.display.profiles import Profile1D 11 | from pytissueoptics.rayscattering.tests import SHOW_VISUAL_TESTS 12 | from pytissueoptics.scene.tests import compareVisuals 13 | 14 | TEST_IMAGES_DIR = os.path.join(os.path.dirname(__file__), "testImages") 15 | 16 | OVERWRITE_REFERENCE_IMAGES = False 17 | 18 | 19 | class TestProfile1D(unittest.TestCase): 20 | def testWhenShow_shouldPlotTheProfileWithCorrectDataAndAxis(self): 21 | profile = Profile1D(np.array([1, 2, 8, 4, 1]), Direction.X_NEG, limits=(0, 1), name="Test Profile along X_NEG") 22 | 23 | with patch("matplotlib.pyplot.show") as mockShow: 24 | profile.show() 25 | mockShow.assert_called_once() 26 | 27 | referenceImage = os.path.join(TEST_IMAGES_DIR, "profile1D.png") 28 | 29 | if OVERWRITE_REFERENCE_IMAGES: 30 | plt.savefig(referenceImage) 31 | self.skipTest("Overwriting reference image") 32 | 33 | if not SHOW_VISUAL_TESTS: 34 | self.skipTest( 35 | "Visual tests are disabled. Set rayscattering.tests.SHOW_VISUAL_TESTS to True to enable them." 36 | ) 37 | 38 | with tempfile.TemporaryDirectory() as tempdir: 39 | currentImage = os.path.join(tempdir, "test.png") 40 | plt.savefig(currentImage) 41 | plt.close() 42 | 43 | isOK = compareVisuals(referenceImage, currentImage, title="TestView2D: View2DProjectionX") 44 | if not isOK: 45 | self.fail("Visual test failed.") 46 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/energyLogging/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/energyLogging/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/energyLogging/testPointCloudFactory.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics import EnergyLogger, ScatteringScene 6 | from pytissueoptics.rayscattering.energyLogging import PointCloudFactory 7 | from pytissueoptics.scene.logger import InteractionKey 8 | 9 | 10 | class TestPointCloudFactory(unittest.TestCase): 11 | SOLID_LABEL_A = "solidA" 12 | SOLID_LABEL_B = "solidB" 13 | SOLID_A_SURFACE = "surfaceA" 14 | SOLID_B_SURFACE = "surfaceB" 15 | N_POINTS_PER_SOLID = 3 16 | N_POINTS_PER_SURFACE = 2 17 | 18 | def testWhenGetPointCloud_shouldReturnWholePointCloud(self): 19 | logger = self._createTestLogger() 20 | pointCloudFactory = PointCloudFactory(logger) 21 | pointCloud = pointCloudFactory.getPointCloud() 22 | 23 | self.assertEqual(self.N_POINTS_PER_SOLID * 2, len(pointCloud.solidPoints)) 24 | self.assertEqual(self.N_POINTS_PER_SURFACE * 2, len(pointCloud.surfacePoints)) 25 | 26 | def testWhenGetPointCloudOfSpecificSolid_shouldOnlyHavePointsInsideSolid(self): 27 | logger = self._createTestLogger() 28 | pointCloudFactory = PointCloudFactory(logger) 29 | pointCloud = pointCloudFactory.getPointCloud(self.SOLID_LABEL_A) 30 | 31 | self.assertEqual(self.N_POINTS_PER_SOLID, len(pointCloud.solidPoints)) 32 | self.assertIsNone(pointCloud.surfacePoints) 33 | 34 | def testWhenGetPointCloudOfSpecificSurface_shouldOnlyHavePointsInsideSurface(self): 35 | logger = self._createTestLogger() 36 | pointCloudFactory = PointCloudFactory(logger) 37 | pointCloud = pointCloudFactory.getPointCloud(self.SOLID_LABEL_A, self.SOLID_A_SURFACE) 38 | 39 | self.assertEqual(self.N_POINTS_PER_SURFACE, len(pointCloud.surfacePoints)) 40 | self.assertIsNone(pointCloud.solidPoints) 41 | 42 | def testWhenGetPointCloudOfSolids_shouldReturnPointCloudWithAllSolidPoints(self): 43 | logger = self._createTestLogger() 44 | pointCloudFactory = PointCloudFactory(logger) 45 | pointCloud = pointCloudFactory.getPointCloudOfSolids() 46 | 47 | self.assertEqual(self.N_POINTS_PER_SOLID * 2, len(pointCloud.solidPoints)) 48 | self.assertIsNone(pointCloud.surfacePoints) 49 | 50 | def testWhenGetPointCloudOfSurfaces_shouldReturnPointCloudWithAllSurfacePoints(self): 51 | logger = self._createTestLogger() 52 | pointCloudFactory = PointCloudFactory(logger) 53 | pointCloud = pointCloudFactory.getPointCloudOfSurfaces() 54 | 55 | self.assertEqual(self.N_POINTS_PER_SURFACE * 2, len(pointCloud.surfacePoints)) 56 | self.assertIsNone(pointCloud.solidPoints) 57 | 58 | def testGivenEmptyLogger_whenGetPointCloud_shouldReturnEmptyPointCloud(self): 59 | logger = EnergyLogger(scene=ScatteringScene([])) 60 | pointCloudFactory = PointCloudFactory(logger) 61 | pointCloud = pointCloudFactory.getPointCloud() 62 | 63 | self.assertIsNone(pointCloud.solidPoints) 64 | self.assertIsNone(pointCloud.surfacePoints) 65 | 66 | def _createTestLogger(self): 67 | logger = EnergyLogger(scene=ScatteringScene([])) 68 | solidPointsA = np.array([[0.5, 1, 0, 0], [0.5, 1, 0, 0.1], [0.5, 1, 0, -0.1]]) 69 | solidPointsB = np.array([[0.5, -1, 0, 0], [0.5, -1, 0, 0.1], [0.5, -1, 0, -0.1]]) 70 | surfacePointsA = np.array([[1, 1, 0, 0.1], [-1, 1, 0, -0.1]]) 71 | surfacePointsB = np.array([[1, -1, 0, 0.1], [-1, -1, 0, -0.1]]) 72 | logger.logDataPointArray(solidPointsA, InteractionKey(self.SOLID_LABEL_A)) 73 | logger.logDataPointArray(solidPointsB, InteractionKey(self.SOLID_LABEL_B)) 74 | logger.logDataPointArray(surfacePointsA, InteractionKey(self.SOLID_LABEL_A, self.SOLID_A_SURFACE)) 75 | logger.logDataPointArray(surfacePointsB, InteractionKey(self.SOLID_LABEL_B, self.SOLID_B_SURFACE)) 76 | return logger 77 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/energyLogging/testPointcloud.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.rayscattering.energyLogging import PointCloud 6 | 7 | 8 | class TestPointCloud(unittest.TestCase): 9 | def testWhenGetLeavingSurfacePoints_shouldReturnSurfacePointsWhereValueIsPositive(self): 10 | pointCloud = self._createTestPointCloud() 11 | 12 | leavingSurfacePoints = pointCloud.leavingSurfacePoints 13 | 14 | self.assertEqual(1, len(leavingSurfacePoints)) 15 | self.assertEqual(1, leavingSurfacePoints[0][0]) 16 | 17 | def testWhenGetEnteringSurfacePoints_shouldReturnSurfacePointsWhereValueIsNegative(self): 18 | pointCloud = self._createTestPointCloud() 19 | 20 | enteringSurfacePoints = pointCloud.enteringSurfacePoints 21 | 22 | self.assertEqual(1, len(enteringSurfacePoints)) 23 | self.assertEqual(-1, enteringSurfacePoints[0][0]) 24 | 25 | def testWhenGetEnteringSurfacePointsPositive_shouldConvertNegativeValuesToPositive(self): 26 | pointCloud = self._createTestPointCloud() 27 | 28 | enteringSurfacePointsPositive = pointCloud.enteringSurfacePointsPositive 29 | 30 | self.assertEqual(1, len(enteringSurfacePointsPositive)) 31 | self.assertEqual(1, enteringSurfacePointsPositive[0][0]) 32 | 33 | def testGivenEmptyPointCloud_whenGetLeavingSurfacePoints_shouldReturnNone(self): 34 | pointCloud = PointCloud(None, None) 35 | leavingSurfacePoints = pointCloud.leavingSurfacePoints 36 | self.assertIsNone(leavingSurfacePoints) 37 | 38 | def testGivenEmptyPointCloud_whenGetEnteringSurfacePoints_shouldReturnNone(self): 39 | pointCloud = PointCloud(None, None) 40 | enteringSurfacePoints = pointCloud.enteringSurfacePoints 41 | self.assertIsNone(enteringSurfacePoints) 42 | 43 | def testGivenEmptyPointCloud_whenGetEnteringSurfacePointsPositive_shouldReturnNone(self): 44 | pointCloud = PointCloud(None, None) 45 | enteringSurfacePointsPositive = pointCloud.enteringSurfacePointsPositive 46 | self.assertIsNone(enteringSurfacePointsPositive) 47 | 48 | @staticmethod 49 | def _createTestPointCloud(): 50 | solidPoints = np.array([[0.5, 1, 0, 0], [0.5, 1, 0, 0.1], [0.5, 1, 0, -0.1]]) 51 | surfacePoints = np.array([[1, 1, 0, 0.1], [-1, 1, 0, -0.1]]) 52 | return PointCloud(solidPoints, surfacePoints) 53 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/materials/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/materials/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/materials/testScatteringMaterial.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | from unittest.mock import patch 4 | 5 | from pytissueoptics.rayscattering.materials import ScatteringMaterial 6 | 7 | 8 | class TestScatteringMaterial(unittest.TestCase): 9 | def testShouldHaveAlbedo(self): 10 | material = ScatteringMaterial(mu_s=8, mu_a=2, g=0.9, n=1.4) 11 | expectedAlbedo = 2 / (2 + 8) 12 | self.assertEqual(expectedAlbedo, material.getAlbedo()) 13 | 14 | @patch("random.random") 15 | def testShouldHaveScatteringDistance(self, mockRandom): 16 | randomDistanceRatio = 0.5 17 | mockRandom.return_value = randomDistanceRatio 18 | material = ScatteringMaterial(mu_s=8, mu_a=2, g=0.9, n=1.4) 19 | 20 | expectedScatteringDistance = -math.log(randomDistanceRatio) / (2 + 8) 21 | self.assertEqual(expectedScatteringDistance, material.getScatteringDistance()) 22 | 23 | def testShouldHaveScatteringAngles(self): 24 | material = ScatteringMaterial(mu_s=8, mu_a=2, g=1, n=1.4) 25 | angles = material.getScatteringAngles() 26 | self.assertEqual(2, len(angles)) 27 | 28 | def testGivenFullAnisotropyFactor_shouldHaveZeroThetaScatteringAngle(self): 29 | material = ScatteringMaterial(mu_s=8, mu_a=2, g=1, n=1.4) 30 | theta, phi = material.getScatteringAngles() 31 | self.assertEqual(0, theta) 32 | 33 | @patch("random.random") 34 | def testShouldHaveThetaScatteringAngleBetween0AndPi(self, mockRandom): 35 | mockRandom.return_value = 0 36 | material = ScatteringMaterial(mu_s=8, mu_a=2, g=0, n=1.4) 37 | theta, _ = material.getScatteringAngles() 38 | self.assertEqual(math.pi, theta) 39 | 40 | mockRandom.return_value = 0.5 41 | theta, _ = material.getScatteringAngles() 42 | self.assertEqual(math.pi / 2, theta) 43 | 44 | mockRandom.return_value = 1 45 | theta, _ = material.getScatteringAngles() 46 | self.assertEqual(0, theta) 47 | 48 | @patch("random.random") 49 | def testShouldHavePhiScatteringAngleBetween0And2Pi(self, mockRandom): 50 | mockRandom.return_value = 0 51 | material = ScatteringMaterial(mu_s=8, mu_a=2, g=0, n=1.4) 52 | _, phi = material.getScatteringAngles() 53 | self.assertEqual(0, phi) 54 | 55 | mockRandom.return_value = 0.5 56 | _, phi = material.getScatteringAngles() 57 | self.assertEqual(math.pi, phi) 58 | 59 | mockRandom.return_value = 1 60 | _, phi = material.getScatteringAngles() 61 | self.assertEqual(2 * math.pi, phi) 62 | 63 | def testGivenVacuumMaterial_shouldHaveZeroAlbedo(self): 64 | vacuum = ScatteringMaterial() 65 | self.assertEqual(0, vacuum.getAlbedo()) 66 | 67 | def testGivenVacuumMaterial_shouldHaveInfiniteScatteringDistance(self): 68 | vacuum = ScatteringMaterial() 69 | self.assertEqual(math.inf, vacuum.getScatteringDistance()) 70 | 71 | def testGivenTwoMaterialsWithTheSameProperties_shouldHaveSameHash(self): 72 | material1 = ScatteringMaterial(mu_s=8, mu_a=2, g=0.9, n=1.4) 73 | material2 = ScatteringMaterial(mu_s=8, mu_a=2, g=0.9, n=1.4) 74 | self.assertEqual(hash(material1), hash(material2)) 75 | 76 | def testGivenTwoMaterialsWithDifferentProperties_shouldHaveDifferentHash(self): 77 | material1 = ScatteringMaterial(mu_s=8, mu_a=2, g=0.9, n=1.4) 78 | material2 = ScatteringMaterial(mu_s=8, mu_a=2, g=0.9, n=1.5) 79 | self.assertNotEqual(hash(material1), hash(material2)) 80 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/opencl/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/opencl/config/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/config/testIPPTable.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import tempfile 4 | import unittest 5 | 6 | from pytissueoptics.rayscattering.opencl.config.IPPTable import DEFAULT_IPP, IPPTable 7 | 8 | 9 | def tempTablePath(func): 10 | def wrapper(*args, **kwargs): 11 | with tempfile.TemporaryDirectory() as tempDir: 12 | IPPTable.TABLE_PATH = os.path.join(tempDir, "ipp.json") 13 | func(*args, **kwargs) 14 | 15 | return wrapper 16 | 17 | 18 | class TestIPPTable(unittest.TestCase): 19 | @tempTablePath 20 | def testGivenNoIPPTableFile_shouldCreateAndSetDefaultIPPValues(self): 21 | self.table = IPPTable() 22 | 23 | self.assertTrue(os.path.exists(IPPTable.TABLE_PATH)) 24 | for key, (N, IPP) in DEFAULT_IPP.items(): 25 | self.assertEqual(IPP, self.table.getIPP(int(key))) 26 | 27 | @tempTablePath 28 | def testGivenIPPTableFile_shouldLoadIPPValuesFromFile(self): 29 | with open(IPPTable.TABLE_PATH, "w") as f: 30 | f.write('{"1234": [40000, 144.571]}') 31 | 32 | self.table = IPPTable() 33 | 34 | self.assertEqual(144.571, self.table.getIPP(1234)) 35 | 36 | @tempTablePath 37 | def testWhenUpdateIPPWithNewSample_shouldUpdateIPPValueWeightedWithPhotonCount(self): 38 | expHash = 1234 39 | oldN = 1000 40 | oldIPP = 100 41 | with open(IPPTable.TABLE_PATH, "w") as f: 42 | f.write('{"%d": [%d, %f]}' % (expHash, oldN, oldIPP)) 43 | self.table = IPPTable() 44 | 45 | sampleN = 4000 46 | sampleIPP = 200 47 | self.table.updateIPP(expHash, sampleN, sampleIPP) 48 | 49 | expectedIPP = 180 50 | self.assertEqual(expectedIPP, self.table.getIPP(expHash)) 51 | 52 | @tempTablePath 53 | def testWhenUpdateIPP_shouldSaveIPPTableToFile(self): 54 | self.table = IPPTable() 55 | newHash = 1234 56 | N = 4000 57 | IPP = 200 58 | 59 | self.table.updateIPP(newHash, N, IPP) 60 | 61 | with open(IPPTable.TABLE_PATH, "r") as f: 62 | table = json.load(f) 63 | self.assertEqual(N, table[str(newHash)][0]) 64 | self.assertEqual(IPP, table[str(newHash)][1]) 65 | 66 | @tempTablePath 67 | def testWhenUpdateIPPWithNewHash_shouldContainNewHash(self): 68 | self.table = IPPTable() 69 | newHash = 1234 70 | self.assertFalse(newHash in self.table) 71 | 72 | self.table.updateIPP(newHash, 4000, 200) 73 | 74 | self.assertTrue(newHash in self.table) 75 | 76 | @tempTablePath 77 | def testWhenGetIPPWithNonExistingHash_shouldReturnNone(self): 78 | self.table = IPPTable() 79 | self.assertIsNone(self.table.getIPP(1234)) 80 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/src/CLObjects.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.lib import recfunctions as rfn 3 | 4 | from pytissueoptics import Vector 5 | from pytissueoptics.rayscattering.opencl import OPENCL_AVAILABLE 6 | 7 | if OPENCL_AVAILABLE: 8 | import pyopencl as cl 9 | else: 10 | cl = None 11 | 12 | from pytissueoptics.rayscattering.opencl.buffers import CLObject 13 | 14 | 15 | class IntersectionCL(CLObject): 16 | STRUCT_NAME = "Intersection" 17 | STRUCT_DTYPE = np.dtype( 18 | [ 19 | ("exists", cl.cltypes.uint), 20 | ("distance", cl.cltypes.float), 21 | ("position", cl.cltypes.float3), 22 | ("normal", cl.cltypes.float3), 23 | ("surfaceID", cl.cltypes.uint), 24 | ("polygonID", cl.cltypes.uint), 25 | ("distanceLeft", cl.cltypes.float), 26 | ("isSmooth", cl.cltypes.uint), 27 | ("rawNormal", cl.cltypes.float3), 28 | ] 29 | ) 30 | 31 | def __init__( 32 | self, 33 | distance: float = 10, 34 | position=Vector(0, 0, 0), 35 | normal=Vector(0, 0, 1), 36 | surfaceID=0, 37 | polygonID=0, 38 | distanceLeft: float = 0, 39 | **kwargs, 40 | ): 41 | self._distance = distance 42 | self._position = position 43 | self._normal = normal 44 | self._surfaceID = surfaceID 45 | self._polygonID = polygonID 46 | self._distanceLeft = distanceLeft 47 | 48 | super().__init__(**kwargs) 49 | 50 | def _getInitialHostBuffer(self) -> np.ndarray: 51 | buffer = np.empty(1, dtype=self._dtype) 52 | buffer[0]["exists"] = np.uint32(True) 53 | buffer[0]["distance"] = np.float32(self._distance) 54 | buffer = rfn.structured_to_unstructured(buffer) 55 | buffer[0, 2:5] = self._position.array 56 | buffer[0, 6:9] = self._normal.array 57 | buffer = rfn.unstructured_to_structured(buffer, self._dtype) 58 | buffer[0]["surfaceID"] = np.uint32(self._surfaceID) 59 | buffer[0]["polygonID"] = np.uint32(self._polygonID) 60 | buffer[0]["distanceLeft"] = np.float32(self._distanceLeft) 61 | return buffer 62 | 63 | 64 | class RayCL(CLObject): 65 | STRUCT_NAME = "Ray" 66 | STRUCT_DTYPE = np.dtype( 67 | [("origin", cl.cltypes.float4), ("direction", cl.cltypes.float4), ("length", cl.cltypes.float)] 68 | ) 69 | 70 | def __init__(self, origins: np.ndarray, directions: np.ndarray, lengths: np.ndarray): 71 | self._origins = origins 72 | self._directions = directions 73 | self._lengths = lengths 74 | self._N = origins.shape[0] 75 | 76 | super().__init__(skipDeclaration=True) 77 | 78 | def _getInitialHostBuffer(self) -> np.ndarray: 79 | buffer = np.zeros(self._N, dtype=self._dtype) 80 | buffer = rfn.structured_to_unstructured(buffer) 81 | buffer[:, 0:3] = self._origins 82 | buffer[:, 4:7] = self._directions 83 | buffer[:, 8] = self._lengths 84 | buffer = rfn.unstructured_to_structured(buffer, self._dtype) 85 | return buffer 86 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/opencl/src/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/src/testCLIntersection.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | import unittest 4 | 5 | import numpy as np 6 | 7 | from pytissueoptics import Cuboid, ScatteringMaterial, ScatteringScene, Vector 8 | from pytissueoptics.rayscattering.opencl import OPENCL_OK 9 | from pytissueoptics.rayscattering.opencl.CLPhotons import CLScene 10 | from pytissueoptics.rayscattering.opencl.CLProgram import CLProgram 11 | from pytissueoptics.rayscattering.opencl.config.CLConfig import OPENCL_SOURCE_DIR 12 | from pytissueoptics.rayscattering.tests.opencl.src.CLObjects import RayCL 13 | from pytissueoptics.rayscattering.tests.opencl.src.testCLFresnel import IntersectionCL 14 | 15 | 16 | @unittest.skipIf(not OPENCL_OK, "OpenCL device not available.") 17 | class TestCLIntersection(unittest.TestCase): 18 | def setUp(self): 19 | sourcePath = os.path.join(OPENCL_SOURCE_DIR, "intersection.c") 20 | self.program = CLProgram(sourcePath) 21 | 22 | def testRayIntersection(self): 23 | N = 1 24 | _scene = self._getTestScene() 25 | clScene = CLScene(_scene, nWorkUnits=1) 26 | 27 | rayLength = 10 28 | rayOrigin = [0, 0, -7] 29 | rays = RayCL( 30 | origins=np.full((N, 3), rayOrigin), directions=np.full((N, 3), [0, 0, 1]), lengths=np.full(N, rayLength) 31 | ) 32 | intersections = IntersectionCL(skipDeclaration=True) 33 | 34 | try: 35 | self.program.launchKernel( 36 | "findIntersections", 37 | N=N, 38 | arguments=[ 39 | rays, 40 | clScene.nSolids, 41 | clScene.solids, 42 | clScene.surfaces, 43 | clScene.triangles, 44 | clScene.vertices, 45 | clScene.solidCandidates, 46 | intersections, 47 | ], 48 | ) 49 | except Exception: 50 | traceback.print_exc(0) 51 | 52 | self.program.getData(clScene.solidCandidates) 53 | self.program.getData(intersections) 54 | 55 | solidCandidates = clScene.solidCandidates.hostBuffer 56 | self.assertEqual(solidCandidates[0]["distance"], -1) 57 | self.assertEqual(solidCandidates[0]["solidID"], 2) 58 | self.assertEqual(solidCandidates[1]["distance"], 6) 59 | self.assertEqual(solidCandidates[1]["solidID"], 1) 60 | 61 | rayIntersection = intersections.hostBuffer[0] 62 | self.assertEqual(rayIntersection["exists"], 1) 63 | hitPointZ = -1 # taken from scene 64 | self.assertEqual(rayIntersection["distance"], abs(rayOrigin[2] - hitPointZ)) 65 | self.assertEqual(rayIntersection["position"]["x"], 0) 66 | self.assertEqual(rayIntersection["position"]["y"], 0) 67 | self.assertEqual(rayIntersection["position"]["z"], hitPointZ) 68 | self.assertEqual(rayIntersection["normal"]["x"], 0) 69 | self.assertEqual(rayIntersection["normal"]["y"], 0) 70 | self.assertEqual(rayIntersection["normal"]["z"], -1) 71 | self.assertEqual(rayIntersection["distanceLeft"], rayLength - abs(rayOrigin[2] - hitPointZ)) 72 | 73 | def _getTestScene(self): 74 | material1 = ScatteringMaterial(0.1, 0.8, 0.8, 1.4) 75 | material2 = ScatteringMaterial(2, 0.8, 0.8, 1.2) 76 | material3 = ScatteringMaterial(0.5, 0.8, 0.8, 1.3) 77 | 78 | layer1 = Cuboid(a=10, b=10, c=2, position=Vector(0, 0, 0), material=material1, label="Layer 1") 79 | layer2 = Cuboid(a=10, b=10, c=2, position=Vector(0, 0, 0), material=material2, label="Layer 2") 80 | tissue = layer1.stack(layer2, "back") 81 | solid2 = Cuboid(2, 2, 2, position=Vector(10, 0, 0), material=material3) 82 | scene = ScatteringScene([tissue, solid2], worldMaterial=ScatteringMaterial()) 83 | return scene 84 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/opencl/src/testCLRandom.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import numpy as np 5 | 6 | from pytissueoptics.rayscattering.opencl import OPENCL_OK 7 | from pytissueoptics.rayscattering.opencl.buffers import EmptyBuffer, SeedCL 8 | from pytissueoptics.rayscattering.opencl.CLProgram import CLProgram 9 | from pytissueoptics.rayscattering.opencl.config.CLConfig import OPENCL_SOURCE_DIR 10 | 11 | 12 | @unittest.skipIf(not OPENCL_OK, "OpenCL device not available.") 13 | class TestCLRandom(unittest.TestCase): 14 | def setUp(self): 15 | sourcePath = os.path.join(OPENCL_SOURCE_DIR, "random.c") 16 | self.program = CLProgram(sourcePath) 17 | 18 | def testWhenGetRandomValues_shouldBeRandom(self): 19 | nWorkUnits = 10 20 | np.random.seed(0) 21 | seeds = SeedCL(nWorkUnits) 22 | valueBuffer = EmptyBuffer(nWorkUnits) 23 | 24 | self.program.launchKernel("fillRandomFloatBuffer", N=nWorkUnits, arguments=[seeds, valueBuffer]) 25 | 26 | randomValues = self.program.getData(valueBuffer) 27 | self.assertTrue(np.all(randomValues >= 0)) 28 | self.assertTrue(np.all(randomValues <= 1)) 29 | self.assertTrue(len(np.unique(randomValues)) == nWorkUnits) 30 | 31 | def testWhenGetRandomValuesASecondTime_shouldBeDifferent(self): 32 | nWorkUnits = 10 33 | np.random.seed(0) 34 | seeds = SeedCL(nWorkUnits) 35 | 36 | valueBuffer1 = EmptyBuffer(nWorkUnits) 37 | valueBuffer2 = EmptyBuffer(nWorkUnits) 38 | 39 | self.program.launchKernel("fillRandomFloatBuffer", N=nWorkUnits, arguments=[seeds, valueBuffer1]) 40 | self.program.launchKernel("fillRandomFloatBuffer", N=nWorkUnits, arguments=[seeds, valueBuffer2]) 41 | 42 | randomValues1 = self.program.getData(valueBuffer1) 43 | randomValues2 = self.program.getData(valueBuffer2) 44 | self.assertTrue(np.all(randomValues1 != randomValues2)) 45 | 46 | def testGivenSameSeed_shouldGenerateSameRandomValues(self): 47 | nWorkUnits = 10 48 | np.random.seed(0) 49 | seeds = SeedCL(nWorkUnits) 50 | valueBuffer1 = EmptyBuffer(nWorkUnits) 51 | self.program.launchKernel("fillRandomFloatBuffer", N=nWorkUnits, arguments=[seeds, valueBuffer1]) 52 | 53 | np.random.seed(0) 54 | seeds = SeedCL(nWorkUnits) 55 | valueBuffer2 = EmptyBuffer(nWorkUnits) 56 | self.program.launchKernel("fillRandomFloatBuffer", N=nWorkUnits, arguments=[seeds, valueBuffer2]) 57 | 58 | randomValues1 = self.program.getData(valueBuffer1) 59 | randomValues2 = self.program.getData(valueBuffer2) 60 | self.assertTrue(np.all(randomValues1 == randomValues2)) 61 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/statistics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/rayscattering/tests/statistics/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/testScatteringScene.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | from unittest.mock import patch 4 | 5 | from mockito import mock, verify, when 6 | 7 | from pytissueoptics.rayscattering.materials import ScatteringMaterial 8 | from pytissueoptics.rayscattering.scatteringScene import ScatteringScene 9 | from pytissueoptics.scene.solids import Cuboid 10 | from pytissueoptics.scene.viewer import Abstract3DViewer 11 | 12 | 13 | def patchMayaviShow(func): 14 | for module in ["show", "gcf", "figure", "clf", "triangular_mesh"]: 15 | func = patch("mayavi.mlab." + module)(func) 16 | return func 17 | 18 | 19 | class TestScatteringScene(unittest.TestCase): 20 | def testWhenAddingASolidWithAScatteringMaterial_shouldAddSolidToTheScene(self): 21 | scene = ScatteringScene([Cuboid(1, 1, 1, material=ScatteringMaterial())]) 22 | self.assertEqual(len(scene.solids), 1) 23 | 24 | def testWhenAddingASolidWithNoScatteringMaterialDefined_shouldRaiseException(self): 25 | with self.assertRaises(Exception): 26 | ScatteringScene([Cuboid(1, 1, 1)]) 27 | with self.assertRaises(Exception): 28 | ScatteringScene([Cuboid(1, 1, 1, material="Not a scattering material")]) 29 | 30 | def testWhenAddToViewer_shouldAddAllSolidsToViewer(self): 31 | scene = ScatteringScene([Cuboid(1, 1, 1, material=ScatteringMaterial())]) 32 | viewer = mock(Abstract3DViewer) 33 | when(viewer).add(...).thenReturn() 34 | 35 | scene.addToViewer(viewer) 36 | 37 | verify(viewer).add(*scene.solids, ...) 38 | 39 | @patchMayaviShow 40 | def testWhenShow_shouldShowInside3DViewer(self, mockShow, *args): 41 | scene = ScatteringScene([Cuboid(1, 1, 1, material=ScatteringMaterial())]) 42 | scene.show() 43 | 44 | mockShow.assert_called_once() 45 | 46 | def testShouldHaveIPPEstimationUsingMeanAlbedoInInfiniteMedium(self): 47 | material1 = ScatteringMaterial(mu_s=1, mu_a=0.7, g=0.9) 48 | material2 = ScatteringMaterial(mu_s=8, mu_a=1, g=0.9) 49 | meanAlbedo = (material1.getAlbedo() + material2.getAlbedo()) / 2 50 | weightThreshold = 0.0001 51 | 52 | scene = ScatteringScene([Cuboid(1, 1, 1, material=material2)], worldMaterial=material1) 53 | 54 | estimation = scene.getEstimatedIPP(weightThreshold) 55 | expectedEstimation = -math.log(weightThreshold) / meanAlbedo 56 | self.assertAlmostEqual(expectedEstimation, estimation, places=7) 57 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/tests/testUtils.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.rayscattering.utils import labelContained, labelsEqual, logNorm 6 | 7 | 8 | class TestLogNorm(unittest.TestCase): 9 | def testGivenConstantArray_shouldWarnAndReturnNANs(self): 10 | data = np.ones((10, 10)) 11 | with self.assertWarns(Warning): 12 | result = logNorm(data) 13 | self.assertTrue(np.isnan(result).all()) 14 | 15 | def testGivenArray_shouldReturnLogNormalizedArray(self): 16 | data = np.exp(np.arange(1, 6)) 17 | result = logNorm(data) 18 | expected = np.linspace(0, 1, 5) 19 | self.assertTrue(np.allclose(result, expected, atol=1e-5)) 20 | 21 | 22 | class TestLabelsEqual(unittest.TestCase): 23 | def testGivenEqualLabels_shouldReturnTrue(self): 24 | self.assertTrue(labelsEqual("a", "a")) 25 | 26 | def testGivenDifferentLabels_shouldReturnFalse(self): 27 | self.assertFalse(labelsEqual("a", "b")) 28 | 29 | def testGivenDifferentCaseLabels_shouldReturnTrue(self): 30 | self.assertTrue(labelsEqual("a", "A")) 31 | 32 | def testGivenOneLabelNone_shouldReturnFalse(self): 33 | self.assertFalse(labelsEqual("a", None)) 34 | self.assertFalse(labelsEqual(None, "a")) 35 | 36 | def testGivenBothLabelsNone_shouldReturnTrue(self): 37 | self.assertTrue(labelsEqual(None, None)) 38 | 39 | 40 | class TestLabelContained(unittest.TestCase): 41 | def testGivenLabelContained_shouldReturnTrue(self): 42 | self.assertTrue(labelContained("a", ["a", "b", "c"])) 43 | 44 | def testGivenLabelContainedDifferentCase_shouldReturnTrue(self): 45 | self.assertTrue(labelContained("a", ["A", "b", "c"])) 46 | 47 | def testGivenLabelNotContained_shouldReturnFalse(self): 48 | self.assertFalse(labelContained("a", ["b", "c"])) 49 | 50 | def testGivenLabelNone_shouldReturnFalse(self): 51 | self.assertFalse(labelContained(None, ["a", "b", "c"])) 52 | -------------------------------------------------------------------------------- /pytissueoptics/rayscattering/utils.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | from typing import List 3 | 4 | import numpy as np 5 | 6 | warnings.formatwarning = lambda msg, *args, **kwargs: f"{msg}\n" 7 | warn = warnings.warn 8 | 9 | 10 | def logNorm(data, eps=1e-6): 11 | data /= np.max(data) 12 | data = np.log(data + eps) 13 | data -= np.min(data) 14 | data /= np.max(data) 15 | return data 16 | 17 | 18 | def labelsEqual(label1: str, label2: str) -> bool: 19 | if label1 is None and label2 is None: 20 | return True 21 | if label1 is None or label2 is None: 22 | return False 23 | return label1.lower() == label2.lower() 24 | 25 | 26 | def labelContained(label: str, inLabels: List[str]) -> bool: 27 | if label is None: 28 | return False 29 | return any(labelsEqual(label, inLabel) for inLabel in inLabels) 30 | -------------------------------------------------------------------------------- /pytissueoptics/scene/__init__.py: -------------------------------------------------------------------------------- 1 | from .geometry import Vector 2 | from .loader import Loader, loadSolid 3 | from .logger import InteractionKey, Logger 4 | from .material import RefractiveMaterial 5 | from .scene import Scene 6 | from .solids import ( 7 | Cone, 8 | Cube, 9 | Cuboid, 10 | Cylinder, 11 | Ellipsoid, 12 | PlanoConcaveLens, 13 | PlanoConvexLens, 14 | Sphere, 15 | SymmetricLens, 16 | ThickLens, 17 | ) 18 | from .viewer import ViewPointStyle, get3DViewer 19 | 20 | __all__ = [ 21 | "Cuboid", 22 | "Cube", 23 | "Sphere", 24 | "Ellipsoid", 25 | "Cylinder", 26 | "Cone", 27 | "Vector", 28 | "Scene", 29 | "Loader", 30 | "loadSolid", 31 | "get3DViewer", 32 | "ViewPointStyle", 33 | "Logger", 34 | "InteractionKey", 35 | "RefractiveMaterial", 36 | "ThickLens", 37 | "SymmetricLens", 38 | "PlanoConvexLens", 39 | "PlanoConcaveLens", 40 | ] 41 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | from .bbox import BoundingBox 2 | from .polygon import Environment, Polygon 3 | from .quad import Quad 4 | from .rotation import Rotation 5 | from .surfaceCollection import INTERFACE_KEY, SurfaceCollection 6 | from .triangle import Triangle 7 | from .vector import Vector 8 | from .vertex import Vertex 9 | 10 | __all__ = [ 11 | "BoundingBox", 12 | "Polygon", 13 | "Quad", 14 | "Rotation", 15 | "SurfaceCollection", 16 | "Triangle", 17 | "Vector", 18 | "Vertex", 19 | "Environment", 20 | INTERFACE_KEY, 21 | ] 22 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/polygon.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import TYPE_CHECKING, List 3 | 4 | from .bbox import BoundingBox 5 | from .vector import Vector 6 | from .vertex import Vertex 7 | 8 | if TYPE_CHECKING: 9 | from pytissueoptics.scene.solids.solid import Solid 10 | 11 | WORLD_LABEL = "world" 12 | 13 | 14 | @dataclass 15 | class Environment: 16 | material: ... 17 | solid: "Solid" = None 18 | 19 | @property 20 | def solidLabel(self) -> str: 21 | if self.solid: 22 | return self.solid.getLabel() 23 | return WORLD_LABEL 24 | 25 | 26 | class Polygon: 27 | """ 28 | Abstract class for any planar polygon. 29 | 30 | Requires the vertices to be given in an anti-clockwise order 31 | for the normal to point towards the viewer. 32 | """ 33 | 34 | def __init__( 35 | self, 36 | vertices: List[Vertex], 37 | normal: Vector = None, 38 | insideEnvironment: Environment = None, 39 | outsideEnvironment: Environment = None, 40 | surfaceLabel: str = None, 41 | ): 42 | self._vertices = vertices 43 | self._normal = normal 44 | self._insideEnvironment = insideEnvironment 45 | self._outsideEnvironment = outsideEnvironment 46 | self.surfaceLabel = surfaceLabel 47 | if self._normal is None: 48 | self.resetNormal() 49 | 50 | self._bbox = None 51 | self._centroid = None 52 | self.resetCentroid() 53 | self.resetBoundingBox() 54 | self.toSmooth = False 55 | 56 | def __eq__(self, other: "Polygon"): 57 | for vertex in self._vertices: 58 | if vertex not in other.vertices: 59 | return False 60 | return True 61 | 62 | @property 63 | def normal(self) -> Vector: 64 | return self._normal 65 | 66 | @property 67 | def vertices(self) -> List[Vertex]: 68 | return self._vertices 69 | 70 | @property 71 | def insideEnvironment(self) -> Environment: 72 | return self._insideEnvironment 73 | 74 | @property 75 | def outsideEnvironment(self) -> Environment: 76 | return self._outsideEnvironment 77 | 78 | @property 79 | def bbox(self) -> BoundingBox: 80 | return self._bbox 81 | 82 | @property 83 | def centroid(self) -> Vector: 84 | return self._centroid 85 | 86 | def setOutsideEnvironment(self, environment: Environment): 87 | self._outsideEnvironment = environment 88 | 89 | def setInsideEnvironment(self, environment: Environment): 90 | self._insideEnvironment = environment 91 | 92 | def resetCentroid(self): 93 | vertexSum = Vector(0, 0, 0) 94 | for vertex in self._vertices: 95 | vertexSum.add(vertex) 96 | self._centroid = vertexSum / (len(self._vertices)) 97 | 98 | def resetBoundingBox(self): 99 | self._bbox = BoundingBox.fromVertices(self._vertices) 100 | 101 | def resetNormal(self): 102 | """ 103 | For any planar polygon, the first 3 vertices define a triangle with the same normal. 104 | We use two edges of this triangle to compute the normal (in-order cross-product). 105 | """ 106 | edgeA = self._vertices[1] - self._vertices[0] 107 | edgeB = self._vertices[2] - self._vertices[1] 108 | N = edgeA.cross(edgeB) 109 | N.normalize() 110 | self._normal = N 111 | 112 | def getCentroid(self) -> Vector: 113 | centroid = Vector(0, 0, 0) 114 | for vertex in self._vertices: 115 | centroid.add(vertex) 116 | centroid.divide(len(self._vertices)) 117 | return centroid 118 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/primitives.py: -------------------------------------------------------------------------------- 1 | TRIANGLE = "Triangle" 2 | QUAD = "Quad" 3 | POLYGON = "Polygon" 4 | DEFAULT = TRIANGLE 5 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/quad.py: -------------------------------------------------------------------------------- 1 | from .polygon import Environment, Polygon 2 | from .vector import Vector 3 | from .vertex import Vertex 4 | 5 | 6 | class Quad(Polygon): 7 | def __init__( 8 | self, 9 | v1: Vertex, 10 | v2: Vertex, 11 | v3: Vertex, 12 | v4: Vertex, 13 | insideEnvironment: Environment = None, 14 | outsideEnvironment: Environment = None, 15 | normal: Vector = None, 16 | ): 17 | super().__init__( 18 | vertices=[v1, v2, v3, v4], 19 | insideEnvironment=insideEnvironment, 20 | outsideEnvironment=outsideEnvironment, 21 | normal=normal, 22 | ) 23 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/rotation.py: -------------------------------------------------------------------------------- 1 | class Rotation: 2 | def __init__(self, xTheta: float = 0, yTheta: float = 0, zTheta: float = 0): 3 | self._xTheta = xTheta 4 | self._yTheta = yTheta 5 | self._zTheta = zTheta 6 | 7 | @property 8 | def xTheta(self): 9 | return self._xTheta 10 | 11 | @property 12 | def yTheta(self): 13 | return self._yTheta 14 | 15 | @property 16 | def zTheta(self): 17 | return self._zTheta 18 | 19 | def add(self, other: "Rotation"): 20 | self._xTheta += other.xTheta 21 | self._yTheta += other.yTheta 22 | self._zTheta += other.zTheta 23 | 24 | def __bool__(self): 25 | return self._xTheta != 0 or self._yTheta != 0 or self._zTheta != 0 26 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/triangle.py: -------------------------------------------------------------------------------- 1 | from .polygon import Environment, Polygon 2 | from .vector import Vector 3 | from .vertex import Vertex 4 | 5 | 6 | class Triangle(Polygon): 7 | def __init__( 8 | self, 9 | v1: Vertex, 10 | v2: Vertex, 11 | v3: Vertex, 12 | insideEnvironment: Environment = None, 13 | outsideEnvironment: Environment = None, 14 | normal: Vector = None, 15 | ): 16 | super().__init__( 17 | vertices=[v1, v2, v3], 18 | insideEnvironment=insideEnvironment, 19 | outsideEnvironment=outsideEnvironment, 20 | normal=normal, 21 | ) 22 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.scene.geometry import Rotation 6 | from pytissueoptics.scene.geometry.vector import Vector 7 | 8 | 9 | def rotateVerticesArray(verticesArray: np.ndarray, r: Rotation, inverse=False) -> np.ndarray: 10 | rotationMatrix = eulerRotationMatrix(r.xTheta, r.yTheta, r.zTheta, inverse=inverse) 11 | return np.einsum("ij, kj->ki", rotationMatrix, verticesArray) 12 | 13 | 14 | def eulerRotationMatrix(xTheta=0, yTheta=0, zTheta=0, inverse=False) -> np.ndarray: 15 | rotationMatrix = np.identity(3) 16 | if xTheta != 0: 17 | if inverse: 18 | rotationMatrix = np.matmul(rotationMatrix, _xRotationMatrix(-xTheta)) 19 | else: 20 | rotationMatrix = np.matmul(_xRotationMatrix(xTheta), rotationMatrix) 21 | if yTheta != 0: 22 | if inverse: 23 | rotationMatrix = np.matmul(rotationMatrix, _yRotationMatrix(-yTheta)) 24 | else: 25 | rotationMatrix = np.matmul(_yRotationMatrix(yTheta), rotationMatrix) 26 | if zTheta != 0: 27 | if inverse: 28 | rotationMatrix = np.matmul(rotationMatrix, _zRotationMatrix(-zTheta)) 29 | else: 30 | rotationMatrix = np.matmul(_zRotationMatrix(zTheta), rotationMatrix) 31 | return rotationMatrix 32 | 33 | 34 | def _zRotationMatrix(theta) -> np.ndarray: 35 | cosTheta = np.cos(theta * np.pi / 180) 36 | sinTheta = np.sin(theta * np.pi / 180) 37 | return np.asarray([[cosTheta, -sinTheta, 0], [sinTheta, cosTheta, 0], [0, 0, 1]]) 38 | 39 | 40 | def _yRotationMatrix(theta) -> np.ndarray: 41 | cosTheta = np.cos(theta * np.pi / 180) 42 | sinTheta = np.sin(theta * np.pi / 180) 43 | return np.asarray([[cosTheta, 0, sinTheta], [0, 1, 0], [-sinTheta, 0, cosTheta]]) 44 | 45 | 46 | def _xRotationMatrix(theta) -> np.ndarray: 47 | cosTheta = np.cos(theta * np.pi / 180) 48 | sinTheta = np.sin(theta * np.pi / 180) 49 | return np.asarray([[1, 0, 0], [0, cosTheta, -sinTheta], [0, sinTheta, cosTheta]]) 50 | 51 | 52 | def getAxisAngleBetween(fromDirection: Vector, toDirection: Vector) -> Tuple[Vector, float]: 53 | fromDirection.normalize() 54 | toDirection.normalize() 55 | dot = fromDirection.dot(toDirection) 56 | dot = max(min(dot, 1), -1) 57 | angle = np.arccos(dot) 58 | axis = fromDirection.cross(toDirection) 59 | axis.normalize() 60 | axis = axis.array 61 | return Vector(*axis), angle 62 | -------------------------------------------------------------------------------- /pytissueoptics/scene/geometry/vertex.py: -------------------------------------------------------------------------------- 1 | from .vector import Vector 2 | 3 | 4 | class Vertex(Vector): 5 | def __init__(self, x: float = 0, y: float = 0, z: float = 0): 6 | super().__init__(x, y, z) 7 | self.normal = None 8 | -------------------------------------------------------------------------------- /pytissueoptics/scene/intersection/__init__.py: -------------------------------------------------------------------------------- 1 | from .intersectionFinder import FastIntersectionFinder, Intersection, SimpleIntersectionFinder 2 | from .ray import Ray 3 | from .raySource import RaySource, UniformRaySource 4 | 5 | __all__ = [ 6 | "Intersection", 7 | "FastIntersectionFinder", 8 | "SimpleIntersectionFinder", 9 | "Ray", 10 | "RaySource", 11 | "UniformRaySource", 12 | ] 13 | -------------------------------------------------------------------------------- /pytissueoptics/scene/intersection/ray.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.scene.geometry import Vector 2 | 3 | 4 | class Ray: 5 | def __init__(self, origin: Vector, direction: Vector, length: float = None): 6 | self._origin = origin 7 | self._direction = direction 8 | self._direction.normalize() 9 | self._length = length 10 | 11 | @property 12 | def origin(self): 13 | return self._origin 14 | 15 | @property 16 | def direction(self): 17 | return self._direction 18 | 19 | @property 20 | def length(self): 21 | return self._length 22 | -------------------------------------------------------------------------------- /pytissueoptics/scene/intersection/raySource.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import List 3 | 4 | import numpy as np 5 | 6 | from pytissueoptics.scene import Vector 7 | from pytissueoptics.scene.intersection import Ray 8 | 9 | 10 | class RaySource: 11 | def __init__(self): 12 | self._rays: List[Ray] = [] 13 | self._createRays() 14 | 15 | @property 16 | def rays(self): 17 | return self._rays 18 | 19 | def _createRays(self): 20 | raise NotImplementedError 21 | 22 | 23 | class UniformRaySource(RaySource): 24 | def __init__( 25 | self, position: Vector, direction: Vector, xTheta: float, yTheta: float, xResolution: int, yResolution: int 26 | ): 27 | self._position = position 28 | self._direction = direction 29 | self._direction.normalize() 30 | self._theta = (xTheta * np.pi / 180, yTheta * np.pi / 180) 31 | self._resolution = (xResolution, yResolution) 32 | super(UniformRaySource, self).__init__() 33 | 34 | def _createRays(self): 35 | for yTheta in self._getYThetaRange(): 36 | for xTheta in self._getXThetaRange(): 37 | self._createRayAt(xTheta, yTheta) 38 | 39 | def _getXThetaRange(self) -> List[float]: 40 | return np.linspace(0, self._theta[0], self._resolution[0]) - self._theta[0] / 2 41 | 42 | def _getYThetaRange(self) -> List[float]: 43 | return np.linspace(0, self._theta[1], self._resolution[1]) - self._theta[1] / 2 44 | 45 | def _createRayAt(self, xTheta: float, yTheta: float): 46 | self._rays.append(Ray(self._position, self._getRayDirectionAt(xTheta, yTheta))) 47 | 48 | def _getRayDirectionAt(self, xTheta, yTheta) -> Vector: 49 | """ 50 | Returns the (normalized) direction of the ray at the given x and y angle difference 51 | from the source orientation. 52 | 53 | xTheta is defined as the angle from -Z axis towards +X axis. 54 | yTheta is defined as the angle from -Z axis towards +Y axis. 55 | """ 56 | xTheta += math.atan(self._direction.x / self._direction.z) 57 | yTheta += math.asin(self._direction.y) 58 | rayDirection = Vector( 59 | -math.sin(xTheta) * math.cos(yTheta), math.sin(yTheta), -math.cos(xTheta) * math.cos(yTheta) 60 | ) 61 | return rayDirection 62 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/__init__.py: -------------------------------------------------------------------------------- 1 | from .loader import Loader 2 | from .loadSolid import loadSolid 3 | 4 | __all__ = ["Loader", "loadSolid"] 5 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/loadSolid.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.scene.geometry import Vector 2 | from pytissueoptics.scene.loader import Loader 3 | from pytissueoptics.scene.solids import Solid, SolidFactory 4 | 5 | 6 | def loadSolid( 7 | filepath: str, 8 | position: Vector = Vector(0, 0, 0), 9 | material=None, 10 | label: str = "solidFromFile", 11 | smooth=False, 12 | showProgress: bool = True, 13 | ) -> Solid: 14 | solids = Loader().load(filepath, showProgress=showProgress) 15 | return SolidFactory().fromSolids(solids, position, material, label, smooth) 16 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/loader.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from typing import List 3 | 4 | from pytissueoptics.scene.geometry import SurfaceCollection, Triangle, Vector, Vertex, primitives 5 | from pytissueoptics.scene.loader.parsers import OBJParser 6 | from pytissueoptics.scene.loader.parsers.parsedSurface import ParsedSurface 7 | from pytissueoptics.scene.solids import Solid 8 | from pytissueoptics.scene.utils.progressBar import progressBar 9 | 10 | 11 | class Loader: 12 | """ 13 | Base class to manage the conversion between files and Scene() or Solid() from 14 | various types of files. 15 | """ 16 | 17 | def __init__(self): 18 | self._filepath: str = "" 19 | self._fileExtension: str = "" 20 | self._parser = None 21 | 22 | def load(self, filepath: str, showProgress: bool = True) -> List[Solid]: 23 | self._filepath = filepath 24 | self._fileExtension = self._getFileExtension() 25 | self._selectParser(showProgress) 26 | return self._convert(showProgress) 27 | 28 | def _getFileExtension(self) -> str: 29 | return pathlib.Path(self._filepath).suffix 30 | 31 | def _selectParser(self, showProgress: bool = True): 32 | ext = self._fileExtension 33 | if ext == ".obj": 34 | self._parser = OBJParser(self._filepath, showProgress) 35 | else: 36 | raise NotImplementedError("This format is not supported.") 37 | 38 | def _convert(self, showProgress: bool = True) -> List[Solid]: 39 | vertices = [] 40 | for vertex in self._parser.vertices: 41 | vertices.append(Vertex(*vertex)) 42 | 43 | totalProgressBarLength = 0 44 | for objectName, _object in self._parser.objects.items(): 45 | totalProgressBarLength += len(_object.surfaces.items()) 46 | pbar = progressBar( 47 | total=totalProgressBarLength, 48 | desc="Converting File '{}'".format(self._filepath.split("/")[-1]), 49 | unit="surfaces", 50 | disable=not showProgress, 51 | ) 52 | 53 | solids = [] 54 | for objectName, _object in self._parser.objects.items(): 55 | surfaces = SurfaceCollection() 56 | for surfaceLabel, surface in _object.surfaces.items(): 57 | surfaces.add(surfaceLabel, self._convertSurfaceToTriangles(surface, vertices)) 58 | pbar.update(1) 59 | solids.append( 60 | Solid( 61 | position=Vector(0, 0, 0), 62 | vertices=vertices, 63 | surfaces=surfaces, 64 | primitive=primitives.POLYGON, 65 | label=objectName, 66 | ) 67 | ) 68 | 69 | pbar.close() 70 | return solids 71 | 72 | @staticmethod 73 | def _convertSurfaceToTriangles(surface: ParsedSurface, vertices: List[Vertex]) -> List[Triangle]: 74 | """Converting to triangles only since loaded polygons are often not planar.""" 75 | triangles = [] 76 | for polygonIndices in surface.polygons: 77 | polygonVertices = [vertices[i] for i in polygonIndices] 78 | for i in range(len(polygonVertices) - 2): 79 | triangles.append(Triangle(polygonVertices[0], polygonVertices[i + 1], polygonVertices[i + 2])) 80 | return triangles 81 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .obj import OBJParser as OBJParser 2 | from .parser import Parser as Parser 3 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/parsers/obj/__init__.py: -------------------------------------------------------------------------------- 1 | from .objParser import OBJParser as OBJParser 2 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/parsers/parsedObject.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict 3 | 4 | from pytissueoptics.scene.loader.parsers.parsedSurface import ParsedSurface 5 | 6 | 7 | @dataclass 8 | class ParsedObject: 9 | material: str 10 | surfaces: Dict[str, ParsedSurface] 11 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/parsers/parsedSurface.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | 5 | @dataclass 6 | class ParsedSurface: 7 | polygons: List[List[int]] 8 | normals: List[List[int]] 9 | texCoords: List[List[int]] 10 | -------------------------------------------------------------------------------- /pytissueoptics/scene/loader/parsers/parser.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List 2 | 3 | from pytissueoptics.scene.loader.parsers.parsedObject import ParsedObject 4 | 5 | 6 | class Parser: 7 | """ 8 | Parser base class to parse a file and extract relevant information into a structure. 9 | Each parser will manage one file type and convey the information into a standard format 10 | that will then be converted to the Scene language via the Loader class. The parser 11 | is the step in-between reading the file and converting it into the correct python objects 12 | and its dissociation from the Loader is mainly for clarity. 13 | 14 | The standard interface for the parser _object will look like this: 15 | self._object: Dict[str:, ParsedObject:] 16 | All the object will have their name in the dictionary and pointing to a parsedObject dataclass 17 | ParsedObject dataclass will contain a Material and surfaces:Dict[str, ParsedSurface] 18 | ParsedSurfaces will contain the polygon, normal and textureCoordinate indices. 19 | 20 | Other components, such as the vertices, dont need to be stored in the dictionary 21 | The reason is that it is a global entity that has no complexity and is always needed 22 | for the conversion later down the line. 23 | """ 24 | 25 | NO_OBJECT = "noObject" 26 | NO_SURFACE = "noSurface" 27 | 28 | def __init__(self, filepath: str, showProgress: bool = True): 29 | self._filepath = filepath 30 | self._objects: Dict[str, ParsedObject] = {} 31 | self._vertices: List[List[float]] = [] 32 | self._normals: List[List[float]] = [] 33 | self._textureCoords: List[List[float]] = [] 34 | self._currentObjectName: str = self.NO_OBJECT 35 | self._currentSurfaceLabel: str = self.NO_SURFACE 36 | self._checkFileExtension() 37 | self._parse(showProgress) 38 | 39 | def _checkFileExtension(self): 40 | raise NotImplementedError 41 | 42 | def _parse(self, showProgress: bool = True): 43 | raise NotImplementedError 44 | 45 | def _resetSurfaceLabel(self): 46 | self._currentSurfaceLabel = self.NO_SURFACE 47 | 48 | def _validateSurfaceLabel(self): 49 | if self._currentSurfaceLabel not in self._objects[self._currentObjectName].surfaces: 50 | return 51 | idx = 2 52 | while f"{self._currentSurfaceLabel}_{idx}" in self._objects[self._currentObjectName].surfaces: 53 | idx += 1 54 | self._currentSurfaceLabel = f"{self._currentSurfaceLabel}_{idx}" 55 | 56 | @property 57 | def vertices(self): 58 | return self._vertices 59 | 60 | @property 61 | def textureCoords(self): 62 | return self._textureCoords 63 | 64 | @property 65 | def normals(self): 66 | return self._normals 67 | 68 | @property 69 | def objects(self): 70 | return self._objects 71 | -------------------------------------------------------------------------------- /pytissueoptics/scene/logger/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import InteractionKey, Logger 2 | 3 | __all__ = ["InteractionKey", "Logger"] 4 | -------------------------------------------------------------------------------- /pytissueoptics/scene/logger/listArrayContainer.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from typing import Optional 3 | 4 | import numpy as np 5 | 6 | 7 | class ListArrayContainer: 8 | def __init__(self): 9 | self._list = None 10 | self._array = None 11 | 12 | def __len__(self): 13 | length = 0 14 | if self._list is not None: 15 | length += len(self._list) 16 | if self._array is not None: 17 | length += self._array.shape[0] 18 | return length 19 | 20 | @property 21 | def _width(self): 22 | if self._list is not None: 23 | return len(self._list[0]) 24 | elif self._array is not None: 25 | return self._array.shape[1] 26 | else: 27 | return None 28 | 29 | def _assertSameWidth(self, data): 30 | if self._width is None: 31 | return 32 | if isinstance(data, list): 33 | assert len(data) == self._width 34 | elif isinstance(data, np.ndarray): 35 | assert data.shape[1] == self._width 36 | 37 | def append(self, item): 38 | self._assertSameWidth(item) 39 | if isinstance(item, list): 40 | if self._list is None: 41 | self._list = [copy.deepcopy(item)] 42 | else: 43 | self._list.append(item) 44 | elif isinstance(item, np.ndarray): 45 | if self._array is None: 46 | self._array = copy.deepcopy(item) 47 | else: 48 | self._array = np.concatenate((self._array, item), axis=0) 49 | 50 | def extend(self, other: "ListArrayContainer"): 51 | if self._list is None: 52 | self._list = copy.deepcopy(other._list) 53 | elif other._list is not None: 54 | self._list.extend(other._list) 55 | if other._array is not None: 56 | self.append(other._array) 57 | 58 | def getData(self) -> Optional[np.ndarray]: 59 | if self._list is None and self._array is None: 60 | return None 61 | if self._list is None: 62 | return self._array 63 | if self._array is None: 64 | return np.array(self._list) 65 | mergedData = np.concatenate((np.array(self._list), self._array), axis=0) 66 | return mergedData 67 | -------------------------------------------------------------------------------- /pytissueoptics/scene/material.py: -------------------------------------------------------------------------------- 1 | class RefractiveMaterial: 2 | def __init__(self, refractiveIndex): 3 | self.n = refractiveIndex 4 | -------------------------------------------------------------------------------- /pytissueoptics/scene/scene/__init__.py: -------------------------------------------------------------------------------- 1 | from .scene import Scene as Scene 2 | -------------------------------------------------------------------------------- /pytissueoptics/scene/shader/__init__.py: -------------------------------------------------------------------------------- 1 | from .utils import getSmoothNormal as getSmoothNormal 2 | -------------------------------------------------------------------------------- /pytissueoptics/scene/shader/utils.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pytissueoptics.scene.geometry import Polygon, Vector, Vertex 4 | 5 | 6 | def getSmoothNormal(polygon: Polygon, position: Vector) -> Vector: 7 | """If the intersecting polygon was prepared for smoothing (i.e. it has vertex 8 | normals), we interpolate the normal at the intersection point using the normal 9 | of all its vertices. The interpolation is done using the general barycentric 10 | coordinates algorithm from http://www.geometry.caltech.edu/pubs/MHBD02.pdfv.""" 11 | if not polygon.toSmooth: 12 | return polygon.normal 13 | 14 | # Check edge case where the intersection is directly on a vertex, in which case we just return the vertex normal. 15 | for vertex in polygon.vertices: 16 | if (position - vertex).getNorm() < 1e-6: 17 | return vertex.normal 18 | 19 | weights = _getBarycentricWeights(polygon.vertices, position) 20 | 21 | smoothNormal = Vector(0, 0, 0) 22 | for weight, vertex in zip(weights, polygon.vertices): 23 | smoothNormal += vertex.normal * weight 24 | smoothNormal.normalize() 25 | 26 | return smoothNormal 27 | 28 | 29 | def _getBarycentricWeights(vertices: List[Vertex], position: Vector) -> List[float]: 30 | weights = [] 31 | n = len(vertices) 32 | for i, vertex in enumerate(vertices): 33 | prevVertex = vertices[(i - 1) % n] 34 | nextVertex = vertices[(i + 1) % n] 35 | w = (_cotangent(position, vertex, prevVertex) + _cotangent(position, vertex, nextVertex)) / ( 36 | position - vertex 37 | ).getNorm() ** 2 38 | weights.append(w) 39 | return [w / sum(weights) for w in weights] 40 | 41 | 42 | def _cotangent(a: Vector, b: Vector, c: Vector) -> float: 43 | """Cotangent of triangle abc at vertex b.""" 44 | ba = a - b 45 | bc = c - b 46 | norm = ba.cross(bc).getNorm() 47 | if norm < 1e-6: 48 | norm = 1e-6 49 | return bc.dot(ba) / norm 50 | -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/__init__.py: -------------------------------------------------------------------------------- 1 | from .cone import Cone 2 | from .cube import Cube 3 | from .cuboid import Cuboid 4 | from .cylinder import Cylinder 5 | from .ellipsoid import Ellipsoid 6 | from .lens import PlanoConcaveLens, PlanoConvexLens, SymmetricLens, ThickLens 7 | from .solid import Solid 8 | from .solidFactory import SolidFactory 9 | from .sphere import Sphere 10 | 11 | __all__ = [ 12 | "Solid", 13 | "SolidFactory", 14 | "Cuboid", 15 | "Sphere", 16 | "Cube", 17 | "Cylinder", 18 | "Cone", 19 | "Ellipsoid", 20 | "PlanoConvexLens", 21 | "PlanoConcaveLens", 22 | "ThickLens", 23 | "SymmetricLens", 24 | ] 25 | -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/cone.py: -------------------------------------------------------------------------------- 1 | from ..geometry import Vector, primitives 2 | from .cylinder import Cylinder 3 | 4 | 5 | class Cone(Cylinder): 6 | def __init__( 7 | self, 8 | radius: float = 1, 9 | length: float = 1, 10 | u: int = 32, 11 | v: int = 3, 12 | s: int = 1, 13 | position: Vector = Vector(0, 0, 0), 14 | material=None, 15 | primitive: str = primitives.DEFAULT, 16 | label: str = "Cone", 17 | ): 18 | super().__init__(radius, length, u, v, s, position, material, primitive, label) 19 | 20 | def _getShrinkFactor(self, heightAlong: float) -> float: 21 | return (self._length - heightAlong) / self._length 22 | 23 | def _computeQuadMesh(self): 24 | raise NotImplementedError("Quad mesh not implemented for Cylinder") 25 | -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/cube.py: -------------------------------------------------------------------------------- 1 | from ..geometry import Vector, primitives 2 | from .cuboid import Cuboid 3 | 4 | 5 | class Cube(Cuboid): 6 | def __init__( 7 | self, 8 | edge: float, 9 | position: Vector = Vector(0, 0, 0), 10 | material=None, 11 | label: str = "cube", 12 | primitive: str = primitives.DEFAULT, 13 | ): 14 | super().__init__(a=edge, b=edge, c=edge, position=position, material=material, label=label, primitive=primitive) 15 | -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/solidFactory.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pytissueoptics.scene.geometry import SurfaceCollection, Vector, Vertex, primitives 4 | from pytissueoptics.scene.solids import Solid 5 | 6 | 7 | class SolidFactory: 8 | _vertices: List[Vertex] 9 | _surfaces: SurfaceCollection 10 | 11 | def fromSolids( 12 | self, 13 | solids: List[Solid], 14 | position: Vector = Vector(0, 0, 0), 15 | material=None, 16 | label: str = "solidGroup", 17 | smooth=False, 18 | ) -> Solid: 19 | self._vertices = [] 20 | self._surfaces = SurfaceCollection() 21 | self._validateLabels(solids) 22 | self._fillSurfacesAndVertices(solids) 23 | 24 | solid = Solid( 25 | vertices=self._vertices, 26 | surfaces=self._surfaces, 27 | material=material, 28 | label=label, 29 | primitive=primitives.POLYGON, 30 | smooth=smooth, 31 | labelOverride=False, 32 | ) 33 | solid._position = self._getCentroid(solids) 34 | solid.translateTo(position) 35 | return solid 36 | 37 | @staticmethod 38 | def _getCentroid(solids) -> Vector: 39 | vertexSum = Vector(0, 0, 0) 40 | for solid in solids: 41 | vertexSum += solid.position 42 | return vertexSum / (len(solids)) 43 | 44 | def _validateLabels(self, solids): 45 | seenSolidLabels = set() 46 | for solid in solids: 47 | i = 2 48 | baseLabel = solid.getLabel() 49 | while solid.getLabel() in seenSolidLabels: 50 | solid.setLabel(f"{baseLabel}{i}") 51 | i += 1 52 | seenSolidLabels.add(solid.getLabel()) 53 | 54 | def _fillSurfacesAndVertices(self, solids): 55 | for solid in solids: 56 | self._addSolidVertices(solid) 57 | for surfaceLabel in solid.surfaceLabels: 58 | self._surfaces.add(surfaceLabel, solid.getPolygons(surfaceLabel)) 59 | 60 | def _addSolidVertices(self, solid): 61 | currentVerticesIDs = {id(vertex) for vertex in self._vertices} 62 | for vertex in solid.getVertices(): 63 | if id(vertex) not in currentVerticesIDs: 64 | self._vertices.append(vertex) 65 | 66 | def _validateSolidLabel(self, solidLabel: str) -> str: 67 | surfaceLabelsSolidNames = [surfaceLabel.split("_")[0] for surfaceLabel in self._surfaces.surfaceLabels] 68 | if solidLabel not in surfaceLabelsSolidNames: 69 | return solidLabel 70 | idx = 2 71 | solidLabelsWithNumbers = [ 72 | "_".join(surfaceLabel.split("_")[0:2]) for surfaceLabel in self._surfaces.surfaceLabels 73 | ] 74 | while f"{solidLabel}_{idx}" in solidLabelsWithNumbers: 75 | idx += 1 76 | return f"{solidLabel}_{idx}" 77 | -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/sphere.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.scene.geometry import Vector, primitives 2 | from pytissueoptics.scene.solids import Ellipsoid 3 | 4 | 5 | class Sphere(Ellipsoid): 6 | """ 7 | The Sphere is the 3D analog to the circle. Meshing a sphere requires an infinite number of vertices. 8 | The position refers to the vector from global origin to its centroid. 9 | The radius of the sphere will determine the outermost distance from its centroid. 10 | 11 | This class offers two possible methods to generate the sphere mesh. 12 | - With Quads: Specify the number of separation lines on the vertical axis and the horizontal axis of the sphere. 13 | - With Triangle: Specify the order of splitting. This will generate what is known as an IcoSphere. 14 | """ 15 | 16 | def __init__( 17 | self, 18 | radius: float = 1.0, 19 | order: int = 3, 20 | position: Vector = Vector(0, 0, 0), 21 | material=None, 22 | label: str = "sphere", 23 | primitive: str = primitives.DEFAULT, 24 | smooth: bool = True, 25 | ): 26 | self._radius = radius 27 | 28 | super().__init__( 29 | a=radius, 30 | b=radius, 31 | c=radius, 32 | order=order, 33 | position=position, 34 | material=material, 35 | label=label, 36 | primitive=primitive, 37 | smooth=smooth, 38 | ) 39 | 40 | @property 41 | def radius(self): 42 | return self._radius 43 | 44 | def _computeQuadMesh(self): 45 | raise NotImplementedError 46 | 47 | def contains(self, *vertices: Vector) -> bool: 48 | """Only returns true if all vertices are inside the minimum radius of the sphere 49 | (more restrictive with low order spheres).""" 50 | minRadius = self._getMinimumRadius() 51 | for vertex in vertices: 52 | relativeVertex = vertex - self.position 53 | if relativeVertex.getNorm() >= minRadius: 54 | return False 55 | return True 56 | 57 | def _getMinimumRadius(self) -> float: 58 | return (1 - self._getRadiusError()) * self._radius 59 | 60 | def _radiusTowards(self, vertex) -> float: 61 | return self.radius 62 | -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/stack/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/solids/stack/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/solids/stack/stackResult.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, List 3 | 4 | from pytissueoptics.scene.geometry import SurfaceCollection, Vector, Vertex 5 | 6 | 7 | @dataclass 8 | class StackResult: 9 | """Domain DTO to help creation of cuboid stacks.""" 10 | 11 | shape: List[float] 12 | position: Vector 13 | vertices: List[Vertex] 14 | surfaces: SurfaceCollection 15 | primitive: str 16 | layerLabels: Dict[str, List[str]] 17 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot as plt 2 | 3 | SHOW_VISUAL_TESTS = False 4 | 5 | 6 | def compareVisuals(expectedImagePath, currentImagePath, title: str) -> bool: 7 | visualIsOK = True 8 | 9 | def _visualOK(event, OK: bool = True): 10 | plt.close() 11 | nonlocal visualIsOK 12 | visualIsOK = OK 13 | 14 | fig, ax = plt.subplots(1, 2) 15 | ax[0].imshow(plt.imread(expectedImagePath)) 16 | ax[1].imshow(plt.imread(currentImagePath)) 17 | ax[0].set_title("Expected view") 18 | ax[1].set_title("Current view") 19 | axOK = plt.axes([0.7, 0.05, 0.1, 0.075]) 20 | axFAIL = plt.axes([0.81, 0.05, 0.1, 0.075]) 21 | btnOK = plt.Button(axOK, "OK") 22 | btnFAIL = plt.Button(axFAIL, "FAIL") 23 | btnOK.on_clicked(_visualOK) 24 | btnFAIL.on_clicked(lambda event: _visualOK(event, False)) 25 | plt.suptitle(title) 26 | plt.show() 27 | return visualIsOK 28 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/geometry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/geometry/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/geometry/testPolygon.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene.geometry import Polygon, Vector, Vertex 4 | 5 | 6 | class TestPolygon(unittest.TestCase): 7 | def testGivenANewPolygon_shouldDefineItsNormal(self): 8 | polygon = Polygon(vertices=[Vertex(0, 0, 0), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0)]) 9 | self.assertEqual(Vertex(0, 0, 1), polygon.normal) 10 | 11 | def testGivenANewPolygonWithNormal_shouldUseProvidedNormalWithoutNormalizing(self): 12 | forcedNormal = Vector(7, 5, 3) 13 | polygon = Polygon( 14 | vertices=[Vertex(0, 0, 0), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0)], normal=forcedNormal 15 | ) 16 | self.assertEqual(forcedNormal, polygon.normal) 17 | 18 | def testGivenANewPolygon_shouldDefineItsCentroid(self): 19 | polygon = Polygon(vertices=[Vertex(0, 0, 1), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0)]) 20 | self.assertEqual(Vector(5 / 4, 3 / 4, 1 / 4), polygon.centroid) 21 | 22 | def testGivenANewPolygon_whenModifyingVertexAndResetBoundingBox_shouldChangeBbox(self): 23 | triangle = Polygon(vertices=[Vertex(0, 0, 0), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 4, 2)]) 24 | oldBbox = triangle.bbox 25 | triangle.vertices[0].update(5, 1, 1) 26 | triangle.resetBoundingBox() 27 | newBbox = triangle.bbox 28 | self.assertNotEqual(oldBbox, newBbox) 29 | 30 | def testGiven2EqualPolygons_whenEquals_shouldReturnTrue(self): 31 | polygon1 = Polygon(vertices=[Vertex(0, 0, 0), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0)]) 32 | polygon2 = Polygon(vertices=[Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0), Vertex(0, 0, 0)]) 33 | self.assertEqual(polygon1, polygon2) 34 | 35 | def testGiven2DifferentPolygons_whenEquals_shouldReturnFalse(self): 36 | polygon1 = Polygon(vertices=[Vertex(0, 0, 0), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0)]) 37 | polygon2 = Polygon(vertices=[Vertex(1, 0, 0), Vertex(2, 2, 0), Vertex(1, 1, 0), Vertex(0, 0, 0)]) 38 | self.assertNotEqual(polygon1, polygon2) 39 | 40 | def testWhenGetCentroid_shouldReturnAverageVertex(self): 41 | polygon = Polygon(vertices=[Vertex(0, 0, 0), Vertex(2, 0, 0), Vertex(2, 2, 0), Vertex(0, 2, 0)]) 42 | self.assertEqual(Vector(1, 1, 0), polygon.getCentroid()) 43 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/geometry/testRotation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene.geometry import Rotation 4 | 5 | 6 | class TestRotation(unittest.TestCase): 7 | def testGivenDefaultRotation_shouldBeAlignedWithAxes(self): 8 | rotation = Rotation() 9 | 10 | self.assertEqual(0, rotation.xTheta) 11 | self.assertEqual(0, rotation.yTheta) 12 | self.assertEqual(0, rotation.zTheta) 13 | 14 | def testWhenAddOtherRotation_shouldAddItToCurrentRotation(self): 15 | rotation = Rotation(10, 30, 0) 16 | otherRotation = Rotation(90, 0, 90) 17 | 18 | rotation.add(otherRotation) 19 | 20 | self.assertEqual(10 + 90, rotation.xTheta) 21 | self.assertEqual(30 + 0, rotation.yTheta) 22 | self.assertEqual(0 + 90, rotation.zTheta) 23 | 24 | def testGivenNoRotation_whenAskedBoolean_shouldReturnFalse(self): 25 | noRotation = Rotation() 26 | self.assertFalse(noRotation) 27 | 28 | def testGivenRotation_whenAskedBoolean_shouldReturnTrue(self): 29 | noRotation = Rotation(10, 30, 0) 30 | self.assertTrue(noRotation) 31 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/geometry/testTriangle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene.geometry import Triangle, Vector, Vertex 4 | 5 | 6 | class TestTriangle(unittest.TestCase): 7 | def testGivenANewTriangle_shouldDefineItsNormal(self): 8 | triangle = Triangle(v1=Vertex(0, 0, 0), v2=Vertex(2, 0, 0), v3=Vertex(2, 2, 0)) 9 | self.assertEqual(Vector(0, 0, 1), triangle.normal) 10 | 11 | def testGivenANewTriangle_shouldDefineItsCentroid(self): 12 | triangle = Triangle(v1=Vertex(0, 0, 1), v2=Vertex(2, 0, 0), v3=Vertex(2, 2, 0)) 13 | self.assertEqual(Vector(4 / 3, 2 / 3, 1 / 3), triangle.centroid) 14 | 15 | def testGivenANewTriangle_whenModifyingVertex_resetBoundingBoxShouldChangeBbox(self): 16 | triangle = Triangle(v1=Vertex(0, 0, 0), v2=Vertex(2, 0, 0), v3=Vertex(2, 2, 0)) 17 | oldBbox = triangle.bbox 18 | 19 | triangle.vertices[0].update(1, 1, 1) 20 | triangle.resetBoundingBox() 21 | newBbox = triangle.bbox 22 | 23 | self.assertNotEqual(oldBbox, newBbox) 24 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/intersection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/intersection/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/intersection/testRay.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pytissueoptics.scene import Vector 5 | from pytissueoptics.scene.intersection import Ray 6 | 7 | 8 | class TestRay(unittest.TestCase): 9 | def testGivenNewRay_shouldNormalizeItsDirection(self): 10 | ray = Ray(origin=Vector(0, 0, 0), direction=Vector(2, 2, 0), length=10) 11 | self.assertEqual(Vector(math.sqrt(2) / 2, math.sqrt(2) / 2, 0), ray.direction) 12 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/intersection/testUniformRaySource.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pytissueoptics.scene import Vector 5 | from pytissueoptics.scene.intersection import UniformRaySource 6 | 7 | 8 | class TestUniformRaySource(unittest.TestCase): 9 | def testShouldInitializeAllRayOriginsAtTheSourcePosition(self): 10 | sourcePosition = Vector(3, 3, 0) 11 | direction = Vector(0, 0, 1) 12 | xTheta, yTheta = 10, 20 13 | xRes, yRes = 16, 16 14 | 15 | source = UniformRaySource(sourcePosition, direction, xTheta, yTheta, xRes, yRes) 16 | 17 | for ray in source.rays: 18 | self.assertEqual(ray.origin, sourcePosition) 19 | 20 | def testShouldHaveAUniformAngularDistributionOfRaysDirectionCenteredAroundTheSourceDirection(self): 21 | sourcePosition = Vector(3, 3, 0) 22 | sourceXAngle = 30 * math.pi / 180 23 | sourceDirection = Vector(math.sin(sourceXAngle), 0, math.cos(sourceXAngle)) 24 | xTheta, yTheta = 20, 10 25 | xRes, yRes = 5, 2 26 | 27 | source = UniformRaySource(sourcePosition, sourceDirection, xTheta, yTheta, xRes, yRes) 28 | 29 | expectedXAngles = [-10, -5, 0, 5, 10] 30 | expectedYAngles = [-5, 5] 31 | for i, ray in enumerate(source.rays): 32 | xAngle, yAngle = self._getXYAngleOfDirection(ray.direction) 33 | xAngle -= sourceXAngle * 180 / math.pi 34 | 35 | expectedYAngle = expectedYAngles[0] if i < 5 else expectedYAngles[1] 36 | self.assertAlmostEqual(expectedXAngles[i % 5], xAngle) 37 | self.assertAlmostEqual(expectedYAngle, yAngle) 38 | 39 | @staticmethod 40 | def _getXYAngleOfDirection(direction): 41 | return math.atan(direction.x / direction.z) * 180 / math.pi, math.asin(direction.y) * 180 / math.pi 42 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/loader/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/loader/parsers/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/test.wrongExtension: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/loader/parsers/objFiles/test.wrongExtension -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuads.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | # unsupported material lib for testing purposes 5 | mtllib cube.mtl 6 | 7 | v -0.5 -0.5 -0.5 8 | v 0.5 -0.5 -0.5 9 | v 0.5 0.5 -0.5 10 | v -0.5 0.5 -0.5 11 | v -0.5 -0.5 0.5 12 | v 0.5 -0.5 0.5 13 | v 0.5 0.5 0.5 14 | v -0.5 0.5 0.5 15 | # 8 vertices 16 | 17 | vn 0 0 -1 18 | vn 1 0 0 19 | vn 0 0 1 20 | vn -1 0 0 21 | vn 0 1 0 22 | vn 0 -1 0 23 | # 6 normals 24 | 25 | o cube 26 | usemtl metal 03 27 | g front 28 | f 1//1 2//1 3//1 4//1 29 | g back 30 | f 5//3 6//3 7//3 8//3 31 | g left 32 | f 5//4 1//4 4//4 8//4 33 | g right 34 | f 2//2 3//2 7//2 6//2 35 | g top 36 | f 4//5 3//5 7//5 8//5 37 | g bottom 38 | f 1//6 2//6 6//6 5//6 39 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuadsNoObject.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vn 0 0 -1 15 | vn 1 0 0 16 | vn 0 0 1 17 | vn -1 0 0 18 | vn 0 1 0 19 | vn 0 -1 0 20 | # 6 normals 21 | 22 | g front 23 | f 1//1 2//1 3//1 4//1 24 | g back 25 | f 5//3 6//3 7//3 8//3 26 | g left 27 | f 5//4 1//4 4//4 8//4 28 | g right 29 | f 2//2 3//2 7//2 6//2 30 | g top 31 | f 4//5 3//5 7//5 8//5 32 | g bottom 33 | f 1//6 2//6 6//6 5//6 34 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuadsNoObjectName.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vn 0 0 -1 15 | vn 1 0 0 16 | vn 0 0 1 17 | vn -1 0 0 18 | vn 0 1 0 19 | vn 0 -1 0 20 | # 6 normals 21 | 22 | o 23 | usemtl metal 03 24 | g front 25 | f 1//1 2//1 3//1 4//1 26 | g back 27 | f 5//3 6//3 7//3 8//3 28 | g left 29 | f 5//4 1//4 4//4 8//4 30 | g right 31 | f 2//2 3//2 7//2 6//2 32 | g top 33 | f 4//5 3//5 7//5 8//5 34 | g bottom 35 | f 1//6 2//6 6//6 5//6 36 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuadsNoSurface.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vn 0 0 -1 15 | vn 1 0 0 16 | vn 0 0 1 17 | vn -1 0 0 18 | vn 0 1 0 19 | vn 0 -1 0 20 | # 6 normals 21 | 22 | o cube 23 | f 1//1 2//1 3//1 4//1 24 | g back 25 | f 5//3 6//3 7//3 8//3 26 | g left 27 | f 5//4 1//4 4//4 8//4 28 | g right 29 | f 2//2 3//2 7//2 6//2 30 | g top 31 | f 4//5 3//5 7//5 8//5 32 | g bottom 33 | f 1//6 2//6 6//6 5//6 34 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuadsNoSurfaceName.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vn 0 0 -1 15 | vn 1 0 0 16 | vn 0 0 1 17 | vn -1 0 0 18 | vn 0 1 0 19 | vn 0 -1 0 20 | # 6 normals 21 | 22 | o cube 23 | usemtl metal 03 24 | g 25 | f 1//1 2//1 3//1 4//1 26 | g back 27 | f 5//3 6//3 7//3 8//3 28 | g left 29 | f 5//4 1//4 4//4 8//4 30 | g right 31 | f 2//2 3//2 7//2 6//2 32 | g top 33 | f 4//5 3//5 7//5 8//5 34 | g bottom 35 | f 1//6 2//6 6//6 5//6 36 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuadsRepeatingSurface.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vn 0 0 -1 15 | vn 1 0 0 16 | vn 0 0 1 17 | vn -1 0 0 18 | vn 0 1 0 19 | vn 0 -1 0 20 | # 6 normals 21 | 22 | o cube 23 | usemtl metal 03 24 | g face 25 | f 1//1 2//1 3//1 4//1 26 | g face 27 | f 5//3 6//3 7//3 8//3 28 | g face 29 | f 5//4 1//4 4//4 8//4 30 | g face 31 | f 2//2 3//2 7//2 6//2 32 | g face 33 | f 4//5 3//5 7//5 8//5 34 | g face 35 | f 1//6 2//6 6//6 5//6 36 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeQuadsTexture.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vt 1.000000 0.500000 15 | vt 1.000000 1.000000 16 | vt 0.500000 1.000000 17 | vt 0.500000 0.500000 18 | # 4 texture coordinates 19 | 20 | vn 0 0 -1 21 | vn 1 0 0 22 | vn 0 0 1 23 | vn -1 0 0 24 | vn 0 1 0 25 | vn 0 -1 0 26 | # 6 normals 27 | 28 | o cube 29 | usemtl metal 03 30 | g front 31 | f 1/1/1 2/2/1 3/3/1 4/4/1 32 | g back 33 | f 5/1/3 6/1/3 7/1/3 8/1/3 34 | g left 35 | f 5/1/4 1/1/4 4/1/4 8/1/4 36 | g right 37 | f 2/1/2 3/1/2 7/1/2 6/1/2 38 | g top 39 | f 4/1/5 3/1/5 7/1/5 8/1/5 40 | g bottom 41 | f 1/1/6 2/1/6 6/1/6 5/1/6 42 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeTriangles.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | 4 | v -0.5 -0.5 -0.5 5 | v 0.5 -0.5 -0.5 6 | v 0.5 0.5 -0.5 7 | v -0.5 0.5 -0.5 8 | v -0.5 -0.5 0.5 9 | v 0.5 -0.5 0.5 10 | v 0.5 0.5 0.5 11 | v -0.5 0.5 0.5 12 | # 8 vertices 13 | 14 | vn 0 0 -1 15 | vn 1 0 0 16 | vn 0 0 1 17 | vn -1 0 0 18 | vn 0 1 0 19 | vn 0 -1 0 20 | # 6 normals 21 | 22 | o cube 23 | usemtl metal 03 24 | g front 25 | f 1//1 2//1 3//1 26 | f 1//1 4//1 3//1 27 | g back 28 | f 5//3 6//3 7//3 29 | f 5//3 8//3 7//3 30 | g left 31 | f 5//4 1//4 4//4 32 | f 5//4 8//4 4//4 33 | g right 34 | f 2//2 3//2 7//2 35 | f 2//2 6//2 7//2 36 | g top 37 | f 4//5 3//5 7//5 38 | f 4//5 8//5 7//5 39 | g bottom 40 | f 1//6 2//6 6//6 41 | f 1//6 5//6 6//6 42 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/parsers/objFiles/testCubeTrianglesMulti.obj: -------------------------------------------------------------------------------- 1 | # WaveFront *.obj file made by hand for testing purposes 2 | # This is a unit cube with 6 faces and 6 groups 3 | # one face has 5 vertices, and will build 3 triangles. 4 | 5 | v -0.5 -0.5 -0.5 6 | v 0.5 -0.5 -0.5 7 | v 0.5 0.5 -0.5 8 | v -0.5 0.5 -0.5 9 | v -0.5 -0.5 0.5 10 | v 0.5 -0.5 0.5 11 | v 0.5 0.5 0.5 12 | v -0.5 0.5 0.5 13 | v 0.0 0.5 0.5 14 | # 8 vertices 15 | 16 | vn 0 0 -1 17 | vn 1 0 0 18 | vn 0 0 1 19 | vn -1 0 0 20 | vn 0 1 0 21 | vn 0 -1 0 22 | # 6 normals 23 | 24 | o cube 25 | usemtl metal 03 26 | g front 27 | f 1//1 2//1 3//1 28 | f 1//1 4//1 3//1 29 | g back 30 | f 5//3 6//3 7//3 9//3 8//3 31 | g left 32 | f 5//4 1//4 4//4 33 | f 5//4 8//4 4//4 34 | g right 35 | f 2//2 3//2 7//2 36 | f 2//2 6//2 7//2 37 | g top 38 | f 4//5 3//5 7//5 39 | f 4//5 8//5 7//5 40 | g bottom 41 | f 1//6 2//6 6//6 42 | f 1//6 5//6 6//6 43 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/testLoadSolid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from pytissueoptics.scene.loader import loadSolid 5 | 6 | 7 | class TestLoadSolid(unittest.TestCase): 8 | TEST_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | def testWhenLoadingFromOBJ_shouldReturnASolidOfThisFile(self): 11 | solid = loadSolid(self._filepath("testCubeTrianglesMulti.obj"), showProgress=False) 12 | self.assertIsNotNone(solid) 13 | 14 | self.assertEqual(13, len(solid.getPolygons())) 15 | self.assertEqual( 16 | ["cube_front", "cube_back", "cube_left", "cube_right", "cube_top", "cube_bottom"], solid.surfaceLabels 17 | ) 18 | 19 | def _filepath(self, fileName) -> str: 20 | return os.path.join(self.TEST_DIRECTORY, "parsers", "objFiles", fileName) 21 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/loader/testLoader.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from typing import List 4 | 5 | from pytissueoptics.scene.loader import Loader 6 | from pytissueoptics.scene.solids import Solid 7 | 8 | 9 | class TestLoader(unittest.TestCase): 10 | TEST_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 11 | 12 | def testWhenLoadWithWrongExtension_shouldNotLoad(self): 13 | with self.assertRaises(NotImplementedError): 14 | _ = Loader().load(self._filepath("test.wrongExtension"), showProgress=False) 15 | 16 | def testWhenLoadingOBJ_shouldLoad(self): 17 | loader = Loader() 18 | _ = loader.load(self._filepath("testCubeTrianglesMulti.obj"), showProgress=False) 19 | 20 | def testWhenLoadingOBJ_shouldReturnListOfSolids(self): 21 | loader = Loader() 22 | solids = loader.load(self._filepath("testCubeTrianglesMulti.obj"), showProgress=False) 23 | self.assertIsInstance(solids, List) 24 | for solid in solids: 25 | self.assertIsInstance(solid, Solid) 26 | 27 | def testWhenLoadingMultiPolygonObject_shouldSplitInTriangles(self): 28 | loader = Loader() 29 | solids = loader.load(self._filepath("testCubeTrianglesMulti.obj"), showProgress=False) 30 | self.assertEqual(1, len(solids)) 31 | self.assertEqual(13, len(solids[0].getPolygons())) 32 | 33 | def testWhenLoadingMultiGroupObject_shouldSplitCorrectGroups(self): 34 | loader = Loader() 35 | solids = loader.load(self._filepath("testCubeTrianglesMulti.obj"), showProgress=False) 36 | self.assertCountEqual( 37 | ["cube_front", "cube_back", "cube_bottom", "cube_top", "cube_right", "cube_left"], solids[0].surfaceLabels 38 | ) 39 | 40 | def testWhenLoadingMultiGroupObject_shouldHaveCorrectAmountOfElementsPerGroup(self): 41 | loader = Loader() 42 | 43 | solids = loader.load(self._filepath("testCubeTrianglesMulti.obj"), showProgress=False) 44 | 45 | self.assertEqual(2, len(solids[0].surfaces.getPolygons("front"))) 46 | self.assertEqual(3, len(solids[0].surfaces.getPolygons("back"))) 47 | 48 | def _filepath(self, fileName) -> str: 49 | return os.path.join(self.TEST_DIRECTORY, "parsers", "objFiles", fileName) 50 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/logger/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/logger/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/logger/testListArrayContainer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.scene.logger.listArrayContainer import ListArrayContainer 6 | 7 | 8 | class TestListArrayContainer(unittest.TestCase): 9 | def setUp(self): 10 | self.listArrayContainer = ListArrayContainer() 11 | self.otherListArrayContainer = ListArrayContainer() 12 | 13 | def testShouldHaveLengthOfZero(self): 14 | self.assertEqual(0, len(self.listArrayContainer)) 15 | 16 | def testShouldInitializeDataToNone(self): 17 | self.assertIsNone(self.listArrayContainer.getData()) 18 | 19 | def testWhenAppendingList_shouldHaveArrayData(self): 20 | self.listArrayContainer.append([1, 2, 3]) 21 | self.assertTrue(np.array_equal(np.array([[1, 2, 3]]), self.listArrayContainer.getData())) 22 | 23 | def testWhenAppendingArray_shouldHaveArrayData(self): 24 | self.listArrayContainer.append(np.array([[1, 2, 3]])) 25 | self.assertTrue(np.array_equal(np.array([[1, 2, 3]]), self.listArrayContainer.getData())) 26 | 27 | def testWhenAppendingArrayWithMoreColumns_shouldRaiseException(self): 28 | self.listArrayContainer.append(np.array([[1, 2, 3]])) 29 | 30 | with self.assertRaises(AssertionError): 31 | self.listArrayContainer.append(np.array([[4, 5, 6, 7]])) 32 | 33 | def testGivenListAndArrayData_shouldHaveMergedArrayData(self): 34 | self.listArrayContainer.append([1, 2, 3]) 35 | self.listArrayContainer.append(np.array([[4, 5, 6]])) 36 | 37 | self.assertTrue(np.array_equal(np.array([[1, 2, 3], [4, 5, 6]]), self.listArrayContainer.getData())) 38 | 39 | def testGivenListAndArrayData_shouldHaveLengthEqualToTheTotalNumberOfRows(self): 40 | self.listArrayContainer.append([1, 2, 3]) 41 | self.listArrayContainer.append(np.array([[4, 5, 6], [7, 8, 9]])) 42 | 43 | self.assertEqual(3, len(self.listArrayContainer)) 44 | 45 | def testWhenExtending_shouldOnlyExtendThisContainer(self): 46 | self.listArrayContainer.append([1, 2, 3]) 47 | self.listArrayContainer.append(np.array([[4, 5, 6]])) 48 | self.otherListArrayContainer.append([7, 8, 9]) 49 | self.otherListArrayContainer.append(np.array([[10, 11, 12]])) 50 | 51 | self.listArrayContainer.extend(self.otherListArrayContainer) 52 | 53 | self.assertTrue( 54 | np.array_equal(np.array([[1, 2, 3], [7, 8, 9], [4, 5, 6], [10, 11, 12]]), self.listArrayContainer.getData()) 55 | ) 56 | self.assertTrue(np.array_equal(np.array([[7, 8, 9], [10, 11, 12]]), self.otherListArrayContainer.getData())) 57 | 58 | def testWhenExtendingAnEmptyContainerWithAnother_shouldNotExtendListDataWithAReferenceOfTheOthersListData(self): 59 | self.otherListArrayContainer.append([4, 5, 6]) 60 | 61 | self.listArrayContainer.extend(self.otherListArrayContainer) 62 | 63 | self.otherListArrayContainer.append([7, 8, 9]) 64 | self.assertTrue(np.array_equal([[4, 5, 6]], self.listArrayContainer.getData())) 65 | 66 | def testWhenExtendingAnEmptyContainerWithAnother_shouldNotExtendArrayDataWithAReferenceOfTheOthersArrayData(self): 67 | self.otherListArrayContainer.append(np.array([[1, 2, 3]])) 68 | 69 | self.listArrayContainer.extend(self.otherListArrayContainer) 70 | 71 | self.otherListArrayContainer.append(np.array([[4, 5, 6]])) 72 | self.assertTrue(np.array_equal(np.array([[1, 2, 3]]), self.listArrayContainer.getData())) 73 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/scene/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/scene/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/shader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/shader/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/shader/testSmoothing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene import Vector 4 | from pytissueoptics.scene.geometry import Polygon, Vertex 5 | from pytissueoptics.scene.shader import getSmoothNormal 6 | 7 | 8 | class TestSmoothing(unittest.TestCase): 9 | def setUp(self): 10 | # Create a XY square polygon with normals pointing outwards along the Z axis. 11 | vertices = [Vertex(0, 0, 0), Vertex(1, 0, 0), Vertex(1, 1, 0), Vertex(0, 1, 0)] 12 | normals = [Vector(-1, -1, 1), Vector(1, -1, 1), Vector(1, 1, 1), Vector(-1, 1, 1)] 13 | for i in range(4): 14 | normals[i].normalize() 15 | vertices[i].normal = normals[i] 16 | 17 | self.initialNormal = Vector(0, 0, 1) 18 | self.polygon = Polygon(vertices=vertices, normal=self.initialNormal) 19 | self.polygon.toSmooth = True 20 | 21 | def testShouldReturnInterpolatedNormalAtDesiredPosition(self): 22 | position = Vector(0.5, 0.5, 0) 23 | smoothNormal = getSmoothNormal(self.polygon, position) 24 | self.assertEqual(Vector(0, 0, 1), smoothNormal) 25 | 26 | def testGivenPolygonIsNotToSmooth_shouldReturnDefaultPolygonNormal(self): 27 | self.polygon.toSmooth = False 28 | smoothNormal = getSmoothNormal(self.polygon, Vector(0.2, 0.2, 0)) 29 | self.assertEqual(self.initialNormal, smoothNormal) 30 | 31 | def testGivenPositionOnSideOfPolygon_shouldReturnInterpolatedNormalBetweenSideVertices(self): 32 | position = Vector(0.5, 0, 0) 33 | smoothNormal = getSmoothNormal(self.polygon, position) 34 | expectedNormal = Vector(0, -1, 1) 35 | expectedNormal.normalize() 36 | self.assertEqual(expectedNormal, smoothNormal) 37 | 38 | def testGivenPositionOnVertex_shouldReturnVertexNormal(self): 39 | position = Vector(0, 0, 0) 40 | smoothNormal = getSmoothNormal(self.polygon, position) 41 | self.assertEqual(self.polygon.vertices[0].normal, smoothNormal) 42 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/solids/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/solids/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/solids/testCone.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene.geometry import Vector, Vertex, primitives 4 | from pytissueoptics.scene.solids import Cone 5 | 6 | 7 | class TestCone(unittest.TestCase): 8 | def testWhenContainsWithVerticesThatAreAllInsideTheCone_shouldReturnTrue(self): 9 | r = 1 10 | h = 3 11 | midRadius = r * 0.5 12 | f = 0.9 13 | cylinder = Cone(radius=r, length=h, u=32, v=2, position=Vector(0, 0, 0)) 14 | 15 | vertices = [ 16 | Vertex(f * midRadius, 0, 0), 17 | Vertex(0, f * midRadius, 0), 18 | Vertex(-f * midRadius, 0, 0), 19 | Vertex(0, -f * midRadius, 0), 20 | Vertex(0, 0, f * h * 0.5), 21 | Vertex(0, 0, -f * h * 0.5), 22 | ] 23 | 24 | self.assertTrue(cylinder.contains(*vertices)) 25 | 26 | def testWhenContainsWithVerticesThatAreNotInsideTheCone_shouldReturnFalse(self): 27 | r = 1 28 | h = 3 29 | midRadius = r * 0.5 30 | f = 1.1 31 | cylinder = Cone(radius=r, length=h, u=32, v=2, position=Vector(0, 0, 0)) 32 | 33 | vertices = [ 34 | Vertex(f * midRadius, 0, 0), 35 | Vertex(0, f * midRadius, 0), 36 | Vertex(-f * midRadius, 0, 0), 37 | Vertex(0, -f * midRadius, 0), 38 | Vertex(0, 0, f * h * 0.5), 39 | Vertex(0, 0, -f * h * 0.5), 40 | ] 41 | 42 | for vertex in vertices: 43 | self.assertFalse(cylinder.contains(vertex)) 44 | 45 | def testGivenANewWithQuadPrimitive_shouldNotCreateCone(self): 46 | with self.assertRaises(NotImplementedError): 47 | Cone(primitive=primitives.QUAD) 48 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/solids/testSolidGroup.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene.geometry import Vector 4 | from pytissueoptics.scene.solids import Cube, SolidFactory 5 | 6 | 7 | class TestSolidGroup(unittest.TestCase): 8 | def setUp(self): 9 | self.material = "A Material" 10 | self.position = Vector(0, 0, 10) 11 | self.cuboidSurfaceLabels = ["left", "right", "bottom", "top", "front", "back"] 12 | self.cuboid1 = Cube(edge=2, position=Vector(2, 0, 0), material=self.material, label="cuboid1") 13 | self.cuboid2 = Cube(edge=2, position=Vector(-2, 0, 0), material=self.material, label="cuboid2") 14 | self.solidGroup = SolidFactory().fromSolids([self.cuboid1, self.cuboid2], position=self.position) 15 | 16 | def testShouldHaveSurfacesOfAllInputSolids(self): 17 | groupSurfaceLabels = self.solidGroup.surfaceLabels 18 | 19 | self.assertEqual(12, len(groupSurfaceLabels)) 20 | for cuboid in [self.cuboid1, self.cuboid2]: 21 | for surfaceLabel in self.cuboidSurfaceLabels: 22 | self.assertIn(f"{cuboid.getLabel()}_{surfaceLabel}", groupSurfaceLabels) 23 | 24 | def testShouldMoveCentroidToBeAtDesiredPosition(self): 25 | self.assertEqual(self.solidGroup.position, self.position) 26 | 27 | def testGivenSolidsWithTheSameLabel_shouldIncrementSolidLabels(self): 28 | CUBOID_LABEL = "cuboid" 29 | cuboid1 = Cube(edge=1, position=Vector(2, 0, 0), material=self.material, label=CUBOID_LABEL) 30 | cuboid2 = Cube(edge=1, position=Vector(-2, 0, 0), material=self.material, label=CUBOID_LABEL) 31 | cuboid3 = Cube(edge=1, position=Vector(0, 0, 0), material=self.material, label=CUBOID_LABEL) 32 | solidGroup = SolidFactory().fromSolids([cuboid1, cuboid2, cuboid3], position=self.position) 33 | 34 | groupSurfaceLabels = solidGroup.surfaceLabels 35 | self.assertEqual(18, len(groupSurfaceLabels)) 36 | for expectedCuboidLabel in [CUBOID_LABEL, f"{CUBOID_LABEL}2", f"{CUBOID_LABEL}3"]: 37 | for surfaceLabel in self.cuboidSurfaceLabels: 38 | self.assertIn(f"{expectedCuboidLabel}_{surfaceLabel}", groupSurfaceLabels) 39 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/solids/testSphere.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pytissueoptics.scene.geometry import Vector, Vertex, primitives 5 | from pytissueoptics.scene.solids import Sphere 6 | 7 | 8 | class TestSphere(unittest.TestCase): 9 | def testGivenANewDefaultSphere_shouldBePlacedAtOrigin(self): 10 | sphere = Sphere() 11 | self.assertEqual(Vector(0, 0, 0), sphere.position) 12 | 13 | def testGivenANewSphere_shouldBePlacedAtDesiredPosition(self): 14 | position = Vector(2, 2, 1) 15 | sphere = Sphere(position=position) 16 | self.assertEqual(Vector(2, 2, 1), sphere.position) 17 | 18 | def testGivenANewSphereWithQuadPrimitive_shouldNotCreateSphere(self): 19 | with self.assertRaises(NotImplementedError): 20 | Sphere(primitive=primitives.QUAD) 21 | 22 | def testGivenANewDefaultSphere_shouldHaveARadiusOf1(self): 23 | sphere = Sphere() 24 | self.assertEqual(1, sphere.radius) 25 | 26 | def testGivenASphere_shouldHaveCorrectVerticesLength(self): 27 | sphere = Sphere(radius=2) 28 | self.assertEqual(2, sphere._vertices[0].getNorm()) 29 | 30 | def testGivenALowOrderSphere_shouldNotApproachCorrectSphereArea(self): 31 | sphere = Sphere() 32 | icosphereArea = 0 33 | perfectSphereArea = 4 * math.pi * sphere.radius**2 34 | 35 | for polygon in sphere.getPolygons(): 36 | icosphereArea += 0.5 * polygon.vertices[0].cross(polygon.vertices[1]).getNorm() 37 | 38 | self.assertNotAlmostEqual(perfectSphereArea, icosphereArea, 3) 39 | 40 | def testGivenAHighOrderSphere_shouldApproachCorrectSphereArea(self): 41 | sphere = Sphere(radius=1, order=4) 42 | icosphereArea = 0 43 | perfectSphereArea = 4 * math.pi * sphere.radius**2 44 | tolerance = 0.002 45 | 46 | for polygon in sphere.getPolygons(): 47 | AB = polygon.vertices[0] - polygon.vertices[1] 48 | AC = polygon.vertices[0] - polygon.vertices[2] 49 | icosphereArea += 0.5 * AB.cross(AC).getNorm() 50 | 51 | self.assertAlmostEqual(perfectSphereArea, icosphereArea, delta=tolerance * perfectSphereArea) 52 | 53 | def testWhenContainsWithVerticesThatAreAllInsideTheSphere_shouldReturnTrue(self): 54 | sphere = Sphere(1, position=Vector(2, 2, 0)) 55 | vertices = [Vertex(2.5, 2.5, 0), Vertex(2, 2, 0)] 56 | 57 | self.assertTrue(sphere.contains(*vertices)) 58 | 59 | def testWhenContainsWithVerticesThatAreNotAllInsideTheSphere_shouldReturnFalse(self): 60 | sphere = Sphere(1, position=Vector(2, 2, 0)) 61 | vertices = [Vertex(3, 3, 1), Vertex(2, 2, 0)] 62 | 63 | self.assertFalse(sphere.contains(*vertices)) 64 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/tree/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/tree/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/tree/testSpacePartition.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from mockito import expect, verifyNoUnwantedInteractions 4 | 5 | from pytissueoptics.scene.geometry import BoundingBox, Polygon, Vector, Vertex 6 | from pytissueoptics.scene.tree import Node, SpacePartition 7 | from pytissueoptics.scene.tree.treeConstructor import SplitNodeResult, TreeConstructor 8 | 9 | 10 | class TestSpacePartition(unittest.TestCase): 11 | def setUp(self): 12 | poly = Polygon([Vertex(0, 0, 0), Vertex(1, 1, 0), Vertex(1, 2, 3)]) 13 | self.polyList = [poly, poly, poly, poly] 14 | bbox1 = BoundingBox(xLim=[0, 1], yLim=[0.5, 1], zLim=[0.5, 1]) 15 | bbox2 = BoundingBox(xLim=[0.5, 1], yLim=[0.5, 1], zLim=[0.5, 1]) 16 | bbox3 = BoundingBox(xLim=[0, 0.5], yLim=[0.5, 1], zLim=[0.5, 1]) 17 | result1 = SplitNodeResult(False, [bbox1], [self.polyList]) 18 | result2 = SplitNodeResult(False, [bbox2, bbox3], [self.polyList]) 19 | result3 = SplitNodeResult(True, [bbox3], [self.polyList]) 20 | 21 | self.root = Node(polygons=self.polyList, bbox=bbox1) 22 | self.treeConstructor = TreeConstructor() 23 | expect(self.treeConstructor, times=3)._splitNode(...).thenReturn(result1).thenReturn(result2).thenReturn( 24 | result3 25 | ) 26 | self.tree = SpacePartition(bbox1, self.polyList, self.treeConstructor, minLeafSize=1) 27 | 28 | def testShouldHaveNodeCount(self): 29 | count = self.tree.getNodeCount() 30 | verifyNoUnwantedInteractions() 31 | self.assertEqual(3, count) 32 | 33 | def testShouldHaveLeafCount(self): 34 | count = self.tree.getLeafCount() 35 | verifyNoUnwantedInteractions() 36 | self.assertEqual(1, count) 37 | 38 | def testShouldHaveLeafBBoxes(self): 39 | bbox = BoundingBox(xLim=[0.5, 1], yLim=[0.5, 1], zLim=[0.5, 1]) 40 | leafBboxes = self.tree.getLeafBoundingBoxes() 41 | verifyNoUnwantedInteractions() 42 | self.assertEqual([bbox], leafBboxes) 43 | 44 | def testShouldHaveMaxDepth(self): 45 | depth = self.tree.getMaxDepth() 46 | verifyNoUnwantedInteractions() 47 | self.assertEqual(2, depth) 48 | 49 | def testShouldHaveAverageDepth(self): 50 | avgDepth = self.tree.getAverageDepth() 51 | verifyNoUnwantedInteractions() 52 | self.assertEqual(2, avgDepth) 53 | 54 | def testShouldHaveAverageLeafSize(self): 55 | avgLeafSize = self.tree.getAverageLeafSize() 56 | verifyNoUnwantedInteractions() 57 | self.assertEqual(4, avgLeafSize) 58 | 59 | def testShouldHaveLeafPolygons(self): 60 | leafPolygons = self.tree.getLeafPolygons() 61 | verifyNoUnwantedInteractions() 62 | self.assertEqual(self.polyList, leafPolygons) 63 | 64 | def testGivenAPointOnlyInLeafNode_whenSearchPoint_shouldReturnLeafNode(self): 65 | point = Vector(0.6, 0.6, 0.6) 66 | node = self.tree.searchPoint(point) 67 | expectedNodeBbox = BoundingBox(xLim=[0.5, 1], yLim=[0.5, 1], zLim=[0.5, 1]) 68 | self.assertEqual(expectedNodeBbox, node.bbox) 69 | 70 | def testGivenPointNotInLeaf_whenSearchPoint_shouldReturnCorrectNode(self): 71 | point = Vector(0.4, 0.6, 0.6) 72 | node = self.tree.searchPoint(point) 73 | expectedNodeBbox = BoundingBox(xLim=[0, 1], yLim=[0.5, 1], zLim=[0.5, 1]) 74 | self.assertEqual(expectedNodeBbox, node.bbox) 75 | 76 | def testGivenOutsideVector_whenSearchPoint_shouldReturnNone(self): 77 | point = Vector(0.1, 0.4, 0.6) 78 | node = self.tree.searchPoint(point) 79 | expectedNodeBbox = None 80 | self.assertEqual(expectedNodeBbox, node) 81 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/tree/treeConstructor/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/tree/treeConstructor/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/tree/treeConstructor/binary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/tree/treeConstructor/binary/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/utils/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/utils/testNoProgressBar.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene.utils import noProgressBar 4 | 5 | 6 | class TestNoProgressBar(unittest.TestCase): 7 | def testWhenUsingNoProgressBarWithTQDMArguments_shouldWarnAndIterate(self): 8 | value = 0 9 | with self.assertWarns(UserWarning): 10 | pbar = noProgressBar([1, 2, 3], desc="A description") 11 | for element in pbar: 12 | value += element 13 | self.assertEqual(6, value) 14 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/__init__.py -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testImages/images.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/testImages/images.png -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testImages/logger_natural.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/testImages/logger_natural.png -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testImages/scene_natural.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/testImages/scene_natural.png -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testImages/solid_natural_front.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/testImages/solid_natural_front.png -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testImages/solid_optics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/testImages/solid_optics.png -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testImages/sphere_normals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DCC-Lab/PyTissueOptics/e20da2bb6ff460a5dbfd6de73bf4bb05d63e64e8/pytissueoptics/scene/tests/viewer/testImages/sphere_normals.png -------------------------------------------------------------------------------- /pytissueoptics/scene/tests/viewer/testMayaviSolid.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pytissueoptics.scene import Vector 4 | from pytissueoptics.scene.geometry import Polygon, Quad, SurfaceCollection, Triangle, primitives 5 | from pytissueoptics.scene.solids import Solid 6 | from pytissueoptics.scene.viewer.mayavi import MayaviSolid 7 | 8 | 9 | class TestMayaviSolid(unittest.TestCase): 10 | def createSimpleSolid(self, primitive=primitives.TRIANGLE) -> Solid: 11 | V = [Vector(0, 0, 0), Vector(0, 1, 0), Vector(1, 1, 0), Vector(1, 0, 0), Vector(0.5, -1, 0)] 12 | self.surfaces = SurfaceCollection() 13 | if primitive == primitives.TRIANGLE: 14 | self.surfaces.add("Face", [Triangle(V[0], V[1], V[2]), Triangle(V[0], V[2], V[3])]) 15 | if primitive == primitives.QUAD: 16 | self.surfaces.add("Face", [Quad(V[0], V[1], V[2], V[3])]) 17 | if primitive == "Polygon": 18 | self.surfaces.add("Face", [Polygon([V[0], V[1], V[2], V[3], V[4]]), Triangle(V[0], V[1], V[2])]) 19 | self.vertices = V 20 | return Solid(position=Vector(0, 0, 0), vertices=self.vertices, surfaces=self.surfaces, primitive=primitive) 21 | 22 | def testGivenNewMayaviSolidWithTrianglePrimitive_shouldExtractMayaviTriangleMeshFromSolid(self): 23 | solid = self.createSimpleSolid() 24 | mayaviSolid = MayaviSolid(solid) 25 | 26 | x, y, z, polygonIndices = mayaviSolid.triangleMesh.components 27 | self.assertTrue(len(x) == len(y) == len(z)) 28 | self.assertEqual(len(solid.getPolygons()), len(polygonIndices)) 29 | self.assertEqual((0, 2, 3), polygonIndices[1]) 30 | 31 | def testGivenNewMayaviSolidWithQuadPrimitive_shouldExtractMayaviTriangleMeshFromSolid(self): 32 | solid = self.createSimpleSolid(primitive=primitives.QUAD) 33 | mayaviSolid = MayaviSolid(solid) 34 | 35 | x, y, z, polygonIndices = mayaviSolid.triangleMesh.components 36 | self.assertTrue(len(x) == len(y) == len(z)) 37 | self.assertEqual(2 * len(solid.getPolygons()), len(polygonIndices)) 38 | self.assertEqual((0, 2, 3), polygonIndices[1]) 39 | 40 | def testGivenNewMayaviSolidWithArbitraryPolygonPrimitives_shouldExtractMayaviTriangleMeshFromSolid(self): 41 | solid = self.createSimpleSolid(primitive="Polygon") 42 | mayaviSolid = MayaviSolid(solid) 43 | 44 | x, y, z, polygonIndices = mayaviSolid.triangleMesh.components 45 | self.assertTrue(len(x) == len(y) == len(z)) 46 | self.assertEqual(4, len(polygonIndices)) 47 | self.assertEqual((0, 2, 3), polygonIndices[1]) 48 | 49 | def testGivenNewMayaviSolidWithLoadNormals_shouldExtractMayaviNormalsFromSolid(self): 50 | solid = self.createSimpleSolid() 51 | mayaviSolid = MayaviSolid(solid, loadNormals=True) 52 | 53 | x, y, z, u, v, w = mayaviSolid.normals.components 54 | self.assertEqual(len(solid.getPolygons()), len(x)) 55 | self.assertTrue(len(x) == len(y) == len(z) == len(u) == len(v) == len(w)) 56 | 57 | for i, polygon in enumerate(self.surfaces.getPolygons()): 58 | self.assertEqual([x[i], y[i], z[i]], polygon.getCentroid().array) 59 | self.assertEqual([u[i], v[i], w[i]], polygon.normal.array) 60 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/__init__.py: -------------------------------------------------------------------------------- 1 | from .node import Node 2 | from .spacePartition import SpacePartition 3 | from .treeConstructor.treeConstructor import TreeConstructor 4 | 5 | __all__ = ["Node", "SpacePartition", "TreeConstructor"] 6 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/node.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pytissueoptics.scene.geometry import BoundingBox, Polygon 4 | 5 | 6 | class Node: 7 | def __init__(self, parent: "Node" = None, polygons: List[Polygon] = None, bbox: BoundingBox = None, depth: int = 0): 8 | self._parent = parent 9 | self._children = [] 10 | self._polygons = polygons 11 | self._bbox = bbox 12 | self._depth = depth 13 | 14 | @property 15 | def children(self) -> List["Node"]: 16 | return self._children 17 | 18 | @property 19 | def isRoot(self) -> bool: 20 | if self._parent is None: 21 | return True 22 | else: 23 | return False 24 | 25 | @property 26 | def isLeaf(self) -> bool: 27 | if not self._children: 28 | return True 29 | else: 30 | return False 31 | 32 | @property 33 | def polygons(self) -> List[Polygon]: 34 | return self._polygons 35 | 36 | @property 37 | def depth(self) -> int: 38 | return self._depth 39 | 40 | @property 41 | def bbox(self) -> BoundingBox: 42 | return self._bbox 43 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/treeConstructor/__init__.py: -------------------------------------------------------------------------------- 1 | from .splitNodeResult import SplitNodeResult 2 | from .treeConstructor import TreeConstructor 3 | 4 | __all__ = ["SplitNodeResult", "TreeConstructor"] 5 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/treeConstructor/binary/__init__.py: -------------------------------------------------------------------------------- 1 | from .noSplitOneAxisConstructor import NoSplitOneAxisConstructor 2 | from .noSplitThreeAxesConstructor import NoSplitThreeAxesConstructor 3 | from .sahSearchResult import SAHSearchResult 4 | from .splitTreeAxesConstructor import SplitThreeAxesConstructor 5 | 6 | __all__ = ["NoSplitOneAxisConstructor", "NoSplitThreeAxesConstructor", "SplitThreeAxesConstructor", "SAHSearchResult"] 7 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/treeConstructor/binary/noSplitThreeAxesConstructor.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from pytissueoptics.scene.tree import Node 4 | from pytissueoptics.scene.tree.treeConstructor import SplitNodeResult 5 | from pytissueoptics.scene.tree.treeConstructor.binary.noSplitOneAxisConstructor import NoSplitOneAxisConstructor 6 | 7 | 8 | class NoSplitThreeAxesConstructor(NoSplitOneAxisConstructor): 9 | def _splitNode(self, node: Node) -> SplitNodeResult: 10 | self.currentNode = node 11 | splitBbox = self.currentNode.bbox.copy() 12 | minSAH = sys.float_info.max 13 | for axis in ["x", "y", "z"]: 14 | thisSAH = self._searchMinSAHOnAxis(splitBbox, axis, minSAH) 15 | if thisSAH < minSAH: 16 | minSAH = thisSAH 17 | self.result.leftPolygons.extend(self.result.splitPolygons) 18 | self.result.rightPolygons.extend(self.result.splitPolygons) 19 | self._trimChildrenBbox() 20 | stopCondition = self._checkStopCondition() 21 | newNodeResult = SplitNodeResult( 22 | stopCondition, 23 | [self.result.leftBbox, self.result.rightBbox], 24 | [self.result.leftPolygons, self.result.rightPolygons], 25 | ) 26 | return newNodeResult 27 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/treeConstructor/binary/sahSearchResult.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | from pytissueoptics.scene.geometry import BoundingBox, Polygon 5 | 6 | 7 | @dataclass 8 | class SAHSearchResult: 9 | leftPolygons: List[Polygon] 10 | rightPolygons: List[Polygon] 11 | splitPolygons: List[Polygon] 12 | leftBbox: BoundingBox 13 | rightBbox: BoundingBox 14 | splitAxis: str 15 | splitValue: float 16 | 17 | @property 18 | def SAH(self): 19 | return self.leftSAH + self.rightSAH 20 | 21 | @property 22 | def rightSAH(self): 23 | return self.nRight * self.rightBbox.getArea() 24 | 25 | @property 26 | def leftSAH(self): 27 | return self.nLeft * self.leftBbox.getArea() 28 | 29 | @property 30 | def nLeft(self): 31 | return len(self.leftPolygons) 32 | 33 | @property 34 | def nRight(self): 35 | return len(self.rightPolygons) 36 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/treeConstructor/splitNodeResult.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import List 3 | 4 | from pytissueoptics.scene.geometry import BoundingBox, Polygon 5 | 6 | 7 | @dataclass 8 | class SplitNodeResult: 9 | stopCondition: bool 10 | groupsBbox: List[BoundingBox] 11 | polygonGroups: List[List[Polygon]] 12 | -------------------------------------------------------------------------------- /pytissueoptics/scene/tree/treeConstructor/treeConstructor.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.scene.tree import Node 2 | from pytissueoptics.scene.tree.treeConstructor import SplitNodeResult 3 | 4 | 5 | class TreeConstructor: 6 | EPSILON = 1e-6 7 | 8 | def _splitNode(self, node: Node) -> SplitNodeResult: 9 | raise NotImplementedError() 10 | 11 | def constructTree(self, node: Node, maxDepth: int, minLeafSize: int): 12 | if node.depth >= maxDepth or len(node.polygons) <= minLeafSize: 13 | return 14 | 15 | splitNodeResult = self._splitNode(node) 16 | if splitNodeResult.stopCondition: 17 | return 18 | 19 | for i, polygonGroup in enumerate(splitNodeResult.polygonGroups): 20 | if len(polygonGroup) <= 0: 21 | continue 22 | childNode = Node( 23 | parent=node, polygons=polygonGroup, bbox=splitNodeResult.groupsBbox[i], depth=node.depth + 1 24 | ) 25 | node.children.append(childNode) 26 | self.constructTree(childNode, maxDepth, minLeafSize) 27 | -------------------------------------------------------------------------------- /pytissueoptics/scene/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .progressBar import noProgressBar, progressBar 2 | 3 | __all__ = ["noProgressBar", "progressBar"] 4 | -------------------------------------------------------------------------------- /pytissueoptics/scene/utils/progressBar.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | 4 | def noProgressBar(iterable, *args, **kwargs): 5 | warnings.warn("Package 'tqdm' not found. Progress bar will not be shown.") 6 | return iterable 7 | 8 | 9 | try: 10 | from tqdm import tqdm as progressBar 11 | except ImportError: 12 | progressBar = noProgressBar 13 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/__init__.py: -------------------------------------------------------------------------------- 1 | from .abstract3DViewer import Abstract3DViewer 2 | from .displayable import Displayable 3 | from .provider import get3DViewer 4 | from .viewPoint import ViewPointStyle 5 | 6 | __all__ = ["Displayable", "get3DViewer", "Abstract3DViewer", "ViewPointStyle"] 7 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/abstract3DViewer.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | import numpy as np 4 | 5 | from pytissueoptics.scene.solids import Solid 6 | 7 | from .viewPoint import ViewPointStyle 8 | 9 | 10 | class Abstract3DViewer: 11 | @abstractmethod 12 | def setViewPointStyle(self, viewPointStyle: ViewPointStyle): ... 13 | 14 | @abstractmethod 15 | def add( 16 | self, 17 | *solids: Solid, 18 | representation="wireframe", 19 | lineWidth=0.25, 20 | showNormals=False, 21 | normalLength=0.3, 22 | colormap="viridis", 23 | reverseColormap=False, 24 | colorWithPosition=False, 25 | opacity=1, 26 | **kwargs, 27 | ): ... 28 | 29 | @abstractmethod 30 | def addDataPoints( 31 | self, 32 | dataPoints: np.ndarray, 33 | colormap="rainbow", 34 | reverseColormap=False, 35 | scale=0.15, 36 | scaleWithValue=True, 37 | asSpheres=True, 38 | ): 39 | """'dataPoints' has to be of shape (n, 4) where the second axis is (value, x, y, z).""" 40 | ... 41 | 42 | @abstractmethod 43 | def addImage( 44 | self, 45 | image: np.ndarray, 46 | size: tuple = None, 47 | minCorner: tuple = (0, 0), 48 | axis: int = 2, 49 | position: float = 0, 50 | colormap: str = "viridis", 51 | ): ... 52 | 53 | @staticmethod 54 | @abstractmethod 55 | def showVolumeSlicer(hist3D: np.ndarray, colormap: str = "viridis", interpolate=False, **kwargs): ... 56 | 57 | @abstractmethod 58 | def show(self): ... 59 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/displayable.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from .provider import get3DViewer 4 | 5 | 6 | class Displayable: 7 | @abstractmethod 8 | def addToViewer(self, viewer, **kwargs): 9 | pass 10 | 11 | def show(self, **kwargs): 12 | viewer = get3DViewer() 13 | self.addToViewer(viewer, **kwargs) 14 | viewer.show() 15 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/mayavi/__init__.py: -------------------------------------------------------------------------------- 1 | from .mayaviSolid import MayaviObject, MayaviSolid 2 | from .mayaviTriangleMesh import MayaviTriangleMesh 3 | 4 | __all__ = [ 5 | "MayaviObject", 6 | "MayaviSolid", 7 | "MayaviTriangleMesh", 8 | ] 9 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/mayavi/mayaviNormals.py: -------------------------------------------------------------------------------- 1 | from pytissueoptics.scene.geometry import Vector 2 | 3 | 4 | class MayaviNormals: 5 | def __init__(self): 6 | self._x = [] 7 | self._y = [] 8 | self._z = [] 9 | 10 | self._u = [] 11 | self._v = [] 12 | self._w = [] 13 | 14 | def add(self, position: Vector, direction: Vector): 15 | x, y, z = position.array 16 | self._x.append(x) 17 | self._y.append(y) 18 | self._z.append(z) 19 | 20 | u, v, w = direction.array 21 | self._u.append(u) 22 | self._v.append(v) 23 | self._w.append(w) 24 | 25 | @property 26 | def components(self) -> tuple: 27 | return self._x, self._y, self._z, self._u, self._v, self._w 28 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/mayavi/mayaviSolid.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from pytissueoptics.scene.geometry import Polygon, Vertex, primitives 4 | from pytissueoptics.scene.solids import Solid 5 | 6 | from .mayaviNormals import MayaviNormals 7 | from .mayaviTriangleMesh import MayaviTriangleMesh 8 | 9 | 10 | class MayaviObject: 11 | def __init__( 12 | self, vertices: List[Vertex], polygons: List[Polygon], loadNormals=True, primitive=primitives.TRIANGLE 13 | ): 14 | self._vertices = vertices 15 | self._polygons = polygons 16 | self._primitive = primitive 17 | self._loadNormals = loadNormals 18 | 19 | self._x = [] 20 | self._y = [] 21 | self._z = [] 22 | self._polygonsIndices: List[Tuple[int]] = [] 23 | self._normals = MayaviNormals() 24 | 25 | self._create() 26 | 27 | @property 28 | def primitive(self) -> str: 29 | return self._primitive 30 | 31 | def _create(self): 32 | self._separateXYZ() 33 | self._findPolygonIndices() 34 | 35 | def _separateXYZ(self): 36 | for vertex in self._vertices: 37 | self._x.append(vertex.x) 38 | self._y.append(vertex.y) 39 | self._z.append(vertex.z) 40 | 41 | def _findPolygonIndices(self): 42 | vertexToIndex = {} 43 | for i, vertex in enumerate(self._vertices): 44 | vertexToIndex[id(vertex)] = i 45 | 46 | for polygon in self._polygons: 47 | polygonIndices = [] 48 | for vertex in polygon.vertices: 49 | index = vertexToIndex[id(vertex)] 50 | polygonIndices.append(index) 51 | self._polygonsIndices.append(tuple(polygonIndices)) 52 | 53 | if self._loadNormals: 54 | self._normals.add(polygon.getCentroid(), polygon.normal) 55 | 56 | @property 57 | def triangleMesh(self) -> MayaviTriangleMesh: 58 | return MayaviTriangleMesh(self._x, self._y, self._z, self._getTriangleIndices()) 59 | 60 | def _getTriangleIndices(self): 61 | if self.primitive == primitives.TRIANGLE: 62 | return self._polygonsIndices 63 | else: 64 | trianglesIndices = [] 65 | for polygonIndices in self._polygonsIndices: 66 | for i in range(len(polygonIndices) - 2): 67 | trianglesIndices.append((polygonIndices[0], polygonIndices[i + 1], polygonIndices[i + 2])) 68 | return trianglesIndices 69 | 70 | @property 71 | def normals(self) -> MayaviNormals: 72 | return self._normals 73 | 74 | 75 | class MayaviSolid(MayaviObject): 76 | def __init__(self, solid: Solid, loadNormals=True): 77 | super().__init__(solid.vertices, solid.getPolygons(), loadNormals, solid.primitive) 78 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/mayavi/mayaviTriangleMesh.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | 4 | class MayaviTriangleMesh: 5 | def __init__(self, x: List[float], y: List[float], z: List[float], triangleIndices: List[Tuple[int]]): 6 | self._x = x 7 | self._y = y 8 | self._z = z 9 | self._triangleIndices = triangleIndices 10 | 11 | @property 12 | def components(self): 13 | return self._x, self._y, self._z, self._triangleIndices 14 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/null3DViewer.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | 3 | import numpy as np 4 | from scene.solids import Solid 5 | from scene.viewer.abstract3DViewer import Abstract3DViewer 6 | 7 | from pytissueoptics import ViewPointStyle 8 | 9 | 10 | class Null3DViewer(Abstract3DViewer): 11 | def setViewPointStyle(self, viewPointStyle: ViewPointStyle): 12 | pass 13 | 14 | def add( 15 | self, 16 | *solids: Solid, 17 | representation="wireframe", 18 | lineWidth=0.25, 19 | showNormals=False, 20 | normalLength=0.3, 21 | colormap="viridis", 22 | reverseColormap=False, 23 | colorWithPosition=False, 24 | opacity=1, 25 | **kwargs, 26 | ): 27 | pass 28 | 29 | def addDataPoints( 30 | self, 31 | dataPoints: np.ndarray, 32 | colormap="rainbow", 33 | reverseColormap=False, 34 | scale=0.15, 35 | scaleWithValue=True, 36 | asSpheres=True, 37 | ): 38 | pass 39 | 40 | def addImage( 41 | self, 42 | image: np.ndarray, 43 | size: tuple = None, 44 | minCorner: tuple = (0, 0), 45 | axis: int = 2, 46 | position: float = 0, 47 | colormap: str = "viridis", 48 | ): 49 | pass 50 | 51 | @staticmethod 52 | def showVolumeSlicer(hist3D: np.ndarray, colormap: str = "viridis", interpolate=False, **kwargs): 53 | warnings.warn("Attempting to show a volume slicer with a Null3DViewer. No action will be taken.") 54 | 55 | def show(self): 56 | warnings.warn("Attempting to show a Null3DViewer. No action will be taken.") 57 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | from .abstract3DViewer import Abstract3DViewer 5 | 6 | AVAILABLE_BACKENDS = ("mayavi", "null") 7 | 8 | 9 | def get3DViewer() -> Abstract3DViewer: 10 | backend = os.environ.get("PTO_3D_BACKEND", "mayavi").lower() 11 | if backend == "mayavi": 12 | try: 13 | from .mayavi.mayavi3DViewer import Mayavi3DViewer 14 | 15 | return Mayavi3DViewer() 16 | except Exception as e: 17 | warnings.warn( 18 | "Mayavi is not available. Falling back to a null 3D viewer. Fix the following error to use the Mayavi " 19 | "backend or select another backend by setting the PTO_3D_BACKEND environment variable (available " 20 | f"backends: {AVAILABLE_BACKENDS}). \n{e}" 21 | ) 22 | from .null3DViewer import Null3DViewer 23 | 24 | return Null3DViewer() 25 | elif backend == "null": 26 | from .null3DViewer import Null3DViewer 27 | 28 | return Null3DViewer() 29 | else: 30 | raise ValueError(f"Invalid backend '{backend}'. Available backends: {AVAILABLE_BACKENDS}") 31 | -------------------------------------------------------------------------------- /pytissueoptics/scene/viewer/viewPoint.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from enum import Enum 3 | from typing import Optional 4 | 5 | import numpy as np 6 | 7 | 8 | @dataclass 9 | class ViewPoint: 10 | azimuth: Optional[float] 11 | elevation: Optional[float] 12 | distance: Optional[float] 13 | focalpoint: Optional[np.ndarray] 14 | roll: Optional[float] 15 | 16 | 17 | class ViewPointStyle(Enum): 18 | OPTICS = 0 19 | NATURAL = 1 20 | NATURAL_FRONT = 2 21 | 22 | 23 | class ViewPointFactory: 24 | def create(self, viewPointStyle: ViewPointStyle): 25 | if viewPointStyle == ViewPointStyle.OPTICS: 26 | return self.getOpticsViewPoint() 27 | elif viewPointStyle == ViewPointStyle.NATURAL: 28 | return self.getNaturalViewPoint() 29 | elif viewPointStyle == ViewPointStyle.NATURAL_FRONT: 30 | return self.getNaturalFrontViewPoint() 31 | else: 32 | raise ValueError(f"Invalid viewpoint style: {viewPointStyle}") 33 | 34 | @staticmethod 35 | def getOpticsViewPoint(): 36 | return ViewPoint(azimuth=-30, elevation=215, distance=None, focalpoint=None, roll=0) 37 | 38 | @staticmethod 39 | def getNaturalViewPoint(): 40 | return ViewPoint(azimuth=30, elevation=30, distance=None, focalpoint=None, roll=0) 41 | 42 | @staticmethod 43 | def getNaturalFrontViewPoint(): 44 | return ViewPoint(azimuth=0, elevation=0, distance=None, focalpoint=None, roll=None) 45 | -------------------------------------------------------------------------------- /pytissueoptics/testExamples.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import os 3 | import re 4 | import unittest 5 | 6 | from pytissueoptics.examples import EXAMPLE_DIR, EXAMPLE_FILE_PATTERN, EXAMPLE_FILES, EXAMPLE_MODULE, loadExamples 7 | 8 | 9 | class TestExamples(unittest.TestCase): 10 | def testExampleFormat(self): 11 | self.assertTrue(len(EXAMPLE_FILES) > 0) 12 | for file in EXAMPLE_FILES: 13 | name = re.match(EXAMPLE_FILE_PATTERN, file).group(1) 14 | module = importlib.import_module(f"pytissueoptics.examples.{EXAMPLE_MODULE}.{name}") 15 | with open(os.path.join(EXAMPLE_DIR, file), "r") as f: 16 | srcCode = f.read() 17 | with self.subTest(name): 18 | self.assertTrue(hasattr(module, "TITLE")) 19 | self.assertTrue(hasattr(module, "DESCRIPTION")) 20 | self.assertTrue(hasattr(module, "exampleCode")) 21 | self.assertTrue(srcCode.startswith("import env")) 22 | self.assertTrue(srcCode.endswith('if __name__ == "__main__":\n' + " exampleCode()\n")) 23 | 24 | def testLoadExamples(self): 25 | allExamples = loadExamples() 26 | self.assertTrue(len(allExamples) > 0) 27 | for example in allExamples: 28 | with self.subTest(example.name): 29 | self.assertTrue(example.name.startswith("ex")) 30 | self.assertTrue(example.title) 31 | self.assertTrue(example.description) 32 | self.assertTrue(example.func) 33 | self.assertTrue(example.sourceCode) 34 | --------------------------------------------------------------------------------