Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion documentation/backend_documentation/requests_generator.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class RVConfig(BaseModel):
distribution: Literal["poisson", "normal", "gaussian"] = "poisson"
variance: float | None = None # required only for normal/gaussian

class SimulationInput(BaseModel):
class RqsGeneratorInput(BaseModel):
"""Define simulation inputs."""
avg_active_users: RVConfig
avg_request_per_minute_per_user: RVConfig
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from fastapi import APIRouter

from app.core.simulation.simulation_run import run_simulation
from app.schemas.simulation_input import SimulationInput
from app.schemas.requests_generator_input import RqsGeneratorInput
from app.schemas.simulation_output import SimulationOutput

router = APIRouter()

@router.post("/simulation")
async def event_loop_simulation(input_data: SimulationInput) -> SimulationOutput:
async def event_loop_simulation(input_data: RqsGeneratorInput) -> SimulationOutput:
"""Run the simulation and return aggregate KPIs."""
rng = np.random.default_rng()
return run_simulation(input_data, rng=rng)
Expand Down
179 changes: 170 additions & 9 deletions src/app/config/constants.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,184 @@
"""Application constants and configuration values."""
"""
Application-wide constants and configuration values.

This module groups all the *static* enumerations used by the FastSim backend
so that:

* JSON / YAML payloads can be strictly validated with Pydantic.
* Front-end and simulation engine share a single source of truth.
* Ruff, mypy and IDEs can leverage the strong typing provided by Enum classes.

**IMPORTANT:** Changing any enum *value* is a breaking-change for every
stored configuration file. Add new members whenever possible instead of
renaming existing ones.
"""

from enum import IntEnum, StrEnum

# ======================================================================
# CONSTANTS FOR THE REQUEST-GENERATOR COMPONENT
# ======================================================================


class TimeDefaults(IntEnum):
"""Default time-related constants (all in seconds)."""
"""
Default time-related constants (expressed in **seconds**).

These values are used when the user omits an explicit parameter. They also
serve as lower / upper bounds for validation for the requests generator.
"""

MIN_TO_SEC = 60 # 1 minute → 60 s
USER_SAMPLING_WINDOW = 60 # keep U(t) constant for 60 s, default
SIMULATION_TIME = 3_600 # run 1 h if user gives no other value
MIN_SIMULATION_TIME = 1800 # min simulation time
MIN_USER_SAMPLING_WINDOW = 1 # 1 second
MAX_USER_SAMPLING_WINDOW = 120 # 2 minutes
MIN_TO_SEC = 60 # 1 minute → 60 s
USER_SAMPLING_WINDOW = 60 # keep U(t) constant for 60 s
SIMULATION_TIME = 3_600 # run 1 h if user gives no value
MIN_SIMULATION_TIME = 1_800 # enforce at least 30 min
MIN_USER_SAMPLING_WINDOW = 1 # 1 s minimum
MAX_USER_SAMPLING_WINDOW = 120 # 2 min maximum


class Distribution(StrEnum):
"""Allowed probability distributions for an RVConfig."""
"""
Probability distributions accepted by :class:`~app.schemas.RVConfig`.

The *string value* is exactly the identifier that must appear in JSON
payloads. The simulation engine will map each name to the corresponding
random sampler (e.g. ``numpy.random.poisson``).
"""

POISSON = "poisson"
NORMAL = "normal"
LOG_NORMAL = "log_normal"
EXPONENTIAL = "exponential"

# ======================================================================
# CONSTANTS FOR ENDPOINT STEP DEFINITION (REQUEST-HANDLER)
# ======================================================================

# The JSON received by the API for an endpoint step is expected to look like:
#
# {
# "endpoint_name": "/predict",
# "kind": "io_llm",
# "metrics": {
# "cpu_time": 0.150,
# "necessary_ram": 256
# }
# }
#
# The Enum classes below guarantee that only valid *kind* and *metric* keys
# are accepted by the Pydantic schema.


class EndpointStepIO(StrEnum):
"""
I/O-bound operation categories that can occur inside an endpoint *step*.

.. list-table::
:header-rows: 1

* - Constant
- Meaning (executed by coroutine)
* - ``TASK_SPAWN``
- Spawns an additional ``asyncio.Task`` and returns immediately.
* - ``LLM``
- Performs a remote Large-Language-Model inference call.
* - ``WAIT``
- Passive, *non-blocking* wait for I/O completion; no new task spawned.
* - ``DB``
- Round-trip to a relational / NoSQL database.
* - ``CACHE``
- Access to a local or distributed cache layer.

The *value* of each member (``"io_llm"``, ``"io_db"``, …) is the exact
identifier expected in external JSON.
"""

TASK_SPAWN = "io_task_spawn"
LLM = "io_llm"
WAIT = "io_wait"
DB = "io_db"
CACHE = "io_cache"


class EndpointStepCPU(StrEnum):
"""
CPU-bound operation categories inside an endpoint step.

Use these when the coroutine keeps the Python interpreter busy
(GIL-bound or compute-heavy code) rather than waiting for I/O.
"""

INITIAL_PARSING = "initial_parsing"
CPU_BOUND_OPERATION = "cpu_bound_operation"


class EndpointStepRAM(StrEnum):
"""
Memory-related operations inside a step.

Currently limited to a single category, but kept as an Enum so that future
resource types (e.g. GPU memory) can be added without schema changes.
"""

RAM = "ram"


class Metrics(StrEnum):
"""
Keys used inside the ``metrics`` dictionary of a *step*.

* ``NETWORK_LATENCY`` - Mean latency (seconds) incurred on a network edge
*outside* the service (used mainly for validation when steps model
short in-service hops).
* ``CPU_TIME`` - Service time (seconds) during which the coroutine occupies
the CPU / GIL.
* ``NECESSARY_RAM`` - Peak memory (MB) required by the step.
"""

NETWORK_LATENCY = "network_latency"
CPU_TIME = "cpu_time"
IO_WAITING_TIME = "io_waiting_time"
NECESSARY_RAM = "necessary_ram"

# ======================================================================
# CONSTANTS FOR THE RESOURCES OF A SERVER
# ======================================================================

class ServerResourcesDefaults:
"""Resources available for a single server"""

CPU_CORES = 1
MINIMUM_CPU_CORES = 1
RAM_MB = 1024
MINIMUM_RAM_MB = 256
DB_CONNECTION_POOL = None

# ======================================================================
# CONSTANTS FOR THE MACRO-TOPOLOGY GRAPH
# ======================================================================

class SystemNodes(StrEnum):
"""
High-level node categories of the system topology graph.

Each member represents a *macro-component* that may have its own SimPy
resources (CPU cores, DB pool, etc.).
"""

SERVER = "server"
CLIENT = "client"
LOAD_BALANCER = "load_balancer"
API_GATEWAY = "api_gateway"
DATABASE = "database"
CACHE = "cache"


class SystemEdges(StrEnum):
"""
Edge categories connecting different :class:`SystemNodes`.

Currently only network links are modeled; new types (IPC queue, message
bus, stream) can be added without impacting existing payloads.
"""

NETWORK_CONNECTION = "network_connection"
4 changes: 2 additions & 2 deletions src/app/core/event_samplers/gaussian_poisson.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
truncated_gaussian_generator,
uniform_variable_generator,
)
from app.schemas.requests_generator_input import SimulationInput
from app.schemas.requests_generator_input import RqsGeneratorInput


def gaussian_poisson_sampling(
input_data: SimulationInput,
input_data: RqsGeneratorInput,
*,
rng: np.random.Generator | None = None,
) -> Generator[float, None, None]:
Expand Down
4 changes: 2 additions & 2 deletions src/app/core/event_samplers/poisson_poisson.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
poisson_variable_generator,
uniform_variable_generator,
)
from app.schemas.requests_generator_input import SimulationInput
from app.schemas.requests_generator_input import RqsGeneratorInput


def poisson_poisson_sampling(
input_data: SimulationInput,
input_data: RqsGeneratorInput,
*,
rng: np.random.Generator | None = None,
) -> Generator[float, None, None]:
Expand Down
4 changes: 2 additions & 2 deletions src/app/core/simulation/requests_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@

import numpy as np

from app.schemas.requests_generator_input import SimulationInput
from app.schemas.requests_generator_input import RqsGeneratorInput


def requests_generator(
input_data: SimulationInput,
input_data: RqsGeneratorInput,
*,
rng: np.random.Generator | None = None,
) -> Generator[float, None, None]:
Expand Down
4 changes: 2 additions & 2 deletions src/app/core/simulation/simulation_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@

import numpy as np

from app.schemas.simulation_input import SimulationInput
from app.schemas.requests_generator_input import RqsGeneratorInput




def run_simulation(
input_data: SimulationInput,
input_data: RqsGeneratorInput,
*,
rng: np.random.Generator,
) -> SimulationOutput:
Expand Down
13 changes: 13 additions & 0 deletions src/app/schemas/full_simulation_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Definition of the full input for the simulation"""

from pydantic import BaseModel

from app.schemas.requests_generator_input import RqsGeneratorInput
from app.schemas.system_topology_schema.full_system_topology_schema import TopologyGraph


class SimulationPayload(BaseModel):
"""Full input structure to perform a simulation"""

rqs_input: RqsGeneratorInput
topology_graph: TopologyGraph
31 changes: 31 additions & 0 deletions src/app/schemas/random_variables_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Definition of the schema for a Random variable"""

from pydantic import BaseModel, field_validator, model_validator

from app.config.constants import Distribution


class RVConfig(BaseModel):
"""class to configure random variables"""

mean: float
distribution: Distribution = Distribution.POISSON
variance: float | None = None

@field_validator("mean", mode="before")
def ensure_mean_is_numeric(
cls, # noqa: N805
v: object,
) -> float:
"""Ensure `mean` is numeric, then coerce to float."""
err_msg = "mean must be a number (int or float)"
if not isinstance(v, (float, int)):
raise ValueError(err_msg) # noqa: TRY004
return float(v)

@model_validator(mode="after") # type: ignore[arg-type]
def default_variance(cls, model: "RVConfig") -> "RVConfig": # noqa: N805
"""Set variance = mean when distribution == 'normal' and variance is missing."""
if model.variance is None and model.distribution == Distribution.NORMAL:
model.variance = model.mean
return model
32 changes: 4 additions & 28 deletions src/app/schemas/requests_generator_input.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,13 @@
"""Define the schemas for the simulator"""


from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic import BaseModel, Field

from app.config.constants import Distribution, TimeDefaults
from app.config.constants import TimeDefaults
from app.schemas.random_variables_config import RVConfig


class RVConfig(BaseModel):
"""class to configure random variables"""

mean: float
distribution: Distribution = Distribution.POISSON
variance: float | None = None

@field_validator("mean", mode="before")
def ensure_mean_is_numeric(
cls, # noqa: N805
v: object,
) -> float:
"""Ensure `mean` is numeric, then coerce to float."""
err_msg = "mean must be a number (int or float)"
if not isinstance(v, (float, int)):
raise ValueError(err_msg) # noqa: TRY004
return float(v)

@model_validator(mode="after") # type: ignore[arg-type]
def default_variance(cls, model: "RVConfig") -> "RVConfig": # noqa: N805
"""Set variance = mean when distribution == 'normal' and variance is missing."""
if model.variance is None and model.distribution == Distribution.NORMAL:
model.variance = model.mean
return model

class SimulationInput(BaseModel):
class RqsGeneratorInput(BaseModel):
"""Define the expected variables for the simulation"""

avg_active_users: RVConfig
Expand Down
1 change: 1 addition & 0 deletions src/app/schemas/simulation_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class SimulationOutput(BaseModel):
"""Define the output of the simulation"""

total_requests: dict[str, int | float]
# TO DEFINE
metric_2: str
#......
metric_n: str
Loading