Allow Negative Values for `execution_index` when adding a child
Summary
Currently, CoSApp restricts the execution_index parameter to non-negative integers (>= 0). This limitation can potentially restrict the flexibility and expressiveness of the application. I propose allowing negative values for the execution_index parameter to extend its functionality.
Description
At present, the following code in module.py
only allows non-negative integers: link to the code
Motivation
Allowing negative indices could enable users to insert elements at specific positions from the end of a list. Python's list insertion list.insert()
supports this functionality, and adopting the same behavior in system.py could make the API more consistent with Python's native list manipulation features.
Use case
In my application, I have implemented a CombineArrays
system. As its name suggests, it aggregates multiple arrays sourced from various DataEntry
systems to form a matrix. The vectors from this matrix are then extracted to feed the inwards
of other Metric
systems.
The issue arises because I need to specify the list of entries
and metrics
before initializing the CombineArrays
interface. In this specific use case, I aim to create the CombineArrays
system last but want it to execute before the Metric
type systems. I want to achieve this by using the execution_index=-1
argument within the System.add_child()
method.
In this context, allowing negative values for execution_index would offer a more streamlined way to manage the execution order of dynamically connected systems.
Full reproducible example
import abc
from typing import Iterable
import numpy as np
from cosapp.systems import System
np.random.seed(0)
class DataEntry(System):
def setup(self):
# let's say this system takes several inwards (not presented here)
# and generates a outward of type numpy array.
self.add_outward("result", np.random.random((2,)))
class Metric(System):
def setup(self):
# let's say that this system takes an array as input
# and generates results that are not presented here
self.add_inward("vector", np.array([]))
class CombineArrays(System):
def setup(self, rows: Iterable[str], cols: Iterable[str]) -> None:
self.add_property("rows", rows)
self.add_property("cols", cols)
for row_name in self.rows:
self.add_inward(row_name, np.array([]))
for col_name in self.cols:
self.add_outward(col_name, np.array([]))
self.add_outward("matrix", np.array([]))
def compute(self) -> None:
arrays = [getattr(self, name) for name in self.rows]
self.matrix = np.vstack(arrays)
for index, col_name in enumerate(self.cols):
setattr(self, col_name, self.matrix[:, index])
class BaseSystem(System):
def setup(self) -> None:
entries = self.create_entries()
self.add_property("entries", entries)
metrics = self.create_metrics()
self.add_property("metrics", metrics)
combine = self.add_child(
CombineArrays(
"combine",
rows=[entry.name for entry in entries],
cols=[metric.name for metric in metrics],
),
# execution_index=-1 # <--------- HERE IS THE ISSUE **********************
)
for entry in self.entries:
self.connect(entry, combine, {"result": entry.name})
for metric in self.metrics:
self.connect(combine, metric, {metric.name: "vector"})
@abc.abstractmethod
def create_entries(self) -> tuple[DataEntry, ...]:
pass
@abc.abstractmethod
def create_metrics(self) -> tuple[Metric, ...]:
pass
class FirstImplementation(BaseSystem):
def create_entries(self) -> tuple[DataEntry, ...]:
first_entry = self.add_child(DataEntry("first_entry"))
second_entry = self.add_child(DataEntry("second_entry"))
third_entry = self.add_child(DataEntry("third_entry"))
return (first_entry, second_entry, third_entry)
def create_metrics(self) -> tuple[Metric, ...]:
first_metric = self.add_child(Metric("first_metric"))
second_metric = self.add_child(Metric("second_metric"))
return (first_metric, second_metric)
my_system = FirstImplementation('system')
my_system.run_once()
my_system.first_metric.vector
>>> []
although the interface system contains all the information
my_system.combine.matrix
>>> [[0.5488135 0.71518937]
[0.60276338 0.54488318]
[0.4236548 0.64589411]]
execution_index=-1
my_system.first_metric.vector
>>> [0.5488135, 0.60276338, 0.4236548]