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)