Usage

Suppose a module RandomWalker provides capability to simulate a one-dimensional random walker that moves according to a uniform distribution, and also provides a SimService implementation with additional interface features to get and set the position of a random walker during execution. SimService supports passing any data that can be serialized into and out of a RandomWalker simulation instance through methods that the RandomWalker module adds to the interface of its SimService implementation proxies using service_functions,

"""
RandomWalker.py
"""
from random import random
from simservice import service_function

pos: float = 0.0

def step():
    global pos
    pos += 2.0 * random() - 1.0

def get_pos():
    return pos

def set_pos(_val: float):
    global pos
    pos = _val

# Make the current position available on SimService proxies
service_function(get_pos)
# Make the current position settable on SimService proxies
service_function(set_pos)

Now suppose that the RandomWalker module provides a SimService proxy factory service_random_walker to efficiently create SimService proxy instances, where the SimService proxy method step calls the underlying step function of RandomWalker.py. An end-user of RandomWalker can use whatever functions defined in RandomWalker.py are declared as service_functions, and with the same signature, on a proxy instance. For example, if the end-user wishes to impose periodic boundary conditions on their RandomWalker simulation, they can use the get_pos and set_pos service functions declared in RandomWalker.py, which call their underlying functions of the same name,

"""
RandomWalkerUser.py
"""
from RandomWalker import service_random_walker

random_walker_proxy = service_random_walker()
random_walker_proxy.run()
random_walker_proxy.init()
random_walker_proxy.start()
for _ in range(100):
    random_walker_proxy.step()
    # Impose periodic boundary conditions on a domain [-1, 1] using service functions
    pos = random_walker_proxy.get_pos()
    if pos < -1.0:
        random_walker_proxy.set_pos(pos + 2.0)
    elif pos > 1.0:
        random_walker_proxy.set_pos(pos - 2.0)
random_walker_proxy.finish()

For applications that do not require fine-grained control of simulation stages (e.g., run, finish), SimService provides the ExecutionContext to eliminate mundane code,

SimService proxies support serialization and so can be attached to, and executed in, separate processes, whether as single, background processes or in batch execution. PySimService provides a method inside_run that takes a Python function as argument, where the function defines instructions for execution of a proxy that is passed as argument. This functionality gives end-users the ability to prescribe execution instructions to be carried out by a proxy when PySimService.run is called (e.g., by a process that they define). For example, suppose an end-user wishes to execute a batch of RandomWalker simulations as defined above in parallel, and has defined a function execute_in_parallel that executes each of a list of RandomWalker proxies in a separate process. The end-user can define a function inside_run that carries out their simulation on a RandomWalker proxy and set it on each instance before batch execution. After execution, references to each instance and all underlying data are still valid and accessible until passed to close_service,

Note

Calling close_service on services is especially important when using lots of proxies over the lifetime of a program to prevent unnecessary memory usage.

Note that the Python multiprocessing.Pool does not allow creating processes from within created processes by default, which makes creating proxies in parallel illegal. SimService provides NonDaemonicPool, a customized version of multiprocessing.Pool, that permits creating services during parallel execution,

from simservice.utils import NonDaemonicPool

def instantiate_and_run(_):
    """Creates and executes a service and returns the result"""
    proxy_inst = service_random_walker()
    proxy_inst.set_inside_run(inside_run)
    proxy_inst.run()
    return proxy_inst.get_pos()

with NonDaemonicPool(8) as pool:
    final_positions = poolmap(instantiate_and_run, [None] * 80)
mean_position = sum(final_positions) / len(final_positions)