Skip to content

Commit

Permalink
Merge branch 'main' of github.com:BerkeleyLearnVerify/Scenic into aba…
Browse files Browse the repository at this point in the history
…nuelo/code-coverage
  • Loading branch information
Armando Banuelos authored and Armando Banuelos committed Mar 5, 2024
2 parents cc42d24 + 29de7c8 commit 9598868
Show file tree
Hide file tree
Showing 31 changed files with 1,460 additions and 707 deletions.
4 changes: 4 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# .git-blame-ignore-revs
# Ran isort and black on the whole codebase
360c67fab09d172498b3014510ee3658643d12da
# Ran black with 2024 stable style
c6c83f95ff370b75c3ee7130dbd8071bfe8b285a
# Cleaned up test quote spacing
995cd182924dc9e3dbbc941c5b75454ea0cdaaca
29 changes: 0 additions & 29 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,35 +51,6 @@ jobs:
with:
ref: ${{ github.ref }}

- name: Install non-Python dependencies (Linux)
if: ${{ matrix.os == 'ubuntu-latest' }}
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: blender openscad

- name: Restore cached non-Python dependencies (Windows)
id: windows-cache-deps
if: ${{ matrix.os == 'windows-latest' }}
uses: actions/cache@v3
with:
path: downloads
key: windows-deps

- name: Download non-Python dependencies (Windows)
if: ${{ matrix.os == 'windows-latest' && steps.windows-cache-deps.outputs.cache-hit != 'true' }}
run: |
New-Item -Path downloads -ItemType Directory -Force
Invoke-WebRequest https://github.com/openscad/openscad/releases/download/openscad-2021.01/OpenSCAD-2021.01-x86-64.zip -O downloads/openscad.zip
Invoke-WebRequest https://download.blender.org/release/Blender3.6/blender-3.6.0-windows-x64.zip -O downloads/blender.zip
- name: Install non-Python dependencies (Windows)
if: ${{ matrix.os == 'windows-latest' }}
run: |
Expand-Archive -Path downloads/openscad.zip -DestinationPath openscad
Move-Item -Path openscad/openscad-2021.01 -Destination $Env:Programfiles\OpenSCAD
Expand-Archive -Path downloads/blender.zip -DestinationPath blender
Move-Item -Path blender/blender-3.6.0-windows-x64 -Destination "$Env:Programfiles\Blender Foundation\Blender"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
Expand Down
2 changes: 1 addition & 1 deletion docs/_templates/installation.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Next, activate the `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ in which you want to install Scenic.
Activate the `virtual environment <https://docs.python.org/3/tutorial/venv.html>`_ in which you want to install Scenic.
To create and activate a new virtual environment called :file:`venv`, you can run the following commands:

.. venv-setup-start
Expand Down
15 changes: 15 additions & 0 deletions docs/developing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,21 @@ If you're running the test suite on a headless server or just want to stop windo
popping up during testing, use the :command:`--no-graphics` option to skip graphical
tests.

Prior to finalizing a PR or other substantial changes, it's a good idea to run the test suite under all major versions of Python that Scenic supports, in fresh virtual environments.
You can do this automatically with the command :command:`tox`, which by default will test all supported major versions both with and without optional dependencies (this will take a long time).
Some variations:

* :command:`tox -p` will run the various combinations in parallel.

* :command:`tox -m basic` skips testing installations with the optional dependencies.

* :command:`tox -- --fast` only runs the "fast" tests. In general, any arguments after the :command:`--` will get passed to ``pytest``. For example,

* :command:`tox -- tests/syntax/test_specifiers.py` only runs the tests in the given file.

See the `Tox <https://tox.wiki/>`_ website for more information about the available options and how to configure Tox.


.. _debugging:

Debugging
Expand Down
14 changes: 4 additions & 10 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,23 @@ If you encounter any errors, please see our :doc:`install_notes` for suggestions

.. tab:: macOS

Start by downloading `Blender <https://www.blender.org/download/>`__ and `OpenSCAD <https://openscad.org/downloads.html>`__ and installing them into your :file:`Applications` directory.

.. include:: _templates/installation.rst

.. tab:: Linux

Start by installing the Python-Tk interface, Blender, and OpenSCAD.
Start by installing the Python-Tk interface.
You can likely use your system's package manager; e.g. on Debian/Ubuntu run:

.. code-block:: text
sudo apt-get install python3-tk blender openscad
For other Linux distributions or if you need to install from source, see the download pages for `Blender <https://www.blender.org/download/>`__ and `OpenSCAD <https://openscad.org/downloads.html>`__.
sudo apt-get install python3-tk
.. include:: _templates/installation.rst

.. tab:: Windows

These instructions cover installing Scenic natively on Windows; if you are using the `Windows Subsystem for Linux <https://docs.microsoft.com/en-us/windows/wsl/install-win10>`_ (on Windows 10 and newer), see the WSL tab instead.

Start by downloading and running the installers for `Blender <https://www.blender.org/download/>`__ and `OpenSCAD <https://openscad.org/downloads.html>`__.

.. include:: _templates/installation.rst
:end-before: .. venv-setup-start

Expand All @@ -64,12 +58,12 @@ If you encounter any errors, please see our :doc:`install_notes` for suggestions
These instructions cover installing Scenic on the Windows Subsystem for Linux (WSL).

If you haven't already installed WSL, you can do that by running :command:`wsl --install` (in either Command Prompt or PowerShell) and restarting your computer.
Then open a WSL terminal and run the following commands to install Python, the Python-Tk interface, Blender, and OpenSCAD:
Then open a WSL terminal and run the following commands to install Python and the Python-Tk interface:

.. code-block:: text
sudo apt-get update
sudo apt-get install python3 python3-tk blender openscad
sudo apt-get install python3 python3-tk
.. include:: _templates/installation.rst
:end-before: .. venv-setup-start
Expand Down
2 changes: 0 additions & 2 deletions docs/reference/region_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ When checking containment of an `Object` in a 2D region, Scenic will atuomatical

Most 3D regions inherit from either `MeshVolumeRegion` or `MeshSurfaceRegion`, which represent the volume (of a watertight mesh) and the surface of a mesh respectively. Various region classes are also provided to create primitive shapes. `MeshVolumeRegion` can be converted to `MeshSurfaceRegion` (and vice versa) using the the ``getSurfaceRegion`` and ``getVolumeRegion`` methods.

Mesh regions can use one of two engines for mesh operations: Blender or OpenSCAD. This can be controlled using the ``engine`` parameter, passing ``"blender"`` or ``"scad"`` respectively. Blender is generally more tolerant but can produce unreliable output, such as meshes that have microscopic holes. OpenSCAD is generally more precise, but may crash on certain inputs that it considers ill-defined. By default, Scenic uses Blender internally.

PolygonalFootprintRegions represent the :term:`footprint` of a 2D region. See `2D Regions` for more details.

.. autoclass:: scenic.core.regions.MeshVolumeRegion
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ dependencies = [
"dotmap ~= 1.3",
"mapbox_earcut >= 0.12.10",
"matplotlib ~= 3.2",
"manifold3d == 2.3.0",
"networkx >= 2.6",
"numpy ~= 1.24",
"opencv-python ~= 4.5",
Expand All @@ -46,7 +47,7 @@ dependencies = [
"scikit-image ~= 0.21",
"scipy ~= 1.7",
"shapely ~= 2.0",
"trimesh >=3.22.5, <4",
"trimesh >=4.0.9, <5",
]

[project.optional-dependencies]
Expand Down Expand Up @@ -75,11 +76,11 @@ test-full = [ # like 'test' but adds dependencies for optional features
]
dev = [
"scenic[test-full]",
"black ~= 23.0",
"black ~= 24.0",
"isort ~= 5.11",
"pre-commit ~= 3.0",
"pytest-cov >= 3.0.0",
"tox ~= 3.14",
"tox ~= 4.0",
]

[project.urls]
Expand Down
163 changes: 112 additions & 51 deletions src/scenic/core/object_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import trimesh

from scenic.core.distributions import (
FunctionDistribution,
MultiplexerDistribution,
RandomControlFlowError,
Samplable,
Expand Down Expand Up @@ -869,9 +870,11 @@ class OrientedPoint(Point):
"heading": PropertyDefault(
{"orientation"},
{"dynamic", "final"},
lambda self: self.yaw
if alwaysGlobalOrientation(self.parentOrientation)
else self.orientation.yaw,
lambda self: (
self.yaw
if alwaysGlobalOrientation(self.parentOrientation)
else self.orientation.yaw
),
),
"viewAngle": math.tau, # Primarily for backwards compatibility. Set viewAngles instead.
"viewAngles": PropertyDefault(
Expand Down Expand Up @@ -1328,67 +1331,125 @@ def boundingBox(self):
@cached_property
def inradius(self):
"""A lower bound on the inradius of this object"""
# First check if all needed variables are defined. If so, we can
# compute the inradius exactly.
width, length, height = self.width, self.length, self.height
shape = self.shape
if not any(needsSampling(val) for val in (width, length, height, shape)):
shapeRegion = MeshVolumeRegion(
mesh=shape.mesh, dimensions=(width, length, height)
)
return shapeRegion.inradius

# If we havea uniform distribution over shapes and a supportInterval for each dimension,
# we can compute a supportInterval for this object's inradius
# Define a helper function that computes the support of the inradius,
# given the sub supports.
def inradiusSupport(width_s, length_s, height_s, shape_s):
# Unpack the dimension supports (and ignore the shape support)
min_width, max_width = width_s
min_length, max_length = length_s
min_height, max_height = height_s

if None in [
min_width,
max_width,
min_length,
max_length,
min_height,
max_height,
]:
# Can't get a bound on one or more dimensions, abort
return None, None

min_bounds = np.array([min_width, min_length, min_height])
max_bounds = np.array([max_width, max_length, max_height])

# Extract a list of possible shapes
if isinstance(self.shape, Shape):
shapes = [self.shape]
elif isinstance(self.shape, MultiplexerDistribution) and all(
isinstance(opt, Shape) for opt in self.shape.options
):
shapes = self.shape.options
else:
# Something we don't recognize, abort
return None, None

# Define helper class
class InradiusHelper:
def __init__(self, support):
self.support = support
# Get the inradius for each shape with the min and max bounds
min_distances = [
MeshVolumeRegion(mesh=shape.mesh, dimensions=min_bounds).inradius
for shape in shapes
]
max_distances = [
MeshVolumeRegion(mesh=shape.mesh, dimensions=max_bounds).inradius
for shape in shapes
]

def supportInterval(self):
return self.support
distance_range = (min(min_distances), max(max_distances))

# Extract bounds on all dimensions
min_width, max_width = supportInterval(width)
min_length, max_length = supportInterval(length)
min_height, max_height = supportInterval(height)
return distance_range

if None in [min_width, max_width, min_length, max_length, min_height, max_height]:
# Can't get a bound on one or more dimensions, abort
return 0
# Define a helper function that computes the actual inradius
@distributionFunction(support=inradiusSupport)
def inradiusActual(width, length, height, shape):
return MeshVolumeRegion(
mesh=shape.mesh, dimensions=(width, length, height)
).inradius

min_bounds = np.array([min_width, min_length, min_height])
max_bounds = np.array([max_width, max_length, max_height])
# Return the inradius (possibly a distribution) with proper support information
return inradiusActual(self.width, self.length, self.height, self.shape)

# Extract a list of possible shapes
if isinstance(shape, Shape):
shapes = [shape]
elif isinstance(shape, MultiplexerDistribution):
if all(isinstance(opt, Shape) for opt in shape.options):
shapes = shape.options
@cached_property
def planarInradius(self):
"""A lower bound on the planar inradius of this object.
This is defined as the inradius of the polygon of the occupiedSpace
of this object projected into the XY plane, assuming that pitch and
roll are both 0.
"""

# Define a helper function that computes the support of the inradius,
# given the sub supports.
def planarInradiusSupport(width_s, length_s, shape_s):
# Unpack the dimension supports (and ignore the shape support)
min_width, max_width = width_s
min_length, max_length = length_s

if None in [min_width, max_width, min_length, max_length]:
# Can't get a bound on one or more dimensions, abort
return None, None

min_bounds = np.array([min_width, min_length, 1])
max_bounds = np.array([max_width, max_length, 1])

# Extract a list of possible shapes
if isinstance(self.shape, Shape):
shapes = [self.shape]
elif isinstance(self.shape, MultiplexerDistribution) and all(
isinstance(opt, Shape) for opt in self.shape.options
):
shapes = self.shape.options
else:
# Something we don't recognize, abort
return 0
return None, None

# Get the inradius of the projected for each shape with the min and max bounds
min_distances = [
MeshVolumeRegion(
mesh=shape.mesh, dimensions=min_bounds
).boundingPolygon.inradius
for shape in shapes
]
max_distances = [
MeshVolumeRegion(
mesh=shape.mesh, dimensions=max_bounds
).boundingPolygon.inradius
for shape in shapes
]

# Check that all possible shapes contain the origin
if not all(shape.containsCenter for shape in shapes):
# One or more shapes has inradius 0
return 0
distance_range = (min(min_distances), max(max_distances))

# Get the inradius for each shape with the min and max bounds
min_distances = [
MeshVolumeRegion(mesh=shape.mesh, dimensions=min_bounds).inradius
for shape in shapes
]
max_distances = [
MeshVolumeRegion(mesh=shape.mesh, dimensions=max_bounds).inradius
for shape in shapes
]
return distance_range

distance_range = (min(min_distances), max(max_distances))
# Define a helper function that computes the actual planarInradius
@distributionFunction(support=planarInradiusSupport)
def planarInradiusActual(width, length, shape):
return MeshVolumeRegion(
mesh=shape.mesh, dimensions=(width, length, 1)
).boundingPolygon.inradius

return InradiusHelper(support=distance_range)
# Return the planar inradius (possibly a distribution) with proper support information
return planarInradiusActual(self.width, self.length, self.shape)

@cached_property
def surface(self):
Expand Down
Loading

0 comments on commit 9598868

Please sign in to comment.