diff --git a/setup.cfg b/setup.cfg index 75cbb3b2f..e2adfd252 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = underworld3 -version = 0.98b +version = attr: underworld3.__version__ #[build_ext] #inplace = True @@ -20,6 +20,7 @@ install_requires = pint psutil typing_extensions + package_dir = = src packages = find: diff --git a/src/underworld3/__init__.py b/src/underworld3/__init__.py index 6fe29f59e..8f58abc47 100644 --- a/src/underworld3/__init__.py +++ b/src/underworld3/__init__.py @@ -75,6 +75,10 @@ PETSc.Sys.popErrorHandler() +try: + from ._version import __version__ +except ImportError: + __version__ = "Unknown" # check src/underworld3/_version.py def view(): from IPython.display import Latex, Markdown, display @@ -97,8 +101,11 @@ def view(): from ._var_types import * from .utilities._petsc_tools import * from .utilities._nb_tools import * +from .utilities._utils import auditor -from underworld3.utilities import _api_tools +from .utilities import _api_tools +#from underworld3.utilities import _api_tools +from .utilities._utils import auditor import underworld3.adaptivity import underworld3.coordinates @@ -131,31 +138,6 @@ def view(): _libdirs = _OD() _incdirs = _OD({_np.get_include(): None}) - -# def _is_notebook() -> bool: -# """ -# Function to determine if the python environment is a Notebook or not. - -# Returns 'True' if executing in a notebook, 'False' otherwise - -# Script taken from https://stackoverflow.com/a/39662359/8106122 -# """ - -# try: -# shell = get_ipython().__class__.__name__ -# if shell == "ZMQInteractiveShell": -# return True # Jupyter notebook or qtconsole -# elif shell == "TerminalInteractiveShell": -# return False # Terminal running IPython -# else: -# return False # Other type (?) -# except NameError: -# return False # Probably standard Python interpreter - - -# is_notebook = _is_notebook() - - ## ------------------------------------------------------------- # pdoc3 over-rides. pdoc3 has a strange path-traversal algorithm @@ -175,6 +157,3 @@ def view(): # child class modifications __pdoc__["systems.constitutive_models.Constitutive_Model.Parameters"] = False - - -## Add an options dictionary for arbitrary underworld things diff --git a/src/underworld3/_version.py b/src/underworld3/_version.py new file mode 100644 index 000000000..286d177dd --- /dev/null +++ b/src/underworld3/_version.py @@ -0,0 +1 @@ +__version__ = "0.98.1b" diff --git a/src/underworld3/cython/petsc_generic_snes_solvers.pyx b/src/underworld3/cython/petsc_generic_snes_solvers.pyx index afac030bc..3641210d9 100644 --- a/src/underworld3/cython/petsc_generic_snes_solvers.pyx +++ b/src/underworld3/cython/petsc_generic_snes_solvers.pyx @@ -3,7 +3,7 @@ from xmlrpc.client import Boolean import sympy from sympy import sympify -from typing import Optional, Union +from typing import Optional, Union, TypeAlias from petsc4py import PETSc import underworld3 @@ -37,14 +37,6 @@ class SolverBaseClass(uw_object): self.Unknowns = self._Unknowns(self) - self._u = self.Unknowns.u - self._DuDt = self.Unknowns.DuDt - self._DFDt = self.Unknowns.DFDt - - self._L = self.Unknowns.L # grad(u) - self._E = self.Unknowns.E # sym part - self._W = self.Unknowns.W # asym part - self._order = 0 self._constitutive_model = None @@ -190,6 +182,11 @@ class SolverBaseClass(uw_object): return + # Deprecate in favour of properties for solver.F0, solver.F1 + @timing.routine_timer_decorator + def _setup_problem_description(self): + raise RuntimeError("Contact Developers - shouldn't be calling SolverBaseClass _setup_problem_description") + @timing.routine_timer_decorator def add_condition(self, f_id, c_type, conds, label, components=None): """ @@ -201,6 +198,7 @@ class SolverBaseClass(uw_object): ---------- f_id: int Index of the solver's field (equation) to apply the condition. + Note: The solvers field id is usually different to the mesh's field ids. c_type: string BC type. Either dirichlet (essential) or neumann (natural) conditions. conds: array_like of floats or a sympy.Matrix @@ -289,30 +287,13 @@ class SolverBaseClass(uw_object): """ self.add_condition(0, 'dirichlet', conds, boundary, components) - - ## Properties that are common to all solvers - ## F0 and F1 are the force / flux terms, respectively - ## Solvers over-ride these to describe the problem type @property def F0(self): - - f0 = uw.function.expression( - r"\mathbf{f}_0\left( \mathbf{u} \right)", - None, - "Pointwise force term: f_0(u)", - ) - - return f0 + raise RuntimeError("Contact Developers - SolverBaseClass F0 is being used") @property def F1(self): - f1 = uw.function.expression( - r"\mathbf{F}_1\left( \mathbf{u} \right)", - None, - "Pointwise flux term: F_1(u)", - ) - - return f1 + raise RuntimeError("Contact Developers - SolverBaseClass F0 is being used") @property def u(self): @@ -439,16 +420,11 @@ class SNES_Scalar(SolverBaseClass): self.Unknowns.DuDt = DuDt self.Unknowns.DFDt = DFDt - # self.u = u_Field - # self.DuDt = DuDt - # self.DFDt = DFDt - self.name = solver_name self.verbose = verbose self._tolerance = 1.0e-4 ## Todo: this is obviously not particularly robust - if solver_name != "" and not solver_name.endswith("_"): self.petsc_options_prefix = solver_name+"_" else: @@ -488,8 +464,6 @@ class SNES_Scalar(SolverBaseClass): self.petsc_options.delValue("snes_monitor_short") self.petsc_options.delValue("snes_converged_reason") - self._F0 = sympy.Matrix.zeros(1,1) - self._F1 = sympy.Matrix.zeros(1,mesh.dim) self.dm = None @@ -508,7 +482,6 @@ class SNES_Scalar(SolverBaseClass): self.mesh._equation_systems_register.append(self) self.is_setup = False - # super().__init__() @property def tolerance(self): @@ -1085,8 +1058,6 @@ class SNES_Vector(SolverBaseClass): vtype=uw.VarType.VECTOR, degree=degree ) - self._F0 = sympy.Matrix.zeros(1, self.mesh.dim) - self._F1 = sympy.Matrix.zeros(self.mesh.dim, self.mesh.dim) self.dm = None ## sympy.Matrix @@ -1111,8 +1082,6 @@ class SNES_Vector(SolverBaseClass): self.mesh._equation_systems_register.append(self) - # super().__init__() - @property def tolerance(self): return self._tolerance diff --git a/src/underworld3/discretisation.py b/src/underworld3/discretisation.py index 8b5110f84..792d208c6 100644 --- a/src/underworld3/discretisation.py +++ b/src/underworld3/discretisation.py @@ -1650,7 +1650,7 @@ def __init__( self.symbol = symbol if mesh.instance_number > 1: - invisible = "\,\!" * mesh.instance_number + invisible = r"\,\!" * mesh.instance_number self.symbol = f"{{ {{ {invisible} }} {symbol} }}" self.clean_name = re.sub(r"[^a-zA-Z0-9_]", "", name) diff --git a/src/underworld3/function/expressions.py b/src/underworld3/function/expressions.py index b9c2343bc..569fbbb68 100644 --- a/src/underworld3/function/expressions.py +++ b/src/underworld3/function/expressions.py @@ -87,7 +87,7 @@ class UWexpression(Symbol, uw_object): ```{python} alpha = UWexpression( r'\\alpha', - value=3.0e-5, + sym=3.0e-5, description="thermal expansivity" ) print(alpha.sym) @@ -96,17 +96,22 @@ class UWexpression(Symbol, uw_object): """ - def __new__(cls, name, value, description="No description provided"): + def __new__(cls, name, *args, **kwargs,): obj = Symbol.__new__(cls, name) - obj.sym = sympy.sympify(value) - obj.symbol = name return obj - def __init__(self, name, value, description="No description provided"): - - self._sym = sympy.sympify(value) - self._description = description + def __init__(self, name, sym=None, description="No description provided", value=None): + if value is not None and sym is None: + import warnings; + warnings.warn(message=f"DEPRECATION warning, don't use 'value' attribute for expression: {value}, please use 'sym' attribute") + sym = value + if value is not None and sym is not None: + raise ValueError("Both 'sym' and 'value' attributes are provided, please use one") + + self.sym = sympy.sympify(sym) + self.symbol = name + self.description = description return diff --git a/src/underworld3/systems/solvers.py b/src/underworld3/systems/solvers.py index 1e385e7cc..eef470f84 100644 --- a/src/underworld3/systems/solvers.py +++ b/src/underworld3/systems/solvers.py @@ -81,9 +81,6 @@ def __init__( if solver_name == "": solver_name = "Poisson_{}_".format(self.instance_number) - # Register the problem setup function - self._setup_problem_description = self.poisson_problem_description - # default values for properties self.f = sympy.Matrix.zeros(1, 1) @@ -210,9 +207,6 @@ def __init__( if solver_name == "": self.solver_name = "Darcy_{}_".format(self.instance_number) - # Register the problem setup function - self._setup_problem_description = self.darcy_problem_description - # default values for properties self._f = sympy.Matrix([0]) self._k = 1 @@ -458,8 +452,6 @@ def __init__( self._bodyforce = sympy.Matrix([[0] * self.mesh.dim]) - self._setup_problem_description = self.stokes_problem_description - # this attrib records if we need to setup the problem (again) self.is_setup = False @@ -842,7 +834,6 @@ def __init__( if solver_name == "": self.name = "SProj_{}_".format(self.instance_number) - self._setup_problem_description = self.projection_problem_description self.is_setup = False self._smoothing = 0.0 self._uw_weighting_function = 1.0 @@ -974,7 +965,6 @@ def __init__( if solver_name == "": solver_name = "VProj{}_".format(self.instance_number) - self._setup_problem_description = self.projection_problem_description self.is_setup = False self._smoothing = 0.0 self._penalty = 0.0 @@ -1298,8 +1288,6 @@ def __init__( self.is_setup = False self.restore_points_to_domain_func = restore_points_func - self._setup_problem_description = self.adv_diff_slcn_problem_description - ### Setup the history terms ... This version should not build anything ### by default - it's the template / skeleton @@ -1636,8 +1624,6 @@ def __init__( ) self.restore_points_to_domain_func = restore_points_func - self._setup_problem_description = self.navier_stokes_problem_description - self._bodyforce = sympy.Matrix([[0] * self.mesh.dim]) self._constitutive_model = None diff --git a/src/underworld3/utilities/__init__.py b/src/underworld3/utilities/__init__.py index bef031533..ffab01a90 100644 --- a/src/underworld3/utilities/__init__.py +++ b/src/underworld3/utilities/__init__.py @@ -16,8 +16,7 @@ def _append_petsc_path(): from .uw_petsc_gen_xdmf import Xdmf, generateXdmf, generate_uw_Xdmf from .uw_swarmIO import swarm_h5, swarm_xdmf -from ._utils import CaptureStdout, h5_scan, mem_footprint, gather_data +from ._utils import CaptureStdout, h5_scan, mem_footprint, gather_data, auditor from .read_medit_ascii import read_medit_ascii, print_medit_mesh_info from .create_dmplex_from_medit import create_dmplex_from_medit - diff --git a/src/underworld3/utilities/_api_tools.py b/src/underworld3/utilities/_api_tools.py index a0abc8b47..c0d2bd320 100644 --- a/src/underworld3/utilities/_api_tools.py +++ b/src/underworld3/utilities/_api_tools.py @@ -34,36 +34,41 @@ def newfunc(*args, **kwargs): return newfunc +class uw_object(): + """ + The UW (mixin) class adds common functionality that we wish to provide on all uw_objects + such as the view methods (classmethod for generic information and instance method that can be over-ridden) + to provide instance-specific information + """ -class counted_metaclass(type): - def __init__(cls, name, bases, attrs): - super().__init__(name, bases, attrs) - cls._total_instances = 0 - + _obj_count = 0 # a class variable to count the number of objects -class uw_object_counter(object, metaclass=counted_metaclass): def __init__(self): - try: - self.__class__.mro()[1]._total_instances += 1 - except AttributeError: - pass - # print(f"{self.__class__.mro()[1]} is not a uw_object") + super().__init__ - super().__init__() + self._uw_id = uw_object._obj_count + uw_object._obj_count += 1 - self.__class__._total_instances += 1 - self.instance_number = self.__class__._total_instances + # to order of the following decorators matters python + # see - https://stackoverflow.com/questions/128573/using-property-on-classmethods/64738850#64738850 + @classmethod + def uw_object_counter(cls): + """ Number of uw_object instances created """ + return uw_object._obj_count + @property + def instance_number(self): + """ Unique number of the uw_object instance """ + return self._uw_id -class uw_object(uw_object_counter): - """ - The UW (mixin) class adds common functionality that we wish to provide on all uw_objects - such as the view methods (classmethod for generic information and instance method that can be over-ridden) - to provide instance-specific information - """ + def __str__(self): + s = super().__str__() + return f"{self.__class__.__name__} instance {self.instance_number}, {s}" - def __init__(self): - super().__init__() + @staticmethod + def _reset(): + """ Reset the object counter """ + uw_object._obj_count = 0 @class_or_instance_method def _ipython_display_(self_or_cls): @@ -118,10 +123,6 @@ def view(self_or_cls, class_documentation=False): self_or_cls._object_viewer() - @classmethod - def total_instances(cls): - return cls._total_instances - # placeholder def _object_viewer(self): from IPython.display import Latex, Markdown, display diff --git a/src/underworld3/utilities/_utils.py b/src/underworld3/utilities/_utils.py index cc68c73e9..d3d5fd1d9 100755 --- a/src/underworld3/utilities/_utils.py +++ b/src/underworld3/utilities/_utils.py @@ -7,23 +7,121 @@ from collections import UserString from contextlib import redirect_stdout, redirect_stderr +class _uw_record(): + """ + A class to record runtime information about the underworld3 execution environment. + """ -# # Capture the stdout to an object -# class CaptureStdout(list): -# def __enter__(self, split=True): -# self._stdout = sys.stdout -# self.split = split -# sys.stdout = self._stringio = StringIO() -# return self - -# def __exit__(self, *args): -# if split: -# self.extend(self._stringio.getvalue().splitlines()) -# else: -# self.extend(self._stringio.getvalue() -# del self._stringio # free up some memory -# sys.stdout = self._stdout - + def __init__(self): + try: + import mpi4py + comm = mpi4py.MPI.COMM_WORLD + except ImportError: + raise ImportError("Can't import mpi4py for runtime information.") + + # rank 0 only builds the data and then broadcasts it + self._install_data = None + self._runtime_data = None + if comm.rank == 0: + + import sys + import datetime + import subprocess + import warnings + + # get the start time of this piece of code + start_t = datetime.datetime.now().isoformat() + + # get the git version + try: + gv = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip().decode('utf-8') + except Exception as e: + gv = None + warnings.warn( f"Warning: Underworld can't retrieving commit hash: {e}" ) + + # get petsc information + try: + import petsc4py as _petsc4py + from petsc4py import PETSc as _PETSc + petsc_version = _PETSc.Sys.getVersion() + petsc_dir = _petsc4py.get_config()['PETSC_DIR'] + except Exception as e: + petsc_version = None + petsc_dir = None + warnings.warn( f"Warning: Underworld can't retrieving petsc installation details: {e}" ) + + # get h5py information + try: + import h5py as _h5py + h5py_dir = _h5py.__file__ + h5py_version = _h5py.version.version + hdf5_version = _h5py.version.hdf5_version + except Exception as e: + h5py_dir = None + h5py_version = None + hdf5_version = None + warnings.warn( f"Warning: Underworld can't retrieving h5py installation details: {e}" ) + + # get mpi4py information + try: + import mpi4py as _mpi4py + mpi4py_version = _mpi4py.__version__ + except Exception as e: + mpi4py_version = None + warnings.warn( f"Warning: Underworld can't retrieving mpi4py installation details: {e}" ) + + # get just the version + from underworld3 import __version__ as uw_version + + self._install_data = { + "git_version": gv, + "uw_version": uw_version, + "python_versions": sys.version, + "petsc_version": petsc_version, + "petsc_dir": petsc_dir, + "hdf5_version": hdf5_version, + "h5py_version": h5py_version, + "h5py_dir": h5py_dir, + "mpi4py_version": mpi4py_version, + } + + self._runtime_data = { + "start_time": start_t, + "uw_object_count": 0, + } + + # rank 0 broadcast information to other procs + self._install_data = comm.bcast(self._install_data, root=0) + + @property + def get_installation_data(self): + ''' + Get the installation data for the underworld3 installation. + ''' + return self._install_data + + @property + def get_runtime_data(self): + ''' + Get the runtime data for the underworld3 installation. + Note this requires a MPI broadcast to get the data. + ''' + import datetime + import mpi4py + comm = mpi4py.MPI.COMM_WORLD + + if comm.rank == 0: + now = datetime.datetime.now().isoformat() + self._runtime_data.update({"current_time": now}) + + from underworld3.utilities._api_tools import uw_object + object_count = uw_object.uw_object_counter() + self._runtime_data.update({"uw_object_count": object_count}) + + self._runtime_data = comm.bcast(self._runtime_data, root=0) + return self._runtime_data + +auditor = _uw_record() class CaptureStdout(UserString, redirect_stdout): """ diff --git a/test.sh b/test.sh index f4ecda87b..32872fafa 100755 --- a/test.sh +++ b/test.sh @@ -9,7 +9,8 @@ PYTEST="pytest -c tests/pytest.ini" # Run simple tests -$PYTEST tests/test_00*py +$PYTEST tests/test_00[0-4]*py +$PYTEST tests/test_0050*py # Spatial / calculation tests $PYTEST tests/test_01*py tests/test_05*py tests/test_06*py @@ -21,5 +22,5 @@ $PYTEST tests/test_100[0-9]*py $PYTEST tests/test_1010*py tests/test_1011*py tests/test_1050*py # Diffusion / Advection tests -$PYTEST tests/test_1100*py -$PYTEST tests/test_1110*py # Annulus version \ No newline at end of file +# $PYTEST tests/test_1100*py +# $PYTEST tests/test_1110*py # Annulus version diff --git a/tests/test_0050_utils.py b/tests/test_0050_utils.py new file mode 100644 index 000000000..6a4de9918 --- /dev/null +++ b/tests/test_0050_utils.py @@ -0,0 +1,112 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:percent +# text_representation: +# extension: .py +# format_name: percent +# format_version: '1.3' +# jupytext_version: 1.16.2 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +## %% + +import underworld3 as uw +import sympy + +mesh = uw.meshing.StructuredQuadBox(elementRes=(5,) * 2) +x, y = mesh.X + +# %% +v = uw.discretisation.MeshVariable( r"mathbf{u}", mesh, mesh.dim, vtype=uw.VarType.VECTOR, degree=2) +p = uw.discretisation.MeshVariable( r"mathbf{p}", mesh, 1, vtype=uw.VarType.SCALAR, degree=1) + +def bc_1(solver): + s1 = solver + s1.add_dirichlet_bc((0.0, 0.0), "Bottom") + s1.add_dirichlet_bc((y, 0.0), "Top") + + s1.add_dirichlet_bc((sympy.oo, 0.0), "Left") + s1.add_dirichlet_bc((sympy.oo, 0.0), "Right") + +def bc_2(solver): + s1 = solver + s1.add_dirichlet_bc((0.0, sympy.oo), "Bottom") + s1.add_dirichlet_bc((0.0, sympy.oo), "Top") + + s1.add_dirichlet_bc((0.0, 0.0), "Left") + s1.add_dirichlet_bc((0.0, x), "Right") + + +# %% +def vis_model(mesh): + import pyvista as pv + import underworld3.visualisation as vis + + v = mesh.vars['mathbfu'] + pl = pv.Plotter(window_size=(1000, 750)) + + pvmesh = vis.mesh_to_pv_mesh(mesh) + pvmesh.point_data["V"] = vis.vector_fn_to_pv_points(pvmesh, v.sym) + pvmesh.point_data["Vmag"] = vis.scalar_fn_to_pv_points(pvmesh, v.sym.dot(v.sym)) + pvmesh.point_data["V1"] = vis.scalar_fn_to_pv_points(pvmesh, v.sym[1]) + + pl.add_mesh( + pvmesh, + cmap="coolwarm", + edge_color="Black", + show_edges=True, + scalars="Vmag", + use_transparency=False, + opacity=1.0, + ) + + velocity_points = vis.meshVariable_to_pv_cloud(v) + velocity_points.point_data["V"] = vis.vector_fn_to_pv_points(velocity_points, v.sym) + arrows = pl.add_arrows(velocity_points.points, velocity_points.point_data["V"], mag=3e-1, opacity=0.5, show_scalar_bar=False, cmap="coolwarm") + + pl.show(cpos="xy") + + + +# %% +stokes = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +stokes.constitutive_model = uw.constitutive_models.ViscousFlowModel +stokes.constitutive_model.Parameters.shear_viscosity_0 = 1 + +# %% +bc_1(stokes) + +# %% +stokes.solve() + +# %% +# vis_model(mesh) + +# %% +s1 = uw.systems.Stokes(mesh, velocityField=v, pressureField=p) +s1.constitutive_model = uw.constitutive_models.ViscousFlowModel +s1.constitutive_model.Parameters.shear_viscosity_0 = 1 +# stokes._rebuild_after_mesh_update() +bc_2(s1) + +# %% +#stokes.solve() +s1.solve() + +# %% +# vis_model(mesh) + +def test_auditor(): + # assert not values are in install data are None + for v in uw.auditor.get_installation_data.values(): + assert v is not None + + # assert 7 uw_objects are created + assert uw.auditor.get_runtime_data.get('uw_object_count') == 7 + +# %%