Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b7cf5bb
refactor!: Unify update policies
jacopo-massa Apr 13, 2026
4c840b7
build: Adjust Python 3.11 tooling compatibility
jacopo-massa Apr 13, 2026
840d2ca
fix: Restore Python 3.11 compatibility
jacopo-massa Apr 13, 2026
45a89dd
docs: Update echo notebook policy example
jacopo-massa Apr 13, 2026
abf6d60
build: Run tests across supported Python versions
jacopo-massa Apr 13, 2026
c4e1e13
feat: Add built-in update policies
jacopo-massa Apr 13, 2026
b8f65df
test: Expand coverage for built-in policies
jacopo-massa Apr 13, 2026
fdd56c2
docs: Document built-in policy usage
jacopo-massa Apr 13, 2026
d2fe550
feat: Add an off-the-shelf example
jacopo-massa Apr 13, 2026
f6f310e
refactor: Expose scheduled policies as callable classes
jacopo-massa Apr 13, 2026
95b9167
refactor: Expose stateful replay policies as callable classes
jacopo-massa Apr 13, 2026
de4ee26
refactor: Organise shared utility declarations
jacopo-massa Apr 14, 2026
64d7edf
refactor: Refine built-in policy APIs
jacopo-massa Apr 14, 2026
ae95676
test: Align policy tests with the new API
jacopo-massa Apr 14, 2026
e682297
refactor: Remove the shared tools module
jacopo-massa Apr 14, 2026
3c1be49
feat: Add distribution-based update policies
jacopo-massa Apr 14, 2026
d8bf352
test: Split policy tests by family
jacopo-massa Apr 14, 2026
76c654c
docs: Update policy concepts and examples
jacopo-massa Apr 14, 2026
406fb67
refactor: Simplify distribution policy selection
jacopo-massa Apr 14, 2026
839df3e
feat: Add additional noise policies
jacopo-massa Apr 14, 2026
cced970
refactor: Simplify the policies public API
jacopo-massa Apr 14, 2026
4c6a20e
docs: Update policy usage examples
jacopo-massa Apr 14, 2026
9ce47ce
refactor: Rename policy families and simplify degradation helpers
jacopo-massa Apr 14, 2026
d853a53
docs: Update policy usage examples
jacopo-massa Apr 14, 2026
5ce5c08
refactor: Simplify degrade policy primitives
jacopo-massa Apr 15, 2026
15d9976
refactor: Improve runtime logging across simulation components
jacopo-massa Apr 15, 2026
6548d36
chore: Enable trace logging in the off-the-shelf example
jacopo-massa Apr 15, 2026
36d71f3
test: Align logging tests and policy docs
jacopo-massa Apr 15, 2026
be93f77
test: Improve coverage for runtime branches
jacopo-massa Apr 15, 2026
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
9 changes: 7 additions & 2 deletions .github/workflows/upload_coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@ on:

jobs:
test:
name: Run tests and collect coverage
name: Run tests on Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout Source Code
uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: 3.12
python-version: ${{ matrix.python-version }}

- name: Install Dependencies
run: make setup-test
Expand All @@ -38,6 +42,7 @@ jobs:
run: pytest

- name: Upload results to Codecov
if: matrix.python-version == '3.11'
uses: codecov/codecov-action@v6
with:
files: ./coverage.xml
Expand Down
4 changes: 2 additions & 2 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
target-version = "py312"
target-version = "py311"

include = ["eclypse/**/*.py", "tests/**/*.py"]
line-length = 88
Expand Down Expand Up @@ -40,7 +40,7 @@ select = [
"D",
]

ignore = ["D203", "D213", "D100", "D104", "E501", "PLC0415", "UP008"]
ignore = ["D203", "D213", "D100", "D104", "E501", "PLC0415", "UP008", "UP040"]

fixable = ["ALL"]

Expand Down
1 change: 1 addition & 0 deletions docs/source/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Reference
builders
graph
placement
policies
remote
report
simulation
Expand Down
38 changes: 32 additions & 6 deletions docs/source/overview/concepts/topology.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ The two classes share many structural similarities, but differ in purpose and in

.. code-block:: python

from eclypse import policies
from eclypse.graph.infrastructure import Infrastructure

infrastructure = Infrastructure(
infrastructure_id="infra",
node_update_policy=[...],
edge_update_policy=[...],
update_policies=[
policies.failure.availability_flap(0.01, up_probability=0.2),
policies.distribution.uniform(
node_assets=["cpu", "ram"],
edge_assets=["latency", "bandwidth"],
node_distribution=(0.95, 1.05),
edge_distribution=(0.95, 1.05),
),
],
node_assets=[...],
edge_assets=[...],
resource_init="min",
Expand All @@ -33,7 +41,7 @@ The two classes share many structural similarities, but differ in purpose and in
**Key parameters:**

- ``infrastructure_id``: identifier of the infrastructure
- ``node_update_policy`` / ``edge_update_policy``: list of :doc:`update policies <update-policy>` for infrastructure resources
- ``update_policies``: list of :doc:`update policies <update-policy>` for infrastructure resources
- ``node_assets`` / ``edge_assets``: available capabilities (:doc:`asset <assets>` values) of nodes and links
- ``resource_init``: initialisation of resources (*min* or *max*)
- ``seed``: random seed for reproducibility
Expand All @@ -46,12 +54,30 @@ The two classes share many structural similarities, but differ in purpose and in

.. code-block:: python

from eclypse import policies
from eclypse.graph.application import Application

application = Application(
application_id="app",
node_update_policy=[...],
edge_update_policy=[...],
update_policies=[
policies.after(
50,
policies.degrade.reduce(
factor=0.6,
epochs=200,
node_assets=["cpu", "ram"],
edge_assets=["bandwidth"],
),
),
policies.after(
50,
policies.degrade.increase(
factor=1.6667,
epochs=200,
edge_assets=["latency"],
),
),
],
node_assets=[...],
edge_assets=[...],
requirement_init="min",
Expand All @@ -62,7 +88,7 @@ The two classes share many structural similarities, but differ in purpose and in
**Key parameters:**

- ``application_id``: identifier of the application
- ``node_update_policy`` / ``edge_update_policy``: list of :doc:`update policies <update-policy>` for application requirements
- ``update_policies``: list of :doc:`update policies <update-policy>` for application requirements
- ``node_assets`` / ``edge_assets``: resource requirements (:doc:`asset <assets>` values) of services and links
- ``requirement_init``: initialisation of resources (*min* or *max*)
- ``seed``: random seed for reproducibility
Expand Down
177 changes: 156 additions & 21 deletions docs/source/overview/concepts/update-policy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,188 @@ In ECLYPSE, an update policy is a function that defines how the state of the
infrastructure or application evolves over time. It enables dynamic
simulations by modifying node or edge assets at each simulation step.

Unlike assets, update policies are not classes. Instead, they are simple functions with a fixed signature, depending on whether they operate on nodes or edges.
Unlike assets, update policies are not tied to separate node- or edge-specific
interfaces. They are simple graph-oriented callables that receive the graph
being updated.

Function Signature
------------------

There are two kinds of update policies:
.. code-block:: python

from eclypse.graph import AssetGraph

def my_policy(graph: AssetGraph):
...

The graph exposes the standard `networkx` views through ``graph.nodes`` and
``graph.edges``. Each node or edge has an associated data dictionary containing
its current asset values.

Built-in Policies
-----------------

ECLYPSE also provides a catalogue of off-the-shelf policies in
:mod:`eclypse.policies`. The module groups reusable policies into a few common
families:

- **Node update policies**:
- **failure**: availability flapping, node failures, and latency spikes
- **noise**: bounded random walks, momentum walks, and impulse shocks
- **distribution**: uniform, normal, lognormal, triangular, beta, gamma,
truncated-normal, and categorical multiplicative perturbations
- **degrade**: progressive increase or reduction of selected assets through
explicit ``increase()`` and ``reduce()`` policies
- **replay**: replay of node or edge values from records, dataframes, or parquet files
- **schedule**: wrappers such as ``every()``, ``after()``, ``between()``, and ``once_at()``

.. code-block:: python
For most simulations, the easiest workflow is to compose a few built-in
policies and only fall back to a custom callable when the behaviour is
scenario-specific.

def my_node_policy(nodes: NodeView):
...
Using Built-in Policies
-----------------------

Built-in policies are regular graph callables, so you use them exactly like any
custom update policy.

.. code-block:: python
:caption: **Example:** Infrastructure policies composed from ``eclypse.policies``

from eclypse import policies
from eclypse.graph import Infrastructure

infrastructure = Infrastructure(
"edge-cloud",
update_policies=[
policies.failure.availability_flap(
down_probability=0.02,
up_probability=0.5,
node_filter=lambda _, data: data["availability"] > 0,
),
policies.distribution.uniform(
node_assets=["cpu", "ram", "storage"],
edge_assets=["latency", "bandwidth"],
node_asset_distributions={
"cpu": (0.95, 1.05),
"ram": (0.9, 1.1),
"storage": (0.98, 1.02),
},
edge_asset_distributions={
"latency": (0.95, 1.05),
"bandwidth": (0.98, 1.02),
},
),
],
)

Selectors and Assets
--------------------

Most built-in policies separate **what** to change from **where** to change it.

- ``node_assets`` / ``edge_assets`` select which graph assets are updated
- ``node_ids`` / ``edge_ids`` target specific nodes or links
- ``node_filter`` / ``edge_filter`` let you target assets dynamically

.. code-block:: python
:caption: **Example:** Apply noise only to edge nodes and WAN links

- **Edge update policies**:
from eclypse import policies

.. code-block:: python
policy = policies.distribution.uniform(
node_assets=["cpu", "ram"],
edge_assets=["latency"],
node_filter=lambda node_id, data: data.get("tier") == "edge",
edge_filter=lambda source, target, data: data.get("kind") == "wan",
)

def my_edge_policy(edges: EdgeView):
...
Scheduling Policies
-------------------

Both `NodeView` and `EdgeView` are provided by the `networkx` library and behave like dictionaries over the graph structure. Each node or edge has an associated data dictionary containing asset instances.
In particular a node is a tuple of the form ``(node_id, node_data)``, where `node_id` is the node identifier and `node_data` is a dictionary containing the asset instances.
On the other hand, an edge is a tuple of the form ``(source_node_id, target_node_id, edge_data)``, where `source_node_id` and `target_node_id` are the identifiers of the source and target nodes, respectively, and `edge_data` is a dictionary containing the asset instances.
Scheduling wrappers let you activate a policy only during part of the run.

.. code-block:: python
:caption: **Example:** Start value adjustments after step 100

from eclypse import policies

update_policies = [
policies.after(
100,
policies.degrade.reduce(
factor=0.5,
epochs=200,
node_assets=["cpu", "ram", "storage"],
edge_assets=["bandwidth"],
),
),
policies.after(
100,
policies.degrade.increase(
factor=2.0,
epochs=200,
edge_assets=["latency"],
),
),
]

Replay Policies
---------------

Replay helpers are useful when you want the simulation to follow observed
or synthetic measurements over time.

.. code-block:: python
:caption: **Example:** Replay node load from a parquet trace

from eclypse import policies

replay_users = policies.replay.from_parquet(
"examples/user_distribution/dataset.parquet",
target="nodes",
node_id_column="node_id",
time_column="time",
value_columns=["user_count"],
start_step=0,
)

Writing Custom Policies
-----------------------

You can define your own update policies by modifying the relevant asset values within each node or edge.
You can define your own update policies by modifying the relevant asset values
within the graph.

.. code-block:: python
:caption: **Example:** A node policy that caps CPU to a fixed maximum

def cap_cpu(nodes: NodeView):
for _, data in nodes.items():
from eclypse.graph import AssetGraph

def cap_cpu(graph: AssetGraph):
for _, data in graph.nodes.items():
if "cpu" in data:
data["cpu"].value = min(data["cpu"].value, 2.0)
data["cpu"] = min(data["cpu"], 2.0)

.. code-block:: python
:caption: **Example:** An edge policy that increases latency:

def increase_latency(edges: EdgeView):
for _, _, data in edges:
from eclypse.graph import AssetGraph

def add_latency(graph: AssetGraph):
for _, _, data in graph.edges.data():
if "latency" in data:
data["latency"].value += 1.0
data["latency"] += 1.0

Custom vs built-in
------------------

Built-in policies are ideal for common patterns such as failures, distributions,
explicit value adjustments, and replay from traces. When an example or scenario couples
multiple effects in a very specific way, keeping a custom callable is still the
right choice. Several examples in the repository intentionally do that to
preserve their original behaviour.

.. important::

Update policies must always ensure that modified asset values remain consistent.
Use the asset's :py:meth:`~eclypse.graph.assets.asset.Asset.is_consistent()` method if needed. Otherwise, placement and simulation logic may occur on inconsistent data.
Use the asset's :py:meth:`~eclypse.graph.assets.asset.Asset.is_consistent()` method if needed.
Otherwise, placement and simulation logic may occur on inconsistent data.
5 changes: 3 additions & 2 deletions docs/source/overview/examples/echo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ nodes connected through links with different latency and bandwidth values.
.. literalinclude:: ../../../../examples/echo/infrastructure.py
:language: python

The infrastructure is also updated at each iteration through random node and
edge update policies, which simulate changing runtime conditions.
The infrastructure is also updated at each iteration through a graph update
policy that mutates both nodes and links to simulate changing runtime
conditions.

.. dropdown:: Update policy code

Expand Down
11 changes: 11 additions & 0 deletions docs/source/overview/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Examples
:hidden:

echo
off_the_shelf
sock_shop

The examples section complements the getting-started guides with runnable
Expand All @@ -29,6 +30,16 @@ subdirectory of the repository.
communication patterns.


.. grid-item::

.. card:: :octicon:`tools;1em;info` **Off-the-shelf**
:link-type: doc
:link: off_the_shelf

A local simulation composed only from built-in builders, policies,
and placement logic.


.. grid-item::

.. card:: :octicon:`package-dependents;1em;info` **SockShop**
Expand Down
Loading