Skip to content

Commit

Permalink
Significant cleanup x 6.
Browse files Browse the repository at this point in the history
This commit is the next in a commit chain quietly resurrecting BETSE
for a modern audience and the modern Python ecosystem. Specifically,
this commit:

* Defines a new
  `betse.util.math.mathoper.det1d()` function performing the
  one-dimensional determinant previously performed by the
  `numpy.linalg.cross()` function, which no longer supports the
  one-dimensional determinant for some inane reason. (We sigh.)
* Unit tests that `betse.util.math.mathoper.det1d()` function.
* Silences the horrifying `pytest-asyncio` plugin once and for all. Grr!
* Finishes unwinding (i.e., permanently removing) a mandatory runtime
  dependency on the deprecated `setuptools.pkg_resources` subpackage. In
  theory, no functionality that matters should import that subpackage.
* Resolved a variety of erroneous type hints in the core
  `betse.lib.matplotlib.mplfigure` submodule.

(*Master of the blasters!*)
  • Loading branch information
leycec committed Sep 17, 2024
1 parent 2c5fd11 commit 4e5325f
Show file tree
Hide file tree
Showing 14 changed files with 327 additions and 173 deletions.
9 changes: 9 additions & 0 deletions betse/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ class BetsePyDotException(BetseLibException):
pass


class BetseVersionException(BetseLibException):
'''
Version specifier-specific exception applicable to third-party dependency.
versions.
'''

pass


class BetseYamlException(BetseException):
'''
Yet Another Markup Language (YAML)-specific exception.
Expand Down
25 changes: 12 additions & 13 deletions betse/lib/matplotlib/mplfigure.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,44 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright 2014-2025 by Alexis Pietak & Cecil Curry.
# See "LICENSE" for further details.

'''
Low-level :mod:`matplotlib`-specific figure functionality.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To allow matplotlib defaults (e.g., backend, logging) to be replaced
# with application-specific preferences, the unsafe "matplotlib.pyplot"
# submodule must *NOT* be imported here at the top-level.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

from betse.exceptions import BetseMatplotlibException
from betse.util.io.log import logs
from betse.util.type.types import type_check, MatplotlibFigureType

# ....................{ EXCEPTIONS }....................
def die_unless_figure() -> bool:
# ....................{ EXCEPTIONS }....................
def die_unless_figure() -> None:
'''
Raise an exception
``True`` only if one or more figures are currently open with the
Raise an exception if *no* figures are currently open with the
:mod:`matplotlib.pyplot` GCF API.
Raises
-----------
------
BetseMatplotlibException
If no figures are currently open with this API.
See Also
-----------
--------
:func:`is_figure`
Further details.
'''

if not is_figure():
raise BetseMatplotlibException('No matplotlib figures currently open.')

# ....................{ TESTERS }....................
# ....................{ TESTERS }....................
def is_figure() -> bool:
'''
``True`` only if one or more figures are currently open with the
Expand All @@ -54,7 +53,7 @@ def is_figure() -> bool:
# currently open figures. Welcome to Matplotlib Hell, where all is well.
return bool(pyplot.get_fignums())

# ....................{ GETTERS }....................
# ....................{ GETTERS }....................
def get_figure_current() -> MatplotlibFigureType:
'''
Figure most recently opened with the :mod:`matplotlib.pyplot` GCF API.
Expand All @@ -72,7 +71,7 @@ def get_figure_current() -> MatplotlibFigureType:
# Three-letter acronyms do *NOT* constitute a human-readable API.
return pyplot.gcf()

# ....................{ CLOSERS }....................
# ....................{ CLOSERS }....................
@type_check
def close_figure(figure: MatplotlibFigureType) -> None:
'''
Expand Down
2 changes: 1 addition & 1 deletion betse/lib/setuptools/supresource.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def get_pathname(module_name: (str, Requirement), pathname: str) -> str:
:func:`os.path.join`).
Returns
----------
-------
str
Absolute path of this resource.
'''
Expand Down
19 changes: 14 additions & 5 deletions betse/science/math/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
is_convex, is_cyclic_quad, orient_counterclockwise,)
from betse.util.math.geometry.polygon.geopolyconvex import (
clip_counterclockwise)
from betse.util.math.mathoper import cross2d
from betse.util.math.mathoper import det1d
from numpy import array, ndarray
from scipy.spatial import cKDTree, Delaunay

Expand Down Expand Up @@ -924,9 +924,12 @@ def process_voredges(self):
# Finally, go through and calculate mids, len, and tangents of tri_edges, and prepare a mapping between
# each vertices and edges:
for ei, (vi, vj) in enumerate(vor_edges):
# get coordinates associated with each edge
# Coordinates associated with each edge, each defined as a
# 1-dimensional array of only 2 numbers.
vpi = self.vor_verts[vi]
vpj = self.vor_verts[vj]
# print(f'vpi: {repr(vpi)}')
# print(f'vpj: {repr(vpj)}')

tan_v = vpj - vpi

Expand Down Expand Up @@ -962,10 +965,16 @@ def process_voredges(self):
self.vor_edge_len = vor_edge_len[map_vedge_to_tedge]
self.vor_tang = vor_tang[map_vedge_to_tedge]

# Finally, need to correct the orientation of the voronoi edges to make them all 90 degree
# rotations of the tri mesh:
# Finally, need to correct the orientation of the voronoi edges to make
# them all 90 degree rotations of the tri mesh:
for ei, (vti, tti) in enumerate(zip(self.vor_tang, self.tri_tang)):
sign = np.sign(cross2d(tti, vti))
# print(f'ei: {repr(ei)}')
# print(f'tti: {repr(tti)}')
# print(f'vti: {repr(vti)}')
# print(f'vor_tang: {repr(vor_tang)}')
# print(f'vor_tang (mapped): {repr(self.vor_tang)}')
# print(f'tri_tang (mapped): {repr(self.tri_tang)}')
sign = np.sign(det1d(tti, vti))

if sign == 1.0:
self.vor_tang[ei] = -vti
Expand Down
31 changes: 17 additions & 14 deletions betse/util/app/apppath.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
# --------------------( LICENSE )--------------------
# --------------------( LICENSE )--------------------
# Copyright 2014-2025 by Alexis Pietak & Cecil Curry.
# See "LICENSE" for further details.

Expand All @@ -9,21 +9,21 @@
on the local filesystem) hierarchy.
'''

# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ....................{ IMPORTS }....................
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# WARNING: To avoid race conditions during setuptools-based installation, this
# module may import *ONLY* from modules guaranteed to exist at the start of
# installation. This includes all standard Python and application modules but
# *NOT* third-party dependencies, which if currently uninstalled will only be
# installed at some later time in the installation. Likewise, to avoid circular
# import dependencies, the top-level of this module should avoid importing
# application modules where feasible.
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# from betse.util.io.log import logs
from betse.util.type.types import type_check, ModuleOrStrTypes

# ....................{ GETTERS }....................
# ....................{ GETTERS }....................
@type_check
def get_pathname(package: ModuleOrStrTypes, pathname: str) -> str:
'''
Expand Down Expand Up @@ -87,7 +87,7 @@ def get_pathname(package: ModuleOrStrTypes, pathname: str) -> str:
'''

# Avoid circular import dependencies.
from betse.lib.setuptools import supresource
# from betse.lib.setuptools import supresource
from betse.util.path import pathnames, paths
from betse.util.py import pyfreeze
from betse.util.py.module import pymodule
Expand All @@ -98,8 +98,8 @@ def get_pathname(package: ModuleOrStrTypes, pathname: str) -> str:
# If this pathname is absolute rather than relative, raise an exception.
pathnames.die_if_absolute(pathname)

# Name of this package.
package_name = pymodule.get_name_qualified(package)
# # Name of this package.
# package_name = pymodule.get_name_qualified(package)

# If this application is frozen by PyInstaller, canonicalize this path
# relative to the directory to which this application is unfrozen.
Expand All @@ -111,12 +111,15 @@ def get_pathname(package: ModuleOrStrTypes, pathname: str) -> str:
#this can be generalized. If this cannot be generalized, then some
#other means will be needed to achieve the same or a similar effect.
app_pathname = pathnames.join(app_frozen_dirname, pathname)
# Else if this application is a setuptools-installed script wrapper,
# canonicalize this path by deferring to the setuptools resource API.
elif supresource.is_dir(
module_name=package_name, dirname=pathname):
app_pathname = supresource.get_pathname(
module_name=package_name, pathname=pathname)
#FIXME: It's unclear exactly what the Hatch-specific equivalent to a
#setuptools-specific "resource" is or even if there *IS* an equivalent. For
#now, let's deprecate all "supresource" usage entirely. *sigh*
# # Else if this application is a setuptools-installed script wrapper,
# # canonicalize this path by deferring to the setuptools resource API.
# elif supresource.is_dir(
# module_name=package_name, dirname=pathname):
# app_pathname = supresource.get_pathname(
# module_name=package_name, pathname=pathname)
# Else, the current application is either a setuptools-symlinked script
# wrapper *OR* was invoked via the secretive "python3 -m betse"
# command. In either case, this directory's path is directly obtainable
Expand Down
49 changes: 47 additions & 2 deletions betse/util/math/mathoper.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,53 @@
'''

# ....................{ IMPORTS }....................
from betse.util.type.typehints import NDArrayNdim1, NDArrayNdim2
from betse.util.type.typehints import (
NDArrayNdim1,
NDArrayNdim1Size2,
NDArrayNdim2,
)
from numbers import Number

# ....................{ INTERPOLATORS }....................
# ....................{ OPERATIONS ~ 1d }....................
def det1d(arr1: NDArrayNdim1Size2, arr2: NDArrayNdim1Size2) -> Number:
'''
Scalar determinant of two one-dimensional input **2-arrays** (i.e., arrays
containing exactly two numbers each).
This function literally computes the following one-liner:
.. code-block:: pycon
>>> import numpy as np
>>> arr1 = np.asarray([1, 2])
>>> arr2 = np.asarray([4, 5])
>>> det1d = arr1[0] * arr2[1] - arr1[1] * arr2[0]
>>> det1d
-3
Caveats
-------
This function is often (but *not* always) called in lieu of the official
:func:`numpy.linalg.det` function, which requires that both of the passed
2-arrays be congealed into a single two-dimensional input array. Weird!
Parameters
----------
arr1: NDArrayNdim2
First 1-dimensional input 2-array to take the determinant of.
arr2: NDArrayNdim2
Second 1-dimensional input 2-array to take the determinant of.
Returns
-------
Number
Scalar determinant of these one-dimensional input 2-arrays.
'''

# Return us up the one-liner for great justice!
return arr1[0] * arr2[1] - arr1[1] * arr2[0]

# ....................{ OPERATIONS ~ 2d }....................
def cross2d(arr1: NDArrayNdim2, arr2: NDArrayNdim2) -> NDArrayNdim1:
'''
One-dimensional cross product of two two-dimensional input arrays.
Expand Down Expand Up @@ -50,4 +94,5 @@ def cross2d(arr1: NDArrayNdim2, arr2: NDArrayNdim2) -> NDArrayNdim1:
One-dimensional cross product of these two-dimensional input arrays.
'''

# Return us up the one-liner bomb.
return arr1[..., 0] * arr2[..., 1] - arr1[..., 1] * arr2[..., 0]
12 changes: 8 additions & 4 deletions betse/util/test/pytest/mark/pytskip.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ def skip_if_requirement(*requirements_str: str):

# Defer heavyweight imports.
from betse.exceptions import BetseLibException
from betse.lib.setuptools import setuptool
from betse.lib.libs import die_unless_runtime_optional
# from betse.lib.setuptools import setuptool
from betse.util.type.text.string import strjoin

# Human-readable message justifying the skipping of this test or fixture.
Expand All @@ -232,7 +233,8 @@ def skip_if_requirement(*requirements_str: str):
# Skip this test if one or more such dependences are satisfiable.
return _skip_unless_callable_raises_exception(
exception_type=BetseLibException,
func=setuptool.die_unless_requirements_str,
func=die_unless_runtime_optional,
# func=setuptool.die_unless_requirements_str,
args=requirements_str,
reason=reason,
)
Expand Down Expand Up @@ -260,12 +262,14 @@ def skip_unless_requirement(*requirements_str: str):

# Defer heavyweight imports.
from betse.exceptions import BetseLibException
from betse.lib.setuptools import setuptool
from betse.lib.libs import die_unless_runtime_optional
# from betse.lib.setuptools import setuptool

# Skip this test if one or more such dependences are unsatisfiable.
return _skip_if_callable_raises_exception(
exception_type=BetseLibException,
func=setuptool.die_unless_requirements_str,
func=die_unless_runtime_optional,
# func=setuptool.die_unless_requirements_str,
args=requirements_str,
)

Expand Down
Loading

0 comments on commit 4e5325f

Please sign in to comment.