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)