Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions demos/compact_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,16 @@
model file under its original name, effectively compacting its size.
"""

########################################
# Dependencies #
########################################
import mph

from pathlib import Path
from time import perf_counter as now
from time import perf_counter as now


########################################
# Timer #
########################################
class Timer():
class Timer:
"""Convenience class for measuring and displaying elapsed time."""

def __init__(self, margin=4, padding=12):
Expand Down
1 change: 1 addition & 0 deletions demos/create_capacitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import mph


client = mph.start()
model = client.create('capacitor')

Expand Down
6 changes: 2 additions & 4 deletions demos/worker_pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
instructions and results back and forth.
"""

########################################
# Dependencies #
########################################
import mph # Comsol interface

from multiprocessing import Process # external subprocess
from multiprocessing import Queue # inter-process queue
from multiprocessing import cpu_count # number of (logical) cores
Expand Down Expand Up @@ -96,7 +94,7 @@ def boss():
results = Queue()
processes = []
workers = cpu_count()
for n in range(workers):
for _ in range(workers):
process = Process(target=worker, args=(jobs, results))
processes.append(process)
process.start()
Expand Down
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
'members': True, # Include module/class members.
'member-order': 'bysource', # Order members as in source file.
}
autodoc_typehints = 'none' # Rendering type hints doesn't work.
autosummary_generate = False # Stub files are created by hand.
add_module_names = False # Drop module prefix from signatures.

Expand Down
145 changes: 74 additions & 71 deletions mph/client.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
"""Provides the wrapper for a Comsol client instance."""

########################################
# Components #
########################################
from . import discovery # back-end discovery
from .model import Model # model class
from .config import option # configuration

########################################
# Dependencies #
########################################
import jpype # Java bridge
import jpype.imports # Java object imports
import os # operating system
from pathlib import Path # file-system paths
from logging import getLogger # event logging
import faulthandler # traceback dumps

########################################
# Globals #
########################################
log = getLogger(__package__) # event log


########################################
# Constants #
########################################
from __future__ import annotations

from . import discovery
from .model import Model
from .config import option

import jpype
import jpype.imports
import os
import faulthandler
from pathlib import Path
from logging import getLogger

from typing import overload, Iterator
from jpype import JClass


# The following look-up table is used by the `modules()` method. It is
# based on the table on page 41 of Comsol 6.0's Programming Reference
Expand All @@ -34,7 +24,6 @@
# somewhat (drop "Module" everywhere) and leave out the pointless
# trademark symbols. The vendor strings are what we need to query the
# `ModelUtil.hasProduct()` Java method.

modules = {
'COMSOL': 'Comsol core',
'ACDC': 'AC/DC',
Expand Down Expand Up @@ -86,10 +75,9 @@
'WAVEOPTICS': 'Wave Optics',
}

log = getLogger(__package__)


########################################
# Client #
########################################
class Client:
"""
Manages the Comsol client instance.
Expand Down Expand Up @@ -138,11 +126,31 @@ class Client:
/com/comsol/model/util/ModelUtil.html
"""

####################################
# Internal #
####################################
version: str
"""Comsol version (e.g., `'6.0'`) the client is running on."""

standalone: bool
"""Whether this is a stand-alone client or connected to a server."""

port: int | None
"""Port number on which the client has connected to the server."""

host: str | None
"""Host name or IP address of the server the client is connected to."""

java: JClass
"""Java model object that this class instance is wrapped around."""

############
# Internal #
############

def __init__(self, cores=None, version=None, port=None, host='localhost'):
def __init__(self,
cores: int = None,
version: str = None,
port: int = None,
host: str = 'localhost',
):

# Make sure this is the one and only client.
if jpype.isJVMStarted():
Expand Down Expand Up @@ -223,27 +231,18 @@ def __init__(self, cores=None, version=None, port=None, host='localhost'):
# Log that we're done so the start-up time may be inspected.
log.info('Stand-alone client initialized.')

# Save and document instance attributes.
# It seems to be necessary to document the instance attributes here
# towards the end of the method. If done earlier, Sphinx would not
# render them in source-code order, even though that's what we
# request in the configuration. This might be a bug in Sphinx.
self.version = backend['name']
"""Comsol version (e.g., `'6.0'`) the client is running on."""
# Save instance attributes.
self.version = backend['name']
self.standalone = standalone
"""Whether this is a stand-alone client or connected to a server."""
self.port = None
"""Port number on which the client has connected to the server."""
self.host = None
"""Host name or IP address of the server the client is connected to."""
self.java = java
"""Java model object that this class instance is wrapped around."""
self.port = None
self.host = None
self.java = java

# Try to connect to server if not a stand-alone client.
if not standalone and host:
if not standalone and host and port is not None:
self.connect(port, host)

def __repr__(self):
def __repr__(self) -> str:
if self.standalone:
connection = 'stand-alone'
elif self.port:
Expand All @@ -252,17 +251,17 @@ def __repr__(self):
connection = 'disconnected'
return f'{self.__class__.__name__}({connection})'

def __contains__(self, item):
def __contains__(self, item: str | Model) -> bool:
if isinstance(item, str) and item in self.names():
return True
if isinstance(item, Model) and item in self.models():
return True
return False

def __iter__(self):
def __iter__(self) -> Iterator[Model]:
yield from self.models()

def __truediv__(self, name):
def __truediv__(self, name: str) -> Model:
if isinstance(name, str):
for model in self:
if name == model.name():
Expand All @@ -274,30 +273,30 @@ def __truediv__(self, name):
return model
return NotImplemented

####################################
# Inspection #
####################################
##############
# Inspection #
##############

@property
def cores(self):
def cores(self) -> int:
"""Number of processor cores (threads) the Comsol session is using."""
cores = self.java.getPreference('cluster.processor.numberofprocessors')
cores = int(str(cores))
return cores

def models(self):
def models(self) -> list[Model]:
"""Returns all models currently held in memory."""
return [Model(self.java.model(tag)) for tag in self.java.tags()]

def names(self):
def names(self) -> list[str]:
"""Returns the names of all loaded models."""
return [model.name() for model in self.models()]

def files(self):
def files(self) -> list[Path]:
"""Returns the file-system paths of all loaded models."""
return [model.file() for model in self.models()]

def modules(self):
def modules(self) -> list[str]:
"""Returns the names of available licensed modules/products."""
names = []
for (key, value) in modules.items():
Expand All @@ -308,11 +307,11 @@ def modules(self):
pass
return names

####################################
# Interaction #
####################################
###############
# Interaction #
###############

def load(self, file):
def load(self, file: Path | str) -> Model:
"""Loads a model from the given `file` and returns it."""
file = Path(file).resolve()
if self.caching() and file in self.files():
Expand All @@ -324,6 +323,10 @@ def load(self, file):
log.info('Finished loading model.')
return model

@overload
def caching(self, state: None) -> bool: ...
@overload
def caching(self, state: bool): ...
def caching(self, state=None):
"""
Enables or disables caching of previously loaded models.
Expand All @@ -346,7 +349,7 @@ def caching(self, state=None):
log.error(error)
raise ValueError(error)

def create(self, name=None):
def create(self, name: str = None) -> Model:
"""
Creates a new model and returns it as a [`Model`](#Model) instance.

Expand All @@ -362,7 +365,7 @@ def create(self, name=None):
log.debug(f'Created model "{name}" with tag "{java.tag()}".')
return model

def remove(self, model):
def remove(self, model: str | Model):
"""Removes the given [`model`](#Model) from memory."""
if isinstance(model, str):
if model not in self.names():
Expand Down Expand Up @@ -395,11 +398,11 @@ def clear(self):
log.debug('Clearing all models from memory.')
self.java.clear()

####################################
# Remote #
####################################
##########
# Remote #
##########

def connect(self, port, host='localhost'):
def connect(self, port: int, host: str = 'localhost'):
"""
Connects the client to a server.

Expand Down
Loading