Source code for mph.backend

"""
Discovers Comsol installations on the local machine.

This is a helper module that is not part of the public API. It
retrieves information about installed Comsol versions, i.e. available
simulation back-ends, and locates the installation folder.

The discovery mechanism currently only works on Windows, as it relies
on the Registry to provide that information.
"""
__license__ = 'MIT'


########################################
# Dependencies                         #
########################################
import platform                        # platform information
import winreg                          # Windows registry
import re                              # regular expressions
from subprocess import run, PIPE       # external processes
from collections import namedtuple     # named tuples
from functools import lru_cache        # least-recently-used cache
from numpy import array                # numerical arrays
from pathlib import Path               # file paths
from logging import getLogger          # event logging


########################################
# Globals                              #
########################################
logger = getLogger(__package__)        # package-wide event logger


########################################
# Functions                            #
########################################

[docs]@lru_cache(maxsize=1) def versions(): """ Returns version information of available Comsol installations. Version information is returned as a list of named tuples containing the major, minor and patch version numbers, the build number, and the installation folder. Currently, this has only been implemented for the Windows operating system. On other platforms, such as Linux and MacOS, the code here would have to be amended, namely because locating installed software is system-specific. Raises `RuntimeError` if no Comsol installation was found. Raises `NotImplementedError` on operating systems other than Windows. """ # Define data type describing a back-end installation. Version = namedtuple('version', ('major', 'minor', 'patch', 'build', 'folder')) # Bail out if not on the Windows platform. system = platform.system() if system != 'Windows': error = (f'Unsupported operating system "{system}".' f'This library is (currently) Windows-only.') logger.error(error) raise NotImplementedError(error) # Open main Comsol registry node. path_main = r'SOFTWARE\Comsol' try: main = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path_main, access=winreg.KEY_READ | winreg.KEY_WOW64_64KEY) except FileNotFoundError: error = 'Did not find Comsol registry entry.' logger.error(error) raise OSError(error) from None # Parse sub-nodes to get list of installed Comsol versions. versions = {} index = 0 while True: # Get name of next node. Exit loop if list exhausted. try: name = winreg.EnumKey(main, index) index += 1 except OSError: break # Ignore nodes that don't follow naming pattern. if not re.match(r'(?i)Comsol\d+[a-z]?', name): logger.debug(f'Ignoring registry node "{name}".') continue # Open the sub-node. path_node = path_main + '\\' + name logger.debug(f'Checking registry node "{path_node}".') try: node = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path_node, access=winreg.KEY_READ | winreg.KEY_WOW64_64KEY) except FileNotFoundError: error = f'Could not open registry node "{name}".' logger.error(error) continue # Get installation folder from corresponding key. key = 'COMSOLROOT' try: value = winreg.QueryValueEx(node, key) except FileNotFoundError: error = f'Key "{key}" missing in node "{name}".' logger.error(error) continue folder = Path(value[0]) logger.debug(f'Checking installation folder "{folder}".') # Check that server executable exists. path = folder / 'bin' / architecture() / 'comsolmphserver.exe' if not path.exists(): error = 'Did not find Comsol server executable.' logger.error(error) continue # Query the Comsol server's version information. flags = 0x08000000 if platform.system() == 'Windows' else 0 command = [str(path), '--version'] process = run(command, stdout=PIPE, creationflags=flags) if process.returncode != 0: error = 'Querying version information failed.' logger.error(error) answer = process.stdout.decode('ascii', errors='ignore').strip() logger.debug(f'Reported version info is "{answer}".') # Parse out the actual version number. match = re.match(r'(?i)Comsol.*?(\d+(?:\.\d+)*)', answer) if not match: error = f'Unexpected answer "{answer}" to version query.' logger.error(error) continue number = match.group(1) # Break the version number down into parts. parts = number.split('.') if len(parts) > 4: error = f'Reported version "{number}" has more than four parts.' logger.error(error) continue try: parts = [int(part) for part in parts] except ValueError: error = f'Not all parts of version "{number}" are numbers.' logger.error(error) continue parts = parts + [0]*(4-len(parts)) (major, minor, patch, build) = parts # Assign a standardized name to this version. name = f'{major}.{minor}' if patch > 0: name += chr(ord('a') + patch - 1) logger.debug(f'Assigned name "{name}" to this installation.') # Check that Java virtual machine exists. java = folder / 'java' / architecture() / 'jre' / 'bin' jvm = java / 'server' / 'jvm.dll' if not jvm.exists(): error = 'Did not find Java virtual machine.' logger.error(error) continue # Check that Java API folder exists. path = folder / 'plugins' if not path.exists(): error = 'Did not find Comsol API plugins.' logger.error(error) continue # Add to list of installed versions. if name in versions: logger.warning(f'Ignoring duplicate of Comsol version {name}.') else: versions[name] = Version(major, minor, patch, build, folder) # Report error if no Comsol installation was found. if not versions: error = 'Could not locate any Comsol installation.' logger.error(error) raise RuntimeError(error) # Sort versions by name. versions = dict(sorted(versions.items())) # Return list of installed versions. return versions
[docs]def folder(version=None): """ Returns the path to the Comsol installation folder. A specific Comsol `version` can be named, if several are installed, for example `version='5.3a'`. Otherwise the latest version is used. Relies on `versions()` to discover installations. Raises `ValueError` if the requested version is not installed. """ if version is not None: if version not in versions(): error = f'Version {version} is not installed.' logger.error(error) raise ValueError(error) return versions()[version].folder else: last = list(versions().keys())[-1] return versions()[last].folder
[docs]@lru_cache(maxsize=1) def architecture(): """ Returns the name of the "architecture" folder inside the Comsol root folder. That folder name is platform-dependent: • `win64` on Windows • `glnxa64` on Linux • `maci64` on Mac OS These are all builds for 64-bit CPU architectures. As Comsol no longer supports 32-bit architectures, neither does this library. Raises `OSError` if the operating system the application runs on is not supported. Currently, these are all operating systems apart from Windows. """ system = platform.system() if system == 'Windows': return 'win64' elif system == 'Linux': return 'glnxa64' elif system == 'Darwin': return 'maci64' else: error = f'Operating system "{system}" not supported.' logger.error(error) raise OSError(error)
[docs]def inspect(java): """ Inspects a Java object representing a Comsol model feature. This is basically a "pretty-fied" version of the output from the standard `dir` command. It displays (prints to the console) the methods of a model node, given as a `java` object as provided by the Comsol API, as well as the node's "property" names and values, if any are defined. The node's name, tag, and documentation reference marker are listed first. These access methods and a few others, which are common to all objects, are suppressed in the method list further down, for the sake of clarity. """ # Display general information about the feature. print(f'name: {java.name()}') print(f'tag: {java.tag()}') try: print(f'type: {java.getType()}') except AttributeError: pass print(f'display: {java.getDisplayString()}') print(f'doc: {java.docMarker()}') # Display comments and notify if feature is deactivated or has warnings. comments = str(java.comments()) if comments: print(f'comment: {comments}') if not java.isActive(): print('This feature is currently deactivated.') try: if java.hasWarning(): print('This feature has warnings.') except AttributeError: pass # Introspect the feature's attributes. attributes = [attribute for attribute in dir(java)] # Display properties if any are defined. if 'properties' in attributes: print('properties:') names = [str(property) for property in java.properties()] for name in names: dtype = str(java.getValueType(name)) if dtype == 'Int': value = int(java.getInt(name)) elif dtype == 'IntArray': value = array(java.getIntArray(name)) elif dtype == 'IntMatrix': value = array([line for line in java.getIntMatrix(name)]) elif dtype == 'Boolean': value = java.getBoolean(name) elif dtype == 'BooleanArray': value = array(java.getBooleanArray(name)) elif dtype == 'BooleanMatrix': value = array([line for line in java.getBooleanMatrix(name)]) elif dtype == 'Double': value = java.getDouble(name) elif dtype == 'DoubleArray': value = array(java.getDoubleArray(name)) elif dtype == 'DoubleMatrix': value = array([line for line in java.getDoubleMatrix(name)]) elif dtype == 'String': value = str(java.getString(name)) elif dtype == 'StringArray': value = [str(string) for string in java.getStringArray(name)] elif dtype == 'StringMatrix': value = [[str(string) for string in line] for line in java.getStringMatrix(name)] else: value = '[?]' print(f' {name}: {value}') # Define a list of common methods to be suppressed in the output. suppress = ['name', 'label', 'tag', 'getType', 'getDisplayString', 'docMarker', 'help', 'comments', 'toString', 'icon', 'properties', 'hasProperty', 'set', 'getEntryKeys', 'getEntryKeyIndex', 'getValueType', 'getInt', 'getIntArray', 'getIntMatrix', 'getBoolean', 'getBooleanArray', 'getBooleanMatrix', 'getDouble', 'getDoubleArray', 'getDoubleMatrix', 'getString', 'getStringArray', 'getStringMatrix', 'version', 'author', 'resetAuthor', 'lastModifiedBy', 'dateCreated', 'dateModified', 'timeCreated', 'timeModified', 'active', 'isActive', 'isactive', 'hasWarning', 'class_', 'getClass', 'hashCode', 'notify', 'notifyAll', 'wait'] # Display the feature's methods. print('methods:') for name in attributes: if name.startswith('_') or name in suppress: continue print(f' {name}')