From b02da08538a169df5f35c86fbfb5c1f401ee49bd Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Wed, 5 Feb 2025 16:53:59 +0200 Subject: [PATCH 01/13] Add preprocessing runtime to runtime report --- pyproject.toml | 2 +- src/simod/cli.py | 8 +++++++- src/simod/runtime_meter.py | 1 + src/simod/simod.py | 4 ++-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6a020991..6f130546 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "simod" -version = "5.1.2" +version = "5.1.3" authors = [ "Ihar Suvorau ", "David Chapela ", diff --git a/src/simod/cli.py b/src/simod/cli.py index 670e6862..d161c13c 100644 --- a/src/simod/cli.py +++ b/src/simod/cli.py @@ -7,6 +7,7 @@ from pix_framework.filesystem.file_manager import get_random_folder_id from simod.event_log.event_log import EventLog +from simod.runtime_meter import RuntimeMeter from simod.settings.simod_settings import SimodSettings from simod.simod import Simod @@ -87,7 +88,11 @@ def main( output = output if output is not None else (Path.cwd() / "outputs" / get_random_folder_id()).absolute() + # To measure the runtime of each stage + runtimes = RuntimeMeter() + # Read and preprocess event log + runtimes.start(RuntimeMeter.PREPROCESSING) event_log = EventLog.from_path( log_ids=settings.common.log_ids, train_log_path=settings.common.train_log_path, @@ -95,10 +100,11 @@ def main( preprocessing_settings=settings.preprocessing, need_test_partition=settings.common.perform_final_evaluation, ) + runtimes.stop(RuntimeMeter.PREPROCESSING) # Instantiate and run Simod simod = Simod(settings, event_log=event_log, output_dir=output) - simod.run() + simod.run(runtimes=runtimes) if __name__ == "__main__": diff --git a/src/simod/runtime_meter.py b/src/simod/runtime_meter.py index dc108cd0..bb4d38fb 100644 --- a/src/simod/runtime_meter.py +++ b/src/simod/runtime_meter.py @@ -9,6 +9,7 @@ class RuntimeMeter: runtimes: dict TOTAL: str = "SIMOD_TOTAL_RUNTIME" + PREPROCESSING: str = "preprocessing" INITIAL_MODEL: str = "discover-initial-BPS-model" CONTROL_FLOW_MODEL: str = "optimize-control-flow-model" RESOURCE_MODEL: str = "optimize-resource-model" diff --git a/src/simod/simod.py b/src/simod/simod.py index d2036d47..cd4d81f1 100644 --- a/src/simod/simod.py +++ b/src/simod/simod.py @@ -83,13 +83,13 @@ def __init__( self._best_result_dir = self._output_dir / "best_result" create_folder(self._best_result_dir) - def run(self): + def run(self, runtimes: Optional[RuntimeMeter] = None): """ Optimizes the BPS model with the given event log and settings. """ # Runtime object - runtimes = RuntimeMeter() + runtimes = RuntimeMeter() if runtimes is None else runtimes runtimes.start(RuntimeMeter.TOTAL) # Model activities might be different from event log activities if the model has been provided, From f79d94e421db68c8cf9272c491ac1f5f8e750088 Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 01:38:22 +0200 Subject: [PATCH 02/13] Update __init__.py files --- src/simod/__init__.py | 1 + src/simod/settings/__init__.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/simod/__init__.py b/src/simod/__init__.py index e69de29b..e7b5ac10 100644 --- a/src/simod/__init__.py +++ b/src/simod/__init__.py @@ -0,0 +1 @@ +__all__ = ["simod"] diff --git a/src/simod/settings/__init__.py b/src/simod/settings/__init__.py index c09f0f03..9aab2dd8 100644 --- a/src/simod/settings/__init__.py +++ b/src/simod/settings/__init__.py @@ -1,4 +1,5 @@ __all__ = [ + "common_settings", "control_flow_settings", "extraneous_delays_settings", "simod_settings", From 5832db3de059fea8a125f9123c49d582eeab3d6e Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 01:38:42 +0200 Subject: [PATCH 03/13] Add docstrings for Settings module --- src/simod/settings/common_settings.py | 99 +++++++++++++++++++ src/simod/settings/control_flow_settings.py | 97 +++++++++++++++++- .../settings/extraneous_delays_settings.py | 41 ++++++++ src/simod/settings/preprocessing_settings.py | 41 ++++++++ src/simod/settings/resource_model_settings.py | 66 ++++++++++++- src/simod/settings/simod_settings.py | 86 ++++++++++++++-- 6 files changed, 422 insertions(+), 8 deletions(-) diff --git a/src/simod/settings/common_settings.py b/src/simod/settings/common_settings.py index d1a5a2c9..1e30821d 100644 --- a/src/simod/settings/common_settings.py +++ b/src/simod/settings/common_settings.py @@ -14,6 +14,31 @@ class Metric(str, Enum): + """ + Enum class storing the metrics used to evaluate the quality of a BPS model. + + Attributes + ---------- + DL : str + Control-flow Log Distance metric based in the Damerau-Levenshtein distance. + TWO_GRAM_DISTANCE : str + Two-gram distance metric. + THREE_GRAM_DISTANCE : str + Three-gram distance metric. + CIRCADIAN_EMD : str + Earth Mover's Distance (EMD) for circadian event distribution. + CIRCADIAN_WORKFORCE_EMD : str + EMD for circadian workforce distribution. + ARRIVAL_EMD : str + EMD for arrival event distribution. + RELATIVE_EMD : str + EMD for relative event distribution. + ABSOLUTE_EMD : str + EMD for absolute event distribution. + CYCLE_TIME_EMD : str + EMD for cycle time distribution. + """ + DL = "dl" TWO_GRAM_DISTANCE = "two_gram_distance" THREE_GRAM_DISTANCE = "three_gram_distance" @@ -26,6 +51,26 @@ class Metric(str, Enum): @classmethod def from_str(cls, value: Union[str, List[str]]) -> "Union[Metric, List[Metric]]": + """ + Converts a string (or list of strings) representing metric names into an instance (or list of instances) + of the :class:`Metric` enum. + + Parameters + ---------- + value : Union[str, List[str]] + A string representing a metric name or a list of metric names. + + Returns + ------- + Union[:class:`Metric`, List[:class:`Metric`]] + An instance of :class:`Metric` if a single string is provided, + or a list of :class:`Metric` instances if a list of strings is provided. + + Raises + ------ + ValueError + If the provided string does not match any metric name. + """ if isinstance(value, str): return Metric._from_str(value) elif isinstance(value, list): @@ -83,6 +128,36 @@ def __str__(self): class CommonSettings(BaseModel): + """ + General configuration parameters of SIMOD and parameters common to all pipeline stages + + Attributes + ---------- + train_log_path : :class:`~pathlib.Path` + Path to the training log (the one used to discover the BPS model). + log_ids : :class:`EventLogIDs` + Dataclass storing the mapping between the column names in the CSV and their role (case_id, activity, etc.). + test_log_path : :class:`~pathlib.Path`, optional + Path to the event log to perform the final evaluation of the discovered BPS model (if desired). + process_model_path : :class:`~pathlib.Path`, optional + Path to the BPMN model for the control-flow (skip its discovery and use this one). + perform_final_evaluation : bool + Boolean indicating whether to perform the final evaluation of the discovered BPS model. + If true, either use the event log in [test_log_path] if specified, or split the training log to obtain a + testing set. + num_final_evaluations : int + Number of replications of the final evaluation to perform. + evaluation_metrics : list + List of :class:`Metric` evaluation metrics to use in the final evaluation. + use_observed_arrival_distribution : bool + Boolean indicating whether to use the distribution of observed case arrival times (true), or to discover a + probability distribution function to model them (false). + clean_intermediate_files : bool + Boolean indicating whether to delete all intermediate created files. + discover_data_attributes : bool + Boolean indicating whether to discover data attributes and their creation/update rules. + + """ # Log & Model parameters train_log_path: Path = Path("default_path.csv") log_ids: EventLogIDs = PROSIMOS_LOG_IDS @@ -99,6 +174,22 @@ class CommonSettings(BaseModel): @staticmethod def from_dict(config: dict, config_dir: Optional[Path] = None) -> "CommonSettings": + """ + Instantiates the SIMOD common configuration from a dictionary. + + Parameters + ---------- + config : dict + Dictionary with the configuration values for the SIMOD common parameters. + config_dir : :class:`~pathlib.Path`, optional + If the path to the event log(s) is specified in a relative manner, ``[config_dir]`` is used to complete + such paths. If ``None``, relative paths are complemented with the current directory. + + Returns + ------- + :class:`CommonSettings` + Instance of the SIMOD common configuration for the specified dictionary values. + """ base_files_dir = config_dir or Path.cwd() # Training log path @@ -181,6 +272,14 @@ def from_dict(config: dict, config_dir: Optional[Path] = None) -> "CommonSetting ) def to_dict(self) -> dict: + """ + Translate the common configuration stored in this instance into a dictionary. + + Returns + ------- + dict + Python dictionary storing this configuration. + """ return { "train_log_path": str(self.train_log_path), "test_log_path": str(self.test_log_path) if self.test_log_path is not None else None, diff --git a/src/simod/settings/control_flow_settings.py b/src/simod/settings/control_flow_settings.py index a01ee6f9..d86c7803 100644 --- a/src/simod/settings/control_flow_settings.py +++ b/src/simod/settings/control_flow_settings.py @@ -9,11 +9,46 @@ class ProcessModelDiscoveryAlgorithm(str, Enum): + """ + Enumeration of process model discovery algorithms. + + This enum defines the available algorithms for discovering process models from event logs. + + Attributes + ---------- + SPLIT_MINER_V1 : str + Represents the first version of the Split Miner algorithm (`"sm1"`). + SPLIT_MINER_V2 : str + Represents the second version of the Split Miner algorithm (`"sm2"`). + """ + SPLIT_MINER_V1 = "sm1" SPLIT_MINER_V2 = "sm2" @classmethod def from_str(cls, value: str) -> "ProcessModelDiscoveryAlgorithm": + """ + Converts a string representation of a process model discovery algorithm + into the corresponding :class:`ProcessModelDiscoveryAlgorithm` instance. + + This method allows flexible input formats for each algorithm, supporting + multiple variations of their names. + + Parameters + ---------- + value : str + A string representing a process model discovery algorithm. + + Returns + ------- + :class:`ProcessModelDiscoveryAlgorithm` + The corresponding enum instance for the given algorithm name. + + Raises + ------ + ValueError + If the provided string does not match any known algorithm. + """ if value.lower() in [ "sm2", "splitminer2", @@ -51,7 +86,37 @@ def __str__(self): class ControlFlowSettings(BaseModel): """ - Control-flow optimization settings. + Control-flow model configuration parameters. + + This class defines configurable parameters for optimizing the control-flow structure + of a discovered process model, including metric selection, iteration settings, + and various discovery algorithm parameters. + + Attributes + ---------- + optimization_metric : :class:`Metric` + The metric used to evaluate process model quality at each iteration of the optimization process (i.e., + loss function). + num_iterations : int + The number of optimization iterations to perform. + num_evaluations_per_iteration : int + The number of replications for the evaluations of each iteration. + gateway_probabilities : Union[:class:`GatewayProbabilitiesDiscoveryMethod`, List[:class:`GatewayProbabilitiesDiscoveryMethod`]] + Method(s) used to discover gateway probabilities. + mining_algorithm : Optional[:class:`ProcessModelDiscoveryAlgorithm`] + The process model discovery algorithm to use. + epsilon : Optional[Union[float, Tuple[float, float]]] + Number of concurrent relations between events to be captured in the discovery algorithm (between 0.0 and 1.0). + eta : Optional[Union[float, Tuple[float, float]]] + Threshold for filtering the incoming and outgoing edges in the discovery algorithm (between 0.0 and 1.0). + replace_or_joins : Optional[Union[bool, List[bool]]] + Whether to replace non-trivial OR joins. + prioritize_parallelism : Optional[Union[bool, List[bool]]] + Whether to prioritize parallelism over loops. + discover_branch_rules : Optional[bool] + Whether to discover branch rules for gateways. + f_score : Optional[Union[float, Tuple[float, float]]] + Minimum f-score value to consider the discovered data-aware branching rules. """ optimization_metric: Metric = Metric.THREE_GRAM_DISTANCE @@ -70,6 +135,15 @@ class ControlFlowSettings(BaseModel): @staticmethod def one_shot() -> "ControlFlowSettings": + """ + Instantiates the control-flow model configuration for the one-shot mode (i.e., no optimization, one single + iteration). + + Returns + ------- + :class:`ControlFlowSettings` + Instance of the control-flow model configuration for the one-shot mode. + """ return ControlFlowSettings( optimization_metric=Metric.THREE_GRAM_DISTANCE, num_iterations=1, @@ -84,6 +158,19 @@ def one_shot() -> "ControlFlowSettings": @staticmethod def from_dict(config: dict) -> "ControlFlowSettings": + """ + Instantiates the control-flow model configuration from a dictionary. + + Parameters + ---------- + config : dict + Dictionary with the configuration values for the control-flow model parameters. + + Returns + ------- + :class:`ControlFlowSettings` + Instance of the control-flow model configuration for the specified dictionary values. + """ optimization_metric = Metric.from_str(config.get("optimization_metric", "n_gram_distance")) num_iterations = config.get("num_iterations", 10) num_evaluations_per_iteration = config.get("num_evaluations_per_iteration", 3) @@ -123,6 +210,14 @@ def from_dict(config: dict) -> "ControlFlowSettings": ) def to_dict(self) -> dict: + """ + Translate the control-flow model configuration stored in this instance into a dictionary. + + Returns + ------- + dict + Python dictionary storing this configuration. + """ dictionary = { "optimization_metric": self.optimization_metric.value, "num_iterations": self.num_iterations, diff --git a/src/simod/settings/extraneous_delays_settings.py b/src/simod/settings/extraneous_delays_settings.py index 4d3d0a20..b1f4bb81 100644 --- a/src/simod/settings/extraneous_delays_settings.py +++ b/src/simod/settings/extraneous_delays_settings.py @@ -8,6 +8,26 @@ class ExtraneousDelaysSettings(BaseModel): + """ + Configuration settings for extraneous delay optimization. + + This class defines parameters for discovering and optimizing extraneous + delays in process simulations, including optimization metrics, discovery + methods, and iteration settings. + + Attributes + ---------- + optimization_metric : :class:`ExtraneousDelaysOptimizationMetric` + The metric used to evaluate process model quality at each iteration of the optimization process (i.e., + loss function). + num_iterations : int + The number of optimization iterations to perform. + num_evaluations_per_iteration : int + The number of replications for the evaluations of each iteration. + discovery_method : :class:`ExtraneousDelaysDiscoveryMethod` + The method used to discover extraneous delays. + """ + optimization_metric: ExtraneousDelaysOptimizationMetric = ExtraneousDelaysOptimizationMetric.RELATIVE_EMD discovery_method: ExtraneousDelaysDiscoveryMethod = ExtraneousDelaysDiscoveryMethod.COMPLEX num_iterations: int = 1 @@ -15,6 +35,19 @@ class ExtraneousDelaysSettings(BaseModel): @staticmethod def from_dict(config: dict) -> "ExtraneousDelaysSettings": + """ + Instantiates the extraneous delays model configuration from a dictionary. + + Parameters + ---------- + config : dict + Dictionary with the configuration values for the extraneous delays model parameters. + + Returns + ------- + :class:`ExtraneousDelaysSettings` + Instance of the extraneous delays model configuration for the specified dictionary values. + """ optimization_metric = ExtraneousDelaysSettings._match_metric( config.get("optimization_metric", "relative_event_distribution") ) @@ -30,6 +63,14 @@ def from_dict(config: dict) -> "ExtraneousDelaysSettings": ) def to_dict(self) -> dict: + """ + Translate the extraneous delays model configuration stored in this instance into a dictionary. + + Returns + ------- + dict + Python dictionary storing this configuration. + """ return { "optimization_metric": str(self.optimization_metric.name), "discovery_method": str(self.discovery_method.name), diff --git a/src/simod/settings/preprocessing_settings.py b/src/simod/settings/preprocessing_settings.py index 7d2811a9..9d593c45 100644 --- a/src/simod/settings/preprocessing_settings.py +++ b/src/simod/settings/preprocessing_settings.py @@ -3,12 +3,45 @@ class PreprocessingSettings(BaseModel): + """ + Configuration for event log preprocessing. + + This class defines parameters used to preprocess event logs before + SIMOD main pipeline, including concurrency threshold settings + and multitasking options. + + Attributes + ---------- + multitasking : bool + Whether to preprocess the event log to handle resources working in more than one activity at a time. + enable_time_concurrency_threshold : float + Threshold for determining concurrent events (for computing enabled) time based on the ratio of overlapping + w.r.t. their occurrences. Ranges from 0 to 1 (0.3 means that two activities will be considered concurrent + when their execution overlaps in 30% or more of the cases). + concurrency_thresholds : :class:`ConcurrencyThresholds` + Thresholds for the computation of the start times (if missing) based on the Heuristics miner algorithm, + including direct-follows (df), length-2-loops (l2l), and length-1-loops (l1l). + """ + multitasking: bool = False enable_time_concurrency_threshold: float = 0.5 concurrency_thresholds: ConcurrencyThresholds = ConcurrencyThresholds(df=0.75, l2l=0.9, l1l=0.9) @staticmethod def from_dict(config: dict) -> "PreprocessingSettings": + """ + Instantiates SIMOD preprocessing configuration from a dictionary. + + Parameters + ---------- + config : dict + Dictionary with the configuration values for the preprocessing parameters. + + Returns + ------- + :class:`PreprocessingSettings` + Instance of SIMOD preprocessing configuration for the specified dictionary values. + """ return PreprocessingSettings( multitasking=config.get("multitasking", False), enable_time_concurrency_threshold=config.get("enable_time_concurrency_threshold", 0.5), @@ -20,6 +53,14 @@ def from_dict(config: dict) -> "PreprocessingSettings": ) def to_dict(self) -> dict: + """ + Translate the preprocessing configuration stored in this instance into a dictionary. + + Returns + ------- + dict + Python dictionary storing this configuration. + """ return { "multitasking": self.multitasking, "enable_time_concurrency_threshold": self.enable_time_concurrency_threshold, diff --git a/src/simod/settings/resource_model_settings.py b/src/simod/settings/resource_model_settings.py index 4b29196a..03b71254 100644 --- a/src/simod/settings/resource_model_settings.py +++ b/src/simod/settings/resource_model_settings.py @@ -9,7 +9,41 @@ class ResourceModelSettings(BaseModel): """ - Resource Model optimization settings. + Configuration settings for resource model optimization. + + This class defines parameters for optimizing resource allocation and + scheduling in process simulations, including optimization metrics, + discovery methods, and statistical thresholds. + + Attributes + ---------- + optimization_metric : :class:`Metric` + The metric used to evaluate the quality of resource model optimization in each iteration (i.e., loss function). + num_iterations : int + The number of optimization iterations to perform. + num_evaluations_per_iteration : int + The number of replications for the evaluations of each iteration. + discovery_type : :class:`CalendarType` + Type of calendar discovery method used for resource modeling. + granularity : Optional[Union[int, Tuple[int, int]]] + Time granularity for calendar discovery, measured in minutes per granule (e.g., 60 will imply discovering + resource calendars with slots of 1 hour). + confidence : Optional[Union[float, Tuple[float, float]]] + Minimum confidence of the intervals in the discovered calendar of a resource or set of resources (between + 0.0 and 1.0) + support : Optional[Union[float, Tuple[float, float]]] + Minimum support of the intervals in the discovered calendar of a resource or set of resources (between 0.0 + and 1.0) + participation : Optional[Union[float, Tuple[float, float]]] + Participation of a resource in the process to discover a calendar for them, gathered together otherwise + (between 0.0 and 1.0) + fuzzy_angle : Optional[Union[float, Tuple[float, float]]] + Angle of the fuzzy trapezoid when computing the availability probability for an activity (angle from + start to end) + discover_prioritization_rules : bool + Whether to discover case prioritization rules. + discover_batching_rules : bool + Whether to discover batching rules for resource allocation. """ optimization_metric: Metric = Metric.CIRCADIAN_EMD @@ -26,6 +60,15 @@ class ResourceModelSettings(BaseModel): @staticmethod def one_shot() -> "ResourceModelSettings": + """ + Instantiates the resource model configuration for the one-shot mode (i.e., no optimization, one single + iteration). + + Returns + ------- + :class:`ResourceModelSettings` + Instance of the resource model configuration for the one-shot mode. + """ return ResourceModelSettings( optimization_metric=Metric.CIRCADIAN_EMD, num_iterations=1, @@ -42,6 +85,19 @@ def one_shot() -> "ResourceModelSettings": @staticmethod def from_dict(config: dict) -> "ResourceModelSettings": + """ + Instantiates the resource model configuration from a dictionary. + + Parameters + ---------- + config : dict + Dictionary with the configuration values for the resource model parameters. + + Returns + ------- + :class:`ResourceModelSettings` + Instance of the resource model configuration for the specified dictionary values. + """ optimization_metric = Metric.from_str(config.get("optimization_metric", "circadian_emd")) num_iterations = config.get("num_iterations", 10) num_evaluations_per_iteration = config.get("num_evaluations_per_iteration", 3) @@ -81,6 +137,14 @@ def from_dict(config: dict) -> "ResourceModelSettings": ) def to_dict(self) -> dict: + """ + Translate the resource model configuration stored in this instance into a dictionary. + + Returns + ------- + dict + Python dictionary storing this configuration. + """ # Parse general settings dictionary = { "optimization_metric": self.optimization_metric.value, diff --git a/src/simod/settings/simod_settings.py b/src/simod/settings/simod_settings.py index ccebdbf0..a43b2e02 100644 --- a/src/simod/settings/simod_settings.py +++ b/src/simod/settings/simod_settings.py @@ -18,8 +18,24 @@ class SimodSettings(BaseModel): """ - Simod configuration v4 with the settings for all the stages and optimizations. - If configuration is provided in v2, is transformed to v4. + SIMOD configuration v5 with the settings for all the stages and optimizations. + If configuration is provided in v2 or v4, it is automatically translated to v5. + + Attributes + ---------- + common : :class:`~simod.settings.common_settings.CommonSettings` + General configuration parameters of SIMOD and parameters common to all pipeline stages. + preprocessing : :class:`~simod.settings.preprocessing_settings.PreprocessingSettings` + Configuration parameters for the preprocessing stage of SIMOD. + control_flow : :class:`~simod.settings.control_flow.ControlFlowSettings` + Configuration parameters for the control-flow model discovery stage. + resource_model : :class:`~simod.settings.resource_model_settings.ResourceModelSettings` + Configuration parameters for the resource model discovery stage. + extraneous_activity_delays : :class:`~simod.settings.extraneous_delays_settings.ExtraneousDelaysSettings` + Configuration parameters for the extraneous delays model discovery stage. If not provided, the extraneous + delays are not discovered. + version : int + SIMOD version. """ common: CommonSettings = CommonSettings() @@ -32,8 +48,12 @@ class SimodSettings(BaseModel): @staticmethod def default() -> "SimodSettings": """ - Default configuration for Simod. Used mostly for testing purposes. Most of those settings should be discovered - by Simod automatically. + Default configuration for SIMOD. + + Returns + ------- + :class:`SimodSettings` + Instance of the SIMOD configuration with the default values. """ return SimodSettings( @@ -46,6 +66,15 @@ def default() -> "SimodSettings": @staticmethod def one_shot() -> "SimodSettings": + """ + Configuration for SIMOD one-shot. This mode runs SIMOD without optimizing each BPS model component (i.e., + directly discover each BPS model component with default parameters). + + Returns + ------- + :class:`SimodSettings` + Instance of the SIMOD configuration for one-shot mode. + """ return SimodSettings( common=CommonSettings(), preprocessing=PreprocessingSettings(), @@ -56,6 +85,22 @@ def one_shot() -> "SimodSettings": @staticmethod def from_yaml(config: dict, config_dir: Optional[Path] = None) -> "SimodSettings": + """ + Instantiates the SIMOD configuration from a dictionary following the expected YAML structure. + + Parameters + ---------- + config : dict + Dictionary with the configuration values for each of the SIMOD elements. + config_dir : :class:`~pathlib.Path`, optional + If the path to the event log(s) is specified in a relative manner, ``[config_dir]`` is used to complete + such paths. If ``None``, relative paths are complemented with the current directory. + + Returns + ------- + :class:`SimodSettings` + Instance of the SIMOD configuration for the specified dictionary values. + """ assert config["version"] in [2, 4, 5], "Configuration version must be 2, 4, or 5" # Transform from previous version to the latest if needed @@ -109,11 +154,32 @@ def from_yaml(config: dict, config_dir: Optional[Path] = None) -> "SimodSettings @staticmethod def from_path(file_path: Path) -> "SimodSettings": + """ + Instantiates the SIMOD configuration from a YAML file. + + Parameters + ---------- + file_path : :class:`~pathlib.Path` + Path to the YAML file storing the configuration. + + Returns + ------- + :class:`SimodSettings` + Instance of the SIMOD configuration for the specified YAML file. + """ with file_path.open() as f: config = yaml.safe_load(f) return SimodSettings.from_yaml(config, config_dir=file_path.parent) def to_dict(self) -> dict: + """ + Translate the SIMOD configuration stored in this instance into a dictionary. + + Returns + ------- + dict + Python dictionary storing this configuration. + """ dictionary = { "version": self.version, "common": self.common.to_dict(), @@ -128,8 +194,16 @@ def to_dict(self) -> dict: def to_yaml(self, output_dir: Path) -> Path: """ Saves the configuration to a YAML file in the provided output directory. - :param output_dir: Output directory. - :return: None. + + Parameters + ---------- + output_dir : :class:`~pathlib.Path` + Path to the output directory where to store the YAML file with the configuration. + + Returns + ------- + :class:`~pathlib.Path` + Path to the YAML file with the configuration. """ data = yaml.dump(self.to_dict(), sort_keys=False) output_path = output_dir / "configuration.yaml" From ac555a3000cc8de0fa99b0cd1420c37d0c97573e Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 01:38:52 +0200 Subject: [PATCH 04/13] Add docstrings for SIMOD class --- src/simod/simod.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/simod/simod.py b/src/simod/simod.py index cd4d81f1..2fd7da49 100644 --- a/src/simod/simod.py +++ b/src/simod/simod.py @@ -38,7 +38,18 @@ class Simod: """ - SIMOD optimization. + Class to run the full pipeline of SIMOD in order to discover a BPS model from an event log. + + Attributes + ---------- + settings : :class:`~simod.settings.SimodSettings` + Configuration to run SIMOD and all its stages. + event_log : :class:`~simod.event_log.EventLog` + EventLog class storing the preprocessed training, validation, and (optionally) test partitions. + output_dir : :class:`~pathlib.Path` + Path to the folder where to write all the SIMOD outputs. + final_bps_model : :class:`~simod.simulation.parameters.BPS_model.BPSModel` + Instance of the best BPS model discovered by SIMOD. """ # Event log with the train, validation and test logs. @@ -85,7 +96,26 @@ def __init__( def run(self, runtimes: Optional[RuntimeMeter] = None): """ - Optimizes the BPS model with the given event log and settings. + Executes the SIMOD pipeline to discover the BPS model that better reflects the behavior recorded in the input + event log based on the specified configuration. + + Parameters + ---------- + runtimes : :class:`~simod.runtime_meter.RuntimeMeter`, optional + Instance for tracking the runtime of the different stages in the SIMOD pipeline. When provided, SIMOD + pipeline stages will be tracked and reported along with stages previously tracked in the instance (e.g., + preprocessing). If not provided, the runtime tracking reported will only contain SIMOD stages. + + Returns + ------- + None + The method performs in-place execution of the pipeline and does not return a value. + + Notes + ----- + - This method generates all output files under the folder ``[output_dir]//best_result/``. + - This method updates internal attributes of the class, such as `final_bps_model`, with the best BPS model found + during the pipeline execution. """ # Runtime object From caf72f9704ff8c6e04639d27311c03652f703cbf Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 01:39:06 +0200 Subject: [PATCH 05/13] Add docstrings for EventLog module --- src/simod/event_log/event_log.py | 80 ++++++++++++++++++++++++++--- src/simod/event_log/preprocessor.py | 39 ++++++++++---- 2 files changed, 102 insertions(+), 17 deletions(-) diff --git a/src/simod/event_log/event_log.py b/src/simod/event_log/event_log.py index c15fb96c..f650b007 100644 --- a/src/simod/event_log/event_log.py +++ b/src/simod/event_log/event_log.py @@ -13,9 +13,26 @@ class EventLog: """ - Event log class that contains the log and its splits, and column names. - - Use static methods to create an EventLog from a path in other ways that are implemented. + Represents an event log containing process execution data and its partitioned subsets. + + This class provides functionality for storing and managing an event log, including + training, validation, and test partitions. It also supports exporting logs to XES format + and loading event logs from files. + + Attributes + ---------- + train_partition : :class:`pandas.DataFrame` + DataFrame containing the training partition of the event log. + validation_partition : :class:`pandas.DataFrame` + DataFrame containing the validation partition of the event log. + train_validation_partition : :class:`pandas.DataFrame` + DataFrame containing both training and validation data. + test_partition : :class:`pandas.DataFrame` + DataFrame containing the test partition of the event log, if available. + log_ids : :class:`EventLogIDs` + Identifiers for mapping column names in the event log. + process_name : str + The name of the business process associated with the event log, primarily used for file naming. """ train_partition: pd.DataFrame @@ -56,7 +73,34 @@ def from_path( split_ratio: float = 0.8, ) -> "EventLog": """ - Loads an event log from a file and does the log split for training, validation, and test. + Loads an event log from a file and performs partitioning into training, validation, and test subsets. + + Parameters + ---------- + train_log_path : :class:`pathlib.Path` + Path to the training event log file (CSV or CSV.GZ). + log_ids : :class:`EventLogIDs` + Identifiers for mapping column names in the event log. + preprocessing_settings : :class:`PreprocessingSettings`, optional + Settings for preprocessing the event log. + need_test_partition : Optional[bool] + Whether to create a test partition if a separate test log is not provided. + process_name : Optional[str] + Name of the business process. If not provided, it is inferred from the file name. + test_log_path : Optional[:class:`pathlib.Path`] + Path to the test event log file (CSV or CSV.GZ). If provided, the test log is loaded separately. + split_ratio : float, default=0.8 + Ratio for splitting training and validation partitions. + + Returns + ------- + :class:`EventLog` + An instance of :class:`EventLog` with training, validation, and test partitions. + + Raises + ------ + ValueError + If the specified training or test log has an unsupported file extension. """ # Check event log prerequisites if not train_log_path.name.endswith(".csv") and not train_log_path.name.endswith(".csv.gz"): @@ -108,25 +152,45 @@ def from_path( def train_to_xes(self, path: Path): """ - Saves the training log to a XES file. + Saves the training log to an XES file. + + Parameters + ---------- + path : :class:`pathlib.Path` + Destination path for the XES file. """ write_xes(self.train_partition, self.log_ids, path) def validation_to_xes(self, path: Path): """ - Saves the validation log to a XES file. + Saves the validation log to an XES file. + + Parameters + ---------- + path : :class:`pathlib.Path` + Destination path for the XES file. """ write_xes(self.validation_partition, self.log_ids, path) def train_validation_to_xes(self, path: Path): """ - Saves the validation log to a XES file. + Saves the combined training and validation log to an XES file. + + Parameters + ---------- + path : :class:`pathlib.Path` + Destination path for the XES file. """ write_xes(self.train_validation_partition, self.log_ids, path) def test_to_xes(self, path: Path): """ - Saves the test log to a XES file. + Saves the test log to an XES file. + + Parameters + ---------- + path : :class:`pathlib.Path` + Destination path for the XES file. """ write_xes(self.test_partition, self.log_ids, path) diff --git a/src/simod/event_log/preprocessor.py b/src/simod/event_log/preprocessor.py index 721f73ec..803417e4 100644 --- a/src/simod/event_log/preprocessor.py +++ b/src/simod/event_log/preprocessor.py @@ -28,7 +28,18 @@ class Settings: class Preprocessor: """ - Preprocessor executes event log pre-processing according to the `run()` arguments and returns the modified log back. + Handles event log pre-processing by executing various transformations + to estimate missing timestamps and adjust data for multitasking. + + This class modifies an input event log based on the specified settings + and returns the pre-processed log. + + Attributes + ---------- + log : :class:`pandas.DataFrame` + The event log stored as a DataFrame. + log_ids : :class:`EventLogIDs` + Identifiers for mapping column names in the event log. """ _log: pd.DataFrame @@ -46,14 +57,24 @@ def run( enable_time_concurrency_threshold: float = 0.75, ) -> pd.DataFrame: """ - Executes all pre-processing steps and updates the configuration if necessary. - - Start times discovery is always executed if the log does not contain the start time column. - - :param multitasking: Whether to adjust the timestamps for multitasking. - :param concurrency_thresholds: Thresholds for the Heuristics Miner to estimate start/enabled times. - :param enable_time_concurrency_threshold: Threshold for the Heuristics Miner to estimate enabled times. - :return: The pre-processed event log. + Executes event log pre-processing steps based on the specified parameters. + + This includes estimating missing start times, adjusting timestamps + for multitasking scenarios, and computing enabled times. + + Parameters + ---------- + multitasking : bool + Whether to adjust the timestamps for multitasking. + concurrency_thresholds : :class:`ConcurrencyThresholds`, optional + Thresholds for the Heuristics Miner to estimate start times. + enable_time_concurrency_threshold : float + Threshold for estimating enabled times. + + Returns + ------- + :class:`pandas.DataFrame` + The pre-processed event log. """ print_section("Pre-processing") From e63240fea8a55d196cf91f429f677de1a463aede Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 16:33:55 +0200 Subject: [PATCH 06/13] Add docstrings for Simulation module --- src/simod/simulation/parameters/BPS_model.py | 115 ++++++++++++++++++- src/simod/simulation/prosimos.py | 80 ++++++++++--- 2 files changed, 175 insertions(+), 20 deletions(-) diff --git a/src/simod/simulation/parameters/BPS_model.py b/src/simod/simulation/parameters/BPS_model.py index 6b29699b..77096909 100644 --- a/src/simod/simulation/parameters/BPS_model.py +++ b/src/simod/simulation/parameters/BPS_model.py @@ -38,7 +38,44 @@ @dataclass class BPSModel: """ - BPS model class containing all the components to simulate a business process model. + Represents a Business Process Simulation (BPS) model containing all necessary components + to simulate a business process. + + This class manages various elements such as the BPMN process model, resource configurations, + extraneous delays, case attributes, and prioritization/batching rules. It provides methods + to convert the model into a format compatible with Prosimos and handle activity ID mappings. + + Attributes + ---------- + process_model : Optional[:class:`pathlib.Path`] + Path to the BPMN process model file. + gateway_probabilities : Optional[List[:class:`GatewayProbabilities`]] + Probabilities for gateway-based process routing. + case_arrival_model : Optional[:class:`CaseArrivalModel`] + Model for the arrival of new cases in the simulation. + resource_model : Optional[:class:`ResourceModel`] + Model for the resources involved in the process, their working schedules, etc. + extraneous_delays : Optional[List[:class:`ExtraneousDelay`]] + A list of delays representing extraneous waiting times before/after activities. + case_attributes : Optional[List[:class:`CaseAttribute`]] + Case-level attributes and their update rules. + global_attributes : Optional[List[:class:`GlobalAttribute`]] + Global attributes and their update rules. + event_attributes : Optional[List[:class:`EventAttribute`]] + Event-level attributes and their update rules. + prioritization_rules : Optional[List[:class:`PrioritizationRule`]] + A set of case prioritization rules for process execution. + batching_rules : Optional[List[:class:`BatchingRule`]] + Rules defining how activities are batched together. + branch_rules : Optional[List[:class:`BranchRules`]] + Branching rules defining conditional flow behavior in decision points. + calendar_granularity : Optional[int] + Granularity of the resource calendar, expressed in minutes. + + Notes + ----- + - `to_prosimos_format` transforms the model into a dictionary format used by Prosimos. + - `replace_activity_names_with_ids` modifies activity references to use BPMN IDs instead of names. """ process_model: Optional[Path] = None # A path to the model for now, in future the loaded BPMN model @@ -55,6 +92,25 @@ class BPSModel: calendar_granularity: Optional[int] = None def to_prosimos_format(self) -> dict: + """ + Converts the BPS model into a dictionary format compatible with the Prosimos simulation engine. + + This method extracts all relevant process simulation attributes, including resource models, + delays, prioritization rules, and activity mappings, and structures them in a format + understood by Prosimos. + + Returns + ------- + dict + A dictionary representation of the BPS model, ready for simulation in Prosimos. + + Notes + ----- + - If the resource model contains a fuzzy calendar, the model type is set to "FUZZY"; + otherwise, it defaults to "CRISP". + - The function ensures activity labels are properly linked to their respective BPMN IDs. + """ + # Get map activity label -> node ID activity_label_to_id = get_activities_ids_by_name_from_bpmn(self.process_model) @@ -98,13 +154,42 @@ def to_prosimos_format(self) -> dict: return attributes def deep_copy(self) -> "BPSModel": + """ + Creates a deep copy of the current BPSModel instance. + + This ensures that modifying the copied instance does not affect the original. + + Returns + ------- + BPSModel + A new, independent copy of the current BPSModel instance. + + Notes + ----- + This method uses Python's `copy.deepcopy()` to create a full recursive copy of the model. + """ return copy.deepcopy(self) def replace_activity_names_with_ids(self): """ - Updates activity labels with activity IDs from the current (BPMN) process model. + Replaces activity names with their corresponding IDs from the BPMN process model. + + Prosimos requires activity references to be identified by their BPMN node IDs instead of + activity labels. This method updates: + + - Resource associations in the resource profiles. + - Activity-resource distributions. + - Event attributes referencing activity names. - In BPSModel, the activities are referenced by their name, Prosimos uses IDs instead from the BPMN model. + Raises + ------ + KeyError + If an activity name does not exist in the BPMN model. + + Notes + ----- + - This method modifies the model in place. + - It ensures compatibility with Prosimos by aligning activity references with BPMN IDs. """ # Get map activity label -> node ID activity_label_to_id = get_activities_ids_by_name_from_bpmn(self.process_model) @@ -128,6 +213,30 @@ def replace_activity_names_with_ids(self): event_attribute.event_id = activity_label_to_id[event_attribute.event_id] def to_json(self, output_dir: Path, process_name: str) -> Path: + """ + Saves the BPS model in a Prosimos-compatible JSON format. + + This method generates a structured JSON file containing all necessary simulation parameters, + ensuring that the model can be directly used by the Prosimos engine. + + Parameters + ---------- + output_dir : Path + The directory where the JSON file should be saved. + process_name : str + The name of the process, used for naming the output file. + + Returns + ------- + Path + The full path to the generated JSON file. + + Notes + ----- + - The JSON file is created in `output_dir` with a filename based on `process_name`. + - Uses `json.dump()` to serialize the model into a structured format. + - Ensures all attributes are converted into a valid Prosimos format before writing. + """ json_parameters_path = get_simulation_parameters_path(output_dir, process_name) with json_parameters_path.open("w") as f: diff --git a/src/simod/simulation/prosimos.py b/src/simod/simulation/prosimos.py index 4ddfc6e3..4738e41f 100644 --- a/src/simod/simulation/prosimos.py +++ b/src/simod/simulation/prosimos.py @@ -18,7 +18,22 @@ @dataclass class ProsimosSettings: - """Prosimos simulator settings.""" + """ + Configuration settings for running a Prosimos simulation. + + Attributes + ---------- + bpmn_path : :class:`pathlib.Path` + Path to the BPMN process model. + parameters_path : :class:`pathlib.Path` + Path to the Prosimos simulation parameters JSON file. + output_log_path : :class:`pathlib.Path` + Path to store the generated simulation log. + num_simulation_cases : int + Number of cases to simulate. + simulation_start : :class:`pandas.Timestamp` + Start timestamp for the simulation. + """ bpmn_path: Path parameters_path: Path @@ -29,9 +44,18 @@ class ProsimosSettings: def simulate(settings: ProsimosSettings): """ - Simulates a process model using Prosimos. - :param settings: Prosimos settings. - :return: None. + Runs a Prosimos simulation with the provided settings. + + Parameters + ---------- + settings : :class:`ProsimosSettings` + Configuration settings containing paths and parameters for the simulation. + + Notes + ----- + - The function prints the simulation settings and invokes `run_simulation()`. + - The labels of the start event, end event, and event timers are**not** recorded to the output log. + - The simulation generates a process log stored in `settings.output_log_path`. """ print_message(f"Simulation settings: {settings}") @@ -58,19 +82,41 @@ def simulate_and_evaluate( num_simulations: int = 1, ) -> List[dict]: """ - Simulates a process model using Prosimos num_simulations times in parallel. - - :param process_model_path: Path to the BPMN model. - :param parameters_path: Path to the Prosimos parameters. - :param output_dir: Path to the output directory for simulated logs. - :param simulation_cases: Number of cases to simulate. - :param simulation_start_time: Start time of the simulation. - :param validation_log: Validation log. - :param validation_log_ids: Validation log IDs. - :param metrics: Metrics to evaluate the simulated logs with. - :param num_simulations: Number of simulations to run in parallel. Default: 1. More simulations increase - the accuracy of evaluation metrics. - :return: Evaluation metrics. + Simulates a process model using Prosimos multiple times and evaluates the results. + + This function runs the simulation `num_simulations` times in parallel, + compares the generated logs with a validation log, and evaluates them using provided metrics. + + Parameters + ---------- + process_model_path : :class:`Path` + Path to the BPMN process model. + parameters_path : :class:`Path` + Path to the Prosimos simulation parameters JSON file. + output_dir : :class:`Path` + Directory where simulated logs will be stored. + simulation_cases : int + Number of cases to simulate per run. + simulation_start_time : :class:`pandas.Timestamp` + Start timestamp for the simulation. + validation_log : :class:`pandas.DataFrame` + The actual event log to compare against. + validation_log_ids : :class:`EventLogIDs` + Column mappings for identifying events in the validation log. + metrics : List[:class:`simod.settings.common_settings.Metric`] + A list of metrics used to evaluate the simulated logs. + num_simulations : int, optional + Number of parallel simulation runs (default is 1). + + Returns + ------- + List[dict] + A list of evaluation results, one for each simulated log. + + Notes + ----- + - Uses multiprocessing to speed up simulation when `num_simulations > 1`. + - Simulated logs are automatically compared with `validation_log`. """ simulation_log_paths = simulate_in_parallel( From 8ec60bd3a2032da0143e34a182c85d6407e82bd5 Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 16:34:59 +0200 Subject: [PATCH 07/13] Update docstrings for Configuration/Settings module --- src/simod/settings/control_flow_settings.py | 21 +++++++++------- .../settings/extraneous_delays_settings.py | 3 ++- src/simod/settings/resource_model_settings.py | 24 ++++++++++--------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/simod/settings/control_flow_settings.py b/src/simod/settings/control_flow_settings.py index d86c7803..9074efe2 100644 --- a/src/simod/settings/control_flow_settings.py +++ b/src/simod/settings/control_flow_settings.py @@ -88,9 +88,10 @@ class ControlFlowSettings(BaseModel): """ Control-flow model configuration parameters. - This class defines configurable parameters for optimizing the control-flow structure - of a discovered process model, including metric selection, iteration settings, - and various discovery algorithm parameters. + This class defines the ranges of the configurable parameters for optimizing the control-flow + structure of a discovered process model, including metric selection, iteration settings, + and various discovery algorithm parameters. In each iteration of the optimization process, the + parameters are sampled from these values or ranges. Attributes ---------- @@ -102,21 +103,23 @@ class ControlFlowSettings(BaseModel): num_evaluations_per_iteration : int The number of replications for the evaluations of each iteration. gateway_probabilities : Union[:class:`GatewayProbabilitiesDiscoveryMethod`, List[:class:`GatewayProbabilitiesDiscoveryMethod`]] - Method(s) used to discover gateway probabilities. + Fixed method or list of methods to use in each iteration to discover gateway probabilities. mining_algorithm : Optional[:class:`ProcessModelDiscoveryAlgorithm`] The process model discovery algorithm to use. epsilon : Optional[Union[float, Tuple[float, float]]] - Number of concurrent relations between events to be captured in the discovery algorithm (between 0.0 and 1.0). + Fixed number or range for the number of concurrent relations between events to be captured in the discovery + algorithm (between 0.0 and 1.0). eta : Optional[Union[float, Tuple[float, float]]] - Threshold for filtering the incoming and outgoing edges in the discovery algorithm (between 0.0 and 1.0). + Fixed number or range for the threshold for filtering the incoming and outgoing edges in the discovery + algorithm (between 0.0 and 1.0). replace_or_joins : Optional[Union[bool, List[bool]]] - Whether to replace non-trivial OR joins. + Fixed value or list for whether to replace non-trivial OR joins. prioritize_parallelism : Optional[Union[bool, List[bool]]] - Whether to prioritize parallelism over loops. + Fixed value or list for whether to prioritize parallelism over loops. discover_branch_rules : Optional[bool] Whether to discover branch rules for gateways. f_score : Optional[Union[float, Tuple[float, float]]] - Minimum f-score value to consider the discovered data-aware branching rules. + Fixed value or range for the minimum f-score value to consider the discovered data-aware branching rules. """ optimization_metric: Metric = Metric.THREE_GRAM_DISTANCE diff --git a/src/simod/settings/extraneous_delays_settings.py b/src/simod/settings/extraneous_delays_settings.py index b1f4bb81..ffed2d20 100644 --- a/src/simod/settings/extraneous_delays_settings.py +++ b/src/simod/settings/extraneous_delays_settings.py @@ -13,7 +13,8 @@ class ExtraneousDelaysSettings(BaseModel): This class defines parameters for discovering and optimizing extraneous delays in process simulations, including optimization metrics, discovery - methods, and iteration settings. + methods, and iteration settings. In each iteration of the optimization process, the + parameters are sampled from these values or ranges. Attributes ---------- diff --git a/src/simod/settings/resource_model_settings.py b/src/simod/settings/resource_model_settings.py index 03b71254..daf84fdb 100644 --- a/src/simod/settings/resource_model_settings.py +++ b/src/simod/settings/resource_model_settings.py @@ -13,7 +13,8 @@ class ResourceModelSettings(BaseModel): This class defines parameters for optimizing resource allocation and scheduling in process simulations, including optimization metrics, - discovery methods, and statistical thresholds. + discovery methods, and statistical thresholds. In each iteration of the optimization process, the + parameters are sampled from these values or ranges. Attributes ---------- @@ -26,20 +27,21 @@ class ResourceModelSettings(BaseModel): discovery_type : :class:`CalendarType` Type of calendar discovery method used for resource modeling. granularity : Optional[Union[int, Tuple[int, int]]] - Time granularity for calendar discovery, measured in minutes per granule (e.g., 60 will imply discovering - resource calendars with slots of 1 hour). + Fixed value or range for the time granularity for calendar discovery, measured in minutes per granule (e.g., + 60 will imply discovering resource calendars with slots of 1 hour). Must be divisible by 1,440 (number of + minutes in a day). confidence : Optional[Union[float, Tuple[float, float]]] - Minimum confidence of the intervals in the discovered calendar of a resource or set of resources (between - 0.0 and 1.0) + Fixed value or range for the minimum confidence of the intervals in the discovered calendar of a resource + or set of resources (between 0.0 and 1.0). support : Optional[Union[float, Tuple[float, float]]] - Minimum support of the intervals in the discovered calendar of a resource or set of resources (between 0.0 - and 1.0) + Fixed value or range for the minimum support of the intervals in the discovered calendar of a resource or + set of resources (between 0.0 and 1.0). participation : Optional[Union[float, Tuple[float, float]]] - Participation of a resource in the process to discover a calendar for them, gathered together otherwise - (between 0.0 and 1.0) + Fixed value or range for the participation of a resource in the process to discover a calendar for them, + gathered together otherwise (between 0.0 and 1.0). fuzzy_angle : Optional[Union[float, Tuple[float, float]]] - Angle of the fuzzy trapezoid when computing the availability probability for an activity (angle from - start to end) + Fixed value or range for the angle of the fuzzy trapezoid when computing the availability probability for an + activity (angle from start to end). discover_prioritization_rules : bool Whether to discover case prioritization rules. discover_batching_rules : bool From 8ed18e3021fef3c45c7a47fbfc6e55149b7e8068 Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 16:35:22 +0200 Subject: [PATCH 08/13] Add docstrings for Control-flow module --- src/simod/control_flow/discovery.py | 26 ++++++++++----- src/simod/control_flow/optimizer.py | 49 +++++++++++++++++++++++++++-- src/simod/control_flow/settings.py | 49 +++++++++++++++++++++++++++-- 3 files changed, 113 insertions(+), 11 deletions(-) diff --git a/src/simod/control_flow/discovery.py b/src/simod/control_flow/discovery.py index 734d112f..18c17889 100644 --- a/src/simod/control_flow/discovery.py +++ b/src/simod/control_flow/discovery.py @@ -15,13 +15,25 @@ def discover_process_model(log_path: Path, output_model_path: Path, params: HyperoptIterationParams): """ - Run the process model discovery algorithm specified in the [params] to discover - a process model in [output_model_path] from the (XES) event log in [log_path]. - - :param log_path: Path to the event log in XES format for the Split Miner algorithms. - :param output_model_path: Path to write the discovered process model. - :param params: configuration class specifying the process model discovery algorithm and its parameters. - """ + Runs the specified process model discovery algorithm to extract a process model + from an event log and save it to the given output path. + + This function supports Split Miner V1 and Split Miner V2 as discovery algorithms. + + Parameters + ---------- + log_path : :class:`pathlib.Path` + Path to the event log in XES format, required for Split Miner algorithms. + output_model_path : :class:`pathlib.Path` + Path to save the discovered process model. + params : :class:`HyperoptIterationParams` + Configuration containing the process model discovery algorithm and its parameters. + + Raises + ------ + ValueError + If the specified process model discovery algorithm is unknown. + """ if params.mining_algorithm is ProcessModelDiscoveryAlgorithm.SPLIT_MINER_V1: discover_process_model_with_split_miner_v1( SplitMinerV1Settings( diff --git a/src/simod/control_flow/optimizer.py b/src/simod/control_flow/optimizer.py index 8dacd1a0..69d9e2f3 100644 --- a/src/simod/control_flow/optimizer.py +++ b/src/simod/control_flow/optimizer.py @@ -28,6 +28,37 @@ class ControlFlowOptimizer: + """ + Optimizes the control-flow of a business process model using hyperparameter optimization. + + This class performs iterative optimization to refine the structure of a process model + and discover optimal gateway probabilities. It evaluates different configurations to + improve the process model based on a given metric. + + The search space is built based on the parameters ranges in [settings]. + + Attributes + ---------- + event_log : :class:`EventLog` + Event log containing train and validation partitions. + initial_bps_model : :class:`BPSModel` + Business process simulation (BPS) model to use as a base, by replacing its control-flow model + with the discovered one in each iteration. + settings : :class:`ControlFlowSettings` + Configuration settings to build the search space for the optimization process. + base_directory : :class:`pathlib.Path` + Root directory where output files will be stored. + best_bps_model : Optional[:class:`BPSModel`] + Best discovered BPS model after the optimization process. + evaluation_measurements : :class:`pandas.DataFrame` + Quality measures recorded for each hyperopt iteration. + + Notes + ----- + - If no process model is provided, a discovery method will be used. + - Optimization is performed using TPE-hyperparameter optimization. + """ + # Event log with train/validation partitions event_log: EventLog # BPS model taken as starting point @@ -158,8 +189,22 @@ def _hyperopt_iteration(self, hyperopt_iteration_dict: dict): def run(self) -> HyperoptIterationParams: """ - Run Control-Flow & Gateway Probabilities discovery - :return: The parameters of the best iteration of the optimization process. + Runs the control-flow optimization process. + + This method defines the hyperparameter search space and executes a + TPE-hyperparameter optimization process to discover the best control-flow model. + It evaluates multiple iterations and selects the best-performing set of parameters + for its discovery. + + Returns + ------- + :class:`HyperoptIterationParams` + The parameters of the best iteration of the optimization process. + + Raises + ------ + AssertionError + If the best discovered process model path does not exist after optimization. """ # Define search space self.iteration_index = 0 diff --git a/src/simod/control_flow/settings.py b/src/simod/control_flow/settings.py index c3cf8fd9..f8bf77d9 100644 --- a/src/simod/control_flow/settings.py +++ b/src/simod/control_flow/settings.py @@ -10,7 +10,42 @@ @dataclass class HyperoptIterationParams: - """Parameters for a single iteration of the Control-Flow optimization process.""" + """ + Parameters for a single iteration of the Control-Flow optimization process. + + This class defines the configuration settings used during an iteration of the + optimization process, including process model discovery, optimization metric, + and gateway probability discovery. + + Attributes + ---------- + output_dir : :class:`pathlib.Path` + Directory where all output files for the current iteration will be stored. + provided_model_path : Optional[:class:`pathlib.Path`] + Path to a provided BPMN model, if available (no discovery needed). + project_name : str + Name of the project, mainly used for file naming. + optimization_metric : :class:`Metric` + Metric used to evaluate the candidate process model in this iteration. + gateway_probabilities_method : :class:`GatewayProbabilitiesDiscoveryMethod` + Method for discovering gateway probabilities. + mining_algorithm : :class:`ProcessModelDiscoveryAlgorithm` + Algorithm used for process model discovery, if necessary. + epsilon : Optional[float] + Number of concurrent relations between events to be captured in the discovery algorithm (between 0.0 and 1.0). + eta : Optional[float] + Threshold for filtering the incoming and outgoing edges in the discovery algorithm (between 0.0 and 1.0). + replace_or_joins : Optional[bool] + Whether to replace non-trivial OR joins in the discovered model. + prioritize_parallelism : Optional[bool] + Whether to prioritize parallelism or loops for model discovery. + f_score : Optional[float], default=None + Minimum f-score value to consider the discovered data-aware branching rules. + + Notes + ----- + - If `provided_model_path` is specified, process model discovery will be skipped. + """ # General settings output_dir: Path # Directory where to output all the files of the current iteration @@ -29,7 +64,17 @@ class HyperoptIterationParams: f_score: Optional[float] = None # quality gateway for branch rules (f_score) def to_dict(self) -> dict: - """Returns a dictionary with the parameters for this run.""" + """ + Converts the instance into a dictionary representation of the optimization parameters. + + The returned dictionary is structured based on whether a process model needs + to be discovered or if a pre-existing model is provided. + + Returns + ------- + dict + A dictionary containing the optimization parameters for this iteration. + """ optimization_parameters = { "output_dir": str(self.output_dir), "project_name": str(self.project_name), From 6c7480bf89630914c91c4a18fb81f9b50313e304 Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 16:35:34 +0200 Subject: [PATCH 09/13] Add docstrings for Resource model module --- src/simod/resource_model/optimizer.py | 44 +++++++++++++++++++++++++-- src/simod/resource_model/settings.py | 33 ++++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/simod/resource_model/optimizer.py b/src/simod/resource_model/optimizer.py index 8f6074fe..b0e426de 100644 --- a/src/simod/resource_model/optimizer.py +++ b/src/simod/resource_model/optimizer.py @@ -28,6 +28,37 @@ class ResourceModelOptimizer: + """ + Optimizes the resource model of a business process model using hyperparameter optimization. + + This class performs iterative optimization to refine the resource model + and discover optimal resource profiles and availability calendars. It + evaluates different configurations to improve the process model based + on a given metric. + + The search space is built based on the parameters ranges in [settings]. + + Attributes + ---------- + event_log : :class:`EventLog` + Event log containing train and validation partitions. + initial_bps_model : :class:`BPSModel` + Business process simulation (BPS) model to use as a base, by replacing its resource model + with the discovered one in each iteration. + settings : :class:`ControlFlowSettings` + Configuration settings to build the search space for the optimization process. + base_directory : :class:`pathlib.Path` + Root directory where output files will be stored. + best_bps_model : Optional[:class:`BPSModel`] + Best discovered BPS model after the optimization process. + evaluation_measurements : :class:`pandas.DataFrame` + Quality measures recorded for each hyperopt iteration. + + Notes + ----- + - Optimization is performed using TPE-hyperparameter optimization. + """ + # Event log with train/validation partitions event_log: EventLog # BPS model taken as starting point @@ -168,8 +199,17 @@ def _hyperopt_iteration(self, hyperopt_iteration_dict: dict): def run(self) -> HyperoptIterationParams: """ - Run Resource Model (resource profiles, resource calendars and activity-resource performance) discovery. - :return: The parameters of the best iteration of the optimization process. + Runs the resource model optimization process. + + This method defines the hyperparameter search space and executes a + TPE-hyperparameter optimization process to discover the best resource model. + It evaluates multiple iterations and selects the best-performing set of parameters + for its discovery. + + Returns + ------- + :class:`HyperoptIterationParams` + The parameters of the best iteration of the optimization process. """ # Define search space self.iteration_index = 0 diff --git a/src/simod/resource_model/settings.py b/src/simod/resource_model/settings.py index ee4cb48f..aa815346 100644 --- a/src/simod/resource_model/settings.py +++ b/src/simod/resource_model/settings.py @@ -12,7 +12,29 @@ @dataclass class HyperoptIterationParams: - """Parameters for a single iteration of the Resource Model optimization process.""" + """ + Parameters for a single iteration of the Resource Model optimization process. + + This class defines the necessary parameters for optimizing the resource model of the BPS model. + It includes the parameter values for the discovery of resource profiles, calendars, etc. + + Attributes + ---------- + output_dir : :class:`pathlib.Path` + Directory where all files of the current iteration will be stored. + process_model_path : :class:`pathlib.Path` + Path to the BPMN process model used for optimization. + project_name : str + Name of the project for file naming purposes. + optimization_metric : :class:`Metric` + Metric used to evaluate the quality of the current iteration's candidate. + calendar_discovery_params : :class:`CalendarDiscoveryParameters` + Parameters for the resource calendar (i.e., working schedules) discovery. + discover_prioritization_rules : bool, optional + Whether to attempt discovering prioritization rules (default: False). + discover_batching_rules : bool, optional + Whether to attempt discovering batching rules (default: False). + """ # General settings output_dir: Path # Directory where to output all the files of the current iteration @@ -25,7 +47,14 @@ class HyperoptIterationParams: discover_batching_rules: bool = False # Whether to try to add batching or not def to_dict(self) -> dict: - """Returns a dictionary with the parameters for this run.""" + """ + Converts the parameters of the current iteration into a dictionary format. + + Returns + ------- + dict + A dictionary containing the iteration parameters. + """ # Save common params optimization_parameters = { "output_dir": str(self.output_dir), From 5502a4570f8c10888afff648af5a242b300c75d5 Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 16:35:46 +0200 Subject: [PATCH 10/13] Add docstrings for Extraneous delays module --- src/simod/extraneous_delays/optimizer.py | 32 ++++++++++++++++- src/simod/extraneous_delays/types.py | 45 ++++++++++++++++++++++++ src/simod/extraneous_delays/utilities.py | 33 +++++++++++++---- 3 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/simod/extraneous_delays/optimizer.py b/src/simod/extraneous_delays/optimizer.py index 844bf870..2fd41bf9 100644 --- a/src/simod/extraneous_delays/optimizer.py +++ b/src/simod/extraneous_delays/optimizer.py @@ -4,7 +4,6 @@ from extraneous_activity_delays.config import ( Configuration as ExtraneousActivityDelaysConfiguration, - DiscoveryMethod, TimerPlacement, SimulationEngine, SimulationModel, @@ -21,6 +20,24 @@ class ExtraneousDelaysOptimizer: + """ + Optimizer for the discovery of the extraneous delays model. + + This class performs either a direct discovery of the extraneous delays of the process, or launches an iterative + optimization that first discovers the extraneous delays and then adjusts their size to better reflect reality. + + Attributes + ---------- + event_log : :class:`EventLog` + The event log containing the train and validation data. + bps_model : :class:`BPSModel` + The business process simulation model to enhance with extraneous delays, including the BPMN representation. + settings : :class:`ExtraneousDelaysSettings` + Configuration settings for extraneous delay discovery. + base_directory : :class:`pathlib.Path` + Directory where output files will be stored. + """ + def __init__( self, event_log: EventLog, @@ -36,6 +53,19 @@ def __init__( assert self.bps_model.process_model is not None, "BPMN model is not specified." def run(self) -> List[ExtraneousDelay]: + """ + Executes the extraneous delay discovery process. + + This method configures the optimization process, applies either a direct enhancement + or a hyperparameter optimization approach to identify delays, and returns the best + detected delays as a list of `ExtraneousDelay` objects. + + Returns + ------- + List[:class:`ExtraneousDelay`] + A list of detected extraneous delays, each containing activity names, delay IDs, + and their corresponding duration distributions. + """ # Set-up configuration for extraneous delay discovery configuration = ExtraneousActivityDelaysConfiguration( log_ids=self.event_log.log_ids, diff --git a/src/simod/extraneous_delays/types.py b/src/simod/extraneous_delays/types.py index 81e57427..d73e083e 100644 --- a/src/simod/extraneous_delays/types.py +++ b/src/simod/extraneous_delays/types.py @@ -5,11 +5,40 @@ @dataclass class ExtraneousDelay: + """ + Represents an extraneous delay within a business process activity. + + This class encapsulates the details of an identified extraneous delay, + including the affected activity, a unique delay identifier, and the + duration distribution of the delay. + + Attributes + ---------- + activity_name : str + The name of the activity where the extraneous delay occurs. + delay_id : str + A unique identifier for the delay event. + duration_distribution : :class:`DurationDistribution` + The statistical distribution representing the delay duration. + """ + activity_name: str delay_id: str duration_distribution: DurationDistribution def to_dict(self) -> dict: + """ + Converts the extraneous delay into a dictionary format. + + The dictionary representation is compatible with the Prosimos simulation + engine, containing activity details, a unique event identifier, and the + delay duration distribution. + + Returns + ------- + dict + A dictionary representation of the extraneous delay. + """ return { "activity": self.activity_name, "event_id": self.delay_id, @@ -17,6 +46,22 @@ def to_dict(self) -> dict: @staticmethod def from_dict(delay: dict) -> "ExtraneousDelay": + """ + Creates an `ExtraneousDelay` instance from a dictionary. + + This method reconstructs an `ExtraneousDelay` object from a dictionary + containing activity name, delay identifier, and duration distribution. + + Parameters + ---------- + delay : dict + A dictionary representation of an extraneous delay. + + Returns + ------- + :class:`ExtraneousDelay` + An instance of `ExtraneousDelay` with the extracted attributes. + """ return ExtraneousDelay( activity_name=delay["activity"], delay_id=delay["event_id"], diff --git a/src/simod/extraneous_delays/utilities.py b/src/simod/extraneous_delays/utilities.py index 1258cd86..1fb8d745 100644 --- a/src/simod/extraneous_delays/utilities.py +++ b/src/simod/extraneous_delays/utilities.py @@ -15,14 +15,33 @@ def add_timers_to_bpmn_model( timer_placement: TimerPlacement = TimerPlacement.BEFORE, ): """ - Enhance the BPMN model received by adding a timer previous (or after) to each activity denoted by [timers]. + Enhances a BPMN model by adding timers before or after specified activities. - :param process_model: Path to the process model (in BPMN format) to enhance. - :param delays: Dict with the name of each activity as key, and the timer configuration as value. - :param timer_placement: Option to consider the placement of the timers either BEFORE (the extraneous delay - is considered to be happening previously to an activity instance) or AFTER (the - extraneous delay is considered to be happening afterward an activity instance) each - activity. + This function modifies a given BPMN process model by inserting timers + before or after activities that have identified extraneous delays. + + Parameters + ---------- + process_model : :class:`pathlib.Path` + Path to the BPMN process model file to enhance. + delays : List[:class:`ExtraneousDelay`] + A list of extraneous delays, where each delay specifies an activity + and the corresponding timer configuration. + timer_placement : :class:`TimerPlacement`, optional + Specifies whether the timers should be placed **BEFORE** (indicating the + delay happens before an activity instance) or **AFTER** (indicating the + delay happens afterward). Default is `TimerPlacement.BEFORE`. + + Notes + ----- + - This function modifies the BPMN file in place. + - The method searches for tasks within the BPMN model and inserts timers + based on the provided delays. + + Raises + ------ + ValueError + If the BPMN model file does not contain any tasks. """ if len(delays) > 0: # Extract process From 6bde5a2c1c109af54b9fbb8be9bcb4707c33e7ff Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 17:19:01 +0200 Subject: [PATCH 11/13] Update docstrings to unify "optional" parameter declaration --- src/simod/control_flow/discovery.py | 2 +- src/simod/control_flow/optimizer.py | 4 +-- src/simod/control_flow/settings.py | 12 ++++---- src/simod/event_log/event_log.py | 6 ++-- src/simod/extraneous_delays/optimizer.py | 8 ++--- src/simod/extraneous_delays/utilities.py | 2 +- src/simod/resource_model/optimizer.py | 10 +++---- src/simod/resource_model/settings.py | 2 +- src/simod/settings/control_flow_settings.py | 16 +++++----- src/simod/settings/resource_model_settings.py | 10 +++---- src/simod/settings/simod_settings.py | 2 +- src/simod/simod.py | 4 +-- src/simod/simulation/parameters/BPS_model.py | 30 +++++++++---------- src/simod/simulation/prosimos.py | 8 ++--- 14 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/simod/control_flow/discovery.py b/src/simod/control_flow/discovery.py index 18c17889..550afb07 100644 --- a/src/simod/control_flow/discovery.py +++ b/src/simod/control_flow/discovery.py @@ -26,7 +26,7 @@ def discover_process_model(log_path: Path, output_model_path: Path, params: Hype Path to the event log in XES format, required for Split Miner algorithms. output_model_path : :class:`pathlib.Path` Path to save the discovered process model. - params : :class:`HyperoptIterationParams` + params : :class:`~simod.resource_model.settings.HyperoptIterationParams` Configuration containing the process model discovery algorithm and its parameters. Raises diff --git a/src/simod/control_flow/optimizer.py b/src/simod/control_flow/optimizer.py index 69d9e2f3..93cc4e93 100644 --- a/src/simod/control_flow/optimizer.py +++ b/src/simod/control_flow/optimizer.py @@ -48,7 +48,7 @@ class ControlFlowOptimizer: Configuration settings to build the search space for the optimization process. base_directory : :class:`pathlib.Path` Root directory where output files will be stored. - best_bps_model : Optional[:class:`BPSModel`] + best_bps_model : :class:`BPSModel`, optional Best discovered BPS model after the optimization process. evaluation_measurements : :class:`pandas.DataFrame` Quality measures recorded for each hyperopt iteration. @@ -198,7 +198,7 @@ def run(self) -> HyperoptIterationParams: Returns ------- - :class:`HyperoptIterationParams` + :class:`~simod.control_flow.settings.HyperoptIterationParams` The parameters of the best iteration of the optimization process. Raises diff --git a/src/simod/control_flow/settings.py b/src/simod/control_flow/settings.py index f8bf77d9..a82c7d4f 100644 --- a/src/simod/control_flow/settings.py +++ b/src/simod/control_flow/settings.py @@ -21,7 +21,7 @@ class HyperoptIterationParams: ---------- output_dir : :class:`pathlib.Path` Directory where all output files for the current iteration will be stored. - provided_model_path : Optional[:class:`pathlib.Path`] + provided_model_path : :class:`pathlib.Path`, optional Path to a provided BPMN model, if available (no discovery needed). project_name : str Name of the project, mainly used for file naming. @@ -31,15 +31,15 @@ class HyperoptIterationParams: Method for discovering gateway probabilities. mining_algorithm : :class:`ProcessModelDiscoveryAlgorithm` Algorithm used for process model discovery, if necessary. - epsilon : Optional[float] + epsilon : float, optional Number of concurrent relations between events to be captured in the discovery algorithm (between 0.0 and 1.0). - eta : Optional[float] + eta : float, optional Threshold for filtering the incoming and outgoing edges in the discovery algorithm (between 0.0 and 1.0). - replace_or_joins : Optional[bool] + replace_or_joins : bool, optional Whether to replace non-trivial OR joins in the discovered model. - prioritize_parallelism : Optional[bool] + prioritize_parallelism : bool, optional Whether to prioritize parallelism or loops for model discovery. - f_score : Optional[float], default=None + f_score : float], default=Non, optional Minimum f-score value to consider the discovered data-aware branching rules. Notes diff --git a/src/simod/event_log/event_log.py b/src/simod/event_log/event_log.py index f650b007..cbfa04e6 100644 --- a/src/simod/event_log/event_log.py +++ b/src/simod/event_log/event_log.py @@ -83,11 +83,11 @@ def from_path( Identifiers for mapping column names in the event log. preprocessing_settings : :class:`PreprocessingSettings`, optional Settings for preprocessing the event log. - need_test_partition : Optional[bool] + need_test_partition : bool, optional Whether to create a test partition if a separate test log is not provided. - process_name : Optional[str] + process_name : str, optional Name of the business process. If not provided, it is inferred from the file name. - test_log_path : Optional[:class:`pathlib.Path`] + test_log_path : :class:`pathlib.Path`, optional Path to the test event log file (CSV or CSV.GZ). If provided, the test log is loaded separately. split_ratio : float, default=0.8 Ratio for splitting training and validation partitions. diff --git a/src/simod/extraneous_delays/optimizer.py b/src/simod/extraneous_delays/optimizer.py index 2fd41bf9..363be4bb 100644 --- a/src/simod/extraneous_delays/optimizer.py +++ b/src/simod/extraneous_delays/optimizer.py @@ -28,11 +28,11 @@ class ExtraneousDelaysOptimizer: Attributes ---------- - event_log : :class:`EventLog` + event_log : :class:`~simod.event_log.event_log.EventLog` The event log containing the train and validation data. - bps_model : :class:`BPSModel` + bps_model : :class:`~simod.simulation.parameters.BPS_model.BPSModel` The business process simulation model to enhance with extraneous delays, including the BPMN representation. - settings : :class:`ExtraneousDelaysSettings` + settings : :class:`~simod.settings.extraneous_delays_settings.ExtraneousDelaysSettings` Configuration settings for extraneous delay discovery. base_directory : :class:`pathlib.Path` Directory where output files will be stored. @@ -62,7 +62,7 @@ def run(self) -> List[ExtraneousDelay]: Returns ------- - List[:class:`ExtraneousDelay`] + List[:class:`~simod.extraneous_delays.types.ExtraneousDelay`] A list of detected extraneous delays, each containing activity names, delay IDs, and their corresponding duration distributions. """ diff --git a/src/simod/extraneous_delays/utilities.py b/src/simod/extraneous_delays/utilities.py index 1fb8d745..84dc1b10 100644 --- a/src/simod/extraneous_delays/utilities.py +++ b/src/simod/extraneous_delays/utilities.py @@ -24,7 +24,7 @@ def add_timers_to_bpmn_model( ---------- process_model : :class:`pathlib.Path` Path to the BPMN process model file to enhance. - delays : List[:class:`ExtraneousDelay`] + delays : List[:class:`~simod.extraneous_delays.types.ExtraneousDelay`] A list of extraneous delays, where each delay specifies an activity and the corresponding timer configuration. timer_placement : :class:`TimerPlacement`, optional diff --git a/src/simod/resource_model/optimizer.py b/src/simod/resource_model/optimizer.py index b0e426de..63182a43 100644 --- a/src/simod/resource_model/optimizer.py +++ b/src/simod/resource_model/optimizer.py @@ -40,16 +40,16 @@ class ResourceModelOptimizer: Attributes ---------- - event_log : :class:`EventLog` + event_log : :class:`~simod.event_log.event_log.EventLog` Event log containing train and validation partitions. - initial_bps_model : :class:`BPSModel` + initial_bps_model : :class:`~simod.simulation.parameters.BPS_model.BPSModel` Business process simulation (BPS) model to use as a base, by replacing its resource model with the discovered one in each iteration. - settings : :class:`ControlFlowSettings` + settings : :class:`~simod.settings.resource_model_settings.ResourceModelSettings` Configuration settings to build the search space for the optimization process. base_directory : :class:`pathlib.Path` Root directory where output files will be stored. - best_bps_model : Optional[:class:`BPSModel`] + best_bps_model : :class:`~simod.simulation.parameters.BPS_model.BPSModel`, optional Best discovered BPS model after the optimization process. evaluation_measurements : :class:`pandas.DataFrame` Quality measures recorded for each hyperopt iteration. @@ -208,7 +208,7 @@ def run(self) -> HyperoptIterationParams: Returns ------- - :class:`HyperoptIterationParams` + :class:`~simod.resource_model.settings.HyperoptIterationParams` The parameters of the best iteration of the optimization process. """ # Define search space diff --git a/src/simod/resource_model/settings.py b/src/simod/resource_model/settings.py index aa815346..5fa6ba03 100644 --- a/src/simod/resource_model/settings.py +++ b/src/simod/resource_model/settings.py @@ -26,7 +26,7 @@ class HyperoptIterationParams: Path to the BPMN process model used for optimization. project_name : str Name of the project for file naming purposes. - optimization_metric : :class:`Metric` + optimization_metric : :class:`~simod.settings.common_settings.Metric` Metric used to evaluate the quality of the current iteration's candidate. calendar_discovery_params : :class:`CalendarDiscoveryParameters` Parameters for the resource calendar (i.e., working schedules) discovery. diff --git a/src/simod/settings/control_flow_settings.py b/src/simod/settings/control_flow_settings.py index 9074efe2..c2c5f948 100644 --- a/src/simod/settings/control_flow_settings.py +++ b/src/simod/settings/control_flow_settings.py @@ -95,7 +95,7 @@ class ControlFlowSettings(BaseModel): Attributes ---------- - optimization_metric : :class:`Metric` + optimization_metric : :class:`~simod.settings.common_settings.Metric` The metric used to evaluate process model quality at each iteration of the optimization process (i.e., loss function). num_iterations : int @@ -104,21 +104,21 @@ class ControlFlowSettings(BaseModel): The number of replications for the evaluations of each iteration. gateway_probabilities : Union[:class:`GatewayProbabilitiesDiscoveryMethod`, List[:class:`GatewayProbabilitiesDiscoveryMethod`]] Fixed method or list of methods to use in each iteration to discover gateway probabilities. - mining_algorithm : Optional[:class:`ProcessModelDiscoveryAlgorithm`] + mining_algorithm : :class:`ProcessModelDiscoveryAlgorithm`, optional The process model discovery algorithm to use. - epsilon : Optional[Union[float, Tuple[float, float]]] + epsilon : Union[float, Tuple[float, float]], optional Fixed number or range for the number of concurrent relations between events to be captured in the discovery algorithm (between 0.0 and 1.0). - eta : Optional[Union[float, Tuple[float, float]]] + eta : Union[float, Tuple[float, float]], optional Fixed number or range for the threshold for filtering the incoming and outgoing edges in the discovery algorithm (between 0.0 and 1.0). - replace_or_joins : Optional[Union[bool, List[bool]]] + replace_or_joins : Union[bool, List[bool]], optional Fixed value or list for whether to replace non-trivial OR joins. - prioritize_parallelism : Optional[Union[bool, List[bool]]] + prioritize_parallelism : Union[bool, List[bool]], optional Fixed value or list for whether to prioritize parallelism over loops. - discover_branch_rules : Optional[bool] + discover_branch_rules : bool, optional Whether to discover branch rules for gateways. - f_score : Optional[Union[float, Tuple[float, float]]] + f_score : Union[float, Tuple[float, float]], optional Fixed value or range for the minimum f-score value to consider the discovered data-aware branching rules. """ diff --git a/src/simod/settings/resource_model_settings.py b/src/simod/settings/resource_model_settings.py index daf84fdb..23b8a942 100644 --- a/src/simod/settings/resource_model_settings.py +++ b/src/simod/settings/resource_model_settings.py @@ -26,20 +26,20 @@ class ResourceModelSettings(BaseModel): The number of replications for the evaluations of each iteration. discovery_type : :class:`CalendarType` Type of calendar discovery method used for resource modeling. - granularity : Optional[Union[int, Tuple[int, int]]] + granularity : Union[int, Tuple[int, int]], optional Fixed value or range for the time granularity for calendar discovery, measured in minutes per granule (e.g., 60 will imply discovering resource calendars with slots of 1 hour). Must be divisible by 1,440 (number of minutes in a day). - confidence : Optional[Union[float, Tuple[float, float]]] + confidence : Union[float, Tuple[float, float]], optional Fixed value or range for the minimum confidence of the intervals in the discovered calendar of a resource or set of resources (between 0.0 and 1.0). - support : Optional[Union[float, Tuple[float, float]]] + support : Union[float, Tuple[float, float]], optional Fixed value or range for the minimum support of the intervals in the discovered calendar of a resource or set of resources (between 0.0 and 1.0). - participation : Optional[Union[float, Tuple[float, float]]] + participation : Union[float, Tuple[float, float]], optional Fixed value or range for the participation of a resource in the process to discover a calendar for them, gathered together otherwise (between 0.0 and 1.0). - fuzzy_angle : Optional[Union[float, Tuple[float, float]]] + fuzzy_angle : Union[float, Tuple[float, float]], optional Fixed value or range for the angle of the fuzzy trapezoid when computing the availability probability for an activity (angle from start to end). discover_prioritization_rules : bool diff --git a/src/simod/settings/simod_settings.py b/src/simod/settings/simod_settings.py index a43b2e02..bb7b531e 100644 --- a/src/simod/settings/simod_settings.py +++ b/src/simod/settings/simod_settings.py @@ -27,7 +27,7 @@ class SimodSettings(BaseModel): General configuration parameters of SIMOD and parameters common to all pipeline stages. preprocessing : :class:`~simod.settings.preprocessing_settings.PreprocessingSettings` Configuration parameters for the preprocessing stage of SIMOD. - control_flow : :class:`~simod.settings.control_flow.ControlFlowSettings` + control_flow : :class:`~simod.settings.control_flow_settings.ControlFlowSettings` Configuration parameters for the control-flow model discovery stage. resource_model : :class:`~simod.settings.resource_model_settings.ResourceModelSettings` Configuration parameters for the resource model discovery stage. diff --git a/src/simod/simod.py b/src/simod/simod.py index 2fd7da49..f9cfd93e 100644 --- a/src/simod/simod.py +++ b/src/simod/simod.py @@ -42,9 +42,9 @@ class Simod: Attributes ---------- - settings : :class:`~simod.settings.SimodSettings` + settings : :class:`~simod.settings.simod_settings.SimodSettings` Configuration to run SIMOD and all its stages. - event_log : :class:`~simod.event_log.EventLog` + event_log : :class:`~simod.event_log.event_log.EventLog` EventLog class storing the preprocessed training, validation, and (optionally) test partitions. output_dir : :class:`~pathlib.Path` Path to the folder where to write all the SIMOD outputs. diff --git a/src/simod/simulation/parameters/BPS_model.py b/src/simod/simulation/parameters/BPS_model.py index 77096909..308146f3 100644 --- a/src/simod/simulation/parameters/BPS_model.py +++ b/src/simod/simulation/parameters/BPS_model.py @@ -47,29 +47,29 @@ class BPSModel: Attributes ---------- - process_model : Optional[:class:`pathlib.Path`] + process_model : :class:`pathlib.Path`, optional Path to the BPMN process model file. - gateway_probabilities : Optional[List[:class:`GatewayProbabilities`]] + gateway_probabilities : List[:class:`GatewayProbabilities`], optional Probabilities for gateway-based process routing. - case_arrival_model : Optional[:class:`CaseArrivalModel`] + case_arrival_model : :class:`CaseArrivalModel`, optional Model for the arrival of new cases in the simulation. - resource_model : Optional[:class:`ResourceModel`] + resource_model : :class:`ResourceModel`, optional Model for the resources involved in the process, their working schedules, etc. - extraneous_delays : Optional[List[:class:`ExtraneousDelay`]] + extraneous_delays : List[:class:`~simod.extraneous_delays.types.ExtraneousDelay`], optional A list of delays representing extraneous waiting times before/after activities. - case_attributes : Optional[List[:class:`CaseAttribute`]] + case_attributes : List[:class:`CaseAttribute`], optional Case-level attributes and their update rules. - global_attributes : Optional[List[:class:`GlobalAttribute`]] + global_attributes : List[:class:`GlobalAttribute`], optional Global attributes and their update rules. - event_attributes : Optional[List[:class:`EventAttribute`]] + event_attributes : List[:class:`EventAttribute`], optional Event-level attributes and their update rules. - prioritization_rules : Optional[List[:class:`PrioritizationRule`]] + prioritization_rules : List[:class:`PrioritizationRule`], optional A set of case prioritization rules for process execution. - batching_rules : Optional[List[:class:`BatchingRule`]] + batching_rules : List[:class:`BatchingRule`], optional Rules defining how activities are batched together. - branch_rules : Optional[List[:class:`BranchRules`]] + branch_rules : List[:class:`BranchRules`], optional Branching rules defining conditional flow behavior in decision points. - calendar_granularity : Optional[int] + calendar_granularity : int, optional Granularity of the resource calendar, expressed in minutes. Notes @@ -161,7 +161,7 @@ def deep_copy(self) -> "BPSModel": Returns ------- - BPSModel + :class:`BPSModel` A new, independent copy of the current BPSModel instance. Notes @@ -221,14 +221,14 @@ def to_json(self, output_dir: Path, process_name: str) -> Path: Parameters ---------- - output_dir : Path + output_dir : :class:`pathlib.Path` The directory where the JSON file should be saved. process_name : str The name of the process, used for naming the output file. Returns ------- - Path + :class:`pathlib.Path` The full path to the generated JSON file. Notes diff --git a/src/simod/simulation/prosimos.py b/src/simod/simulation/prosimos.py index 4738e41f..47337752 100644 --- a/src/simod/simulation/prosimos.py +++ b/src/simod/simulation/prosimos.py @@ -89,11 +89,11 @@ def simulate_and_evaluate( Parameters ---------- - process_model_path : :class:`Path` + process_model_path : :class:`pathlib.Path` Path to the BPMN process model. - parameters_path : :class:`Path` + parameters_path : :class:`pathlib.Path` Path to the Prosimos simulation parameters JSON file. - output_dir : :class:`Path` + output_dir : :class:`pathlib.Path` Directory where simulated logs will be stored. simulation_cases : int Number of cases to simulate per run. @@ -103,7 +103,7 @@ def simulate_and_evaluate( The actual event log to compare against. validation_log_ids : :class:`EventLogIDs` Column mappings for identifying events in the validation log. - metrics : List[:class:`simod.settings.common_settings.Metric`] + metrics : List[:class:`~simod.settings.common_settings.Metric`] A list of metrics used to evaluate the simulated logs. num_simulations : int, optional Number of parallel simulation runs (default is 1). From 704d1cd97550ef1a8b1f44001dbf90a2c7e5faa6 Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 17:49:15 +0200 Subject: [PATCH 12/13] Add Sphinx configuration and documentation files --- docs/Makefile | 20 ++ docs/make.bat | 35 ++++ docs/requirements.txt | 23 +++ .../source/_static/complete_configuration.yml | 146 ++++++++++++++ docs/source/_static/configuration_example.yml | 98 +++++++++ .../configuration_example_data_aware.yml | 104 ++++++++++ .../_static/configuration_example_fuzzy.yml | 84 ++++++++ .../configuration_example_with_evaluation.yml | 109 ++++++++++ ...on_example_with_provided_process_model.yml | 80 ++++++++ .../source/_static/configuration_one_shot.yml | 69 +++++++ docs/source/_static/simod.png | Bin 0 -> 48295 bytes docs/source/api.rst | 186 ++++++++++++++++++ docs/source/citation.rst | 45 +++++ docs/source/conf.py | 47 +++++ docs/source/index.rst | 43 ++++ docs/source/installation.rst | 88 +++++++++ docs/source/usage.rst | 83 ++++++++ 17 files changed, 1260 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/requirements.txt create mode 100644 docs/source/_static/complete_configuration.yml create mode 100644 docs/source/_static/configuration_example.yml create mode 100644 docs/source/_static/configuration_example_data_aware.yml create mode 100644 docs/source/_static/configuration_example_fuzzy.yml create mode 100644 docs/source/_static/configuration_example_with_evaluation.yml create mode 100644 docs/source/_static/configuration_example_with_provided_process_model.yml create mode 100644 docs/source/_static/configuration_one_shot.yml create mode 100644 docs/source/_static/simod.png create mode 100644 docs/source/api.rst create mode 100644 docs/source/citation.rst create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/installation.rst create mode 100644 docs/source/usage.rst diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 00000000..d0c3cbf1 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 00000000..dc1312ab --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 00000000..24842c6d --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,23 @@ +click==8.1.3 +hyperopt==0.2.7 +lxml==5.3.0 +matplotlib==3.6.0 +networkx==3.2.1 +numpy==1.24.23 +pandas==2.1.0 +pendulum==3.0.0 +pydantic==2.3.0 +python-dotenv==1.0.0 +python-multipart==0.0.12 +pytz==2024.2 +PyYAML==6.0 +requests==2.28.2 +scipy==1.13.0 +statistics==1.0.3.5 +tqdm==4.64.1 +xmltodict==0.13.0 +prosimos==2.0.6 +extraneous-activity-delays==2.1.21 +openxes-cli-py==0.1.15 +pix-framework==0.13.17 +log-distance-measures==2.0.0 diff --git a/docs/source/_static/complete_configuration.yml b/docs/source/_static/complete_configuration.yml new file mode 100644 index 00000000..f37603e7 --- /dev/null +++ b/docs/source/_static/complete_configuration.yml @@ -0,0 +1,146 @@ +version: 5 + +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically estimated (see preprocessing) + start_time: "start_time" # Should be present, but if not, can be estimated (see preprocessing) + end_time: "end_time" + # Use this process model and skip its discovery + process_model_path: ../models/LoanApp_simplified.bpmn + # Event log to evaluate the discovered BPS model with + test_log_path: ../event_logs/LoanApp_simplified_test.csv.gz + # Flag to perform evaluation (if 'test_log_path' not provided) with a test partition of the input log + perform_final_evaluation: true + # Number of evaluations of the discovered BPS model + num_final_evaluations: 10 + # Metrics to evaluate the discovered BPS model (reported in an output file) + evaluation_metrics: + - 3_gram_distance + - 2_gram_distance + - absolute_event_distribution + - relative_event_distribution + - circadian_event_distribution + - arrival_event_distribution + - cycle_time_distribution + # Whether to simulate the arrival times using the distribution of inter-arrival times observed in the training log, + # or fitting a parameterized probabilistic distribution (e.g., norm, expon) with these observed values. + use_observed_arrival_distribution: false + # Whether to delete all files created during the optimization phases or not + clean_intermediate_files: true + # Whether to discover global/case/event attributes and their update rules or not + discover_data_attributes: false + +################# +# Preprocessing # +################# +preprocessing: + # If the log has start times, threshold to consider two activities as concurrent when computing the enabled time + # (if necessary). Two activities would be considered concurrent if their occurrences happening concurrently divided + # by their total occurrences is higher than this threshold. + enable_time_concurrency_threshold: 0.75 + # If true, preprocess multitasking (i.e., one resource performing more than one activity at the same time) by + # adjusting the timestamps (start/end) of those activities being executed at the same time by the same resource. + multitasking: false + # Thresholds for the heuristics' concurrency oracle (only used to estimate start times if missing). + concurrency_df: 0.9 # Directly-Follows threshold + concurrency_l2l: 0.9 # Length 2 loops threshold + concurrency_l1l: 0.9 # Length 1 loops threshold + +################ +# Control-flow # +################ +control_flow: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: n_gram_distance + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Methods for discovering gateway probabilities + gateway_probabilities: + - equiprobable + - discovery + # Discover process model with SplitMiner v1 (options: sm1 or sm2) + mining_algorithm: sm1 + # For Split Miner v1 and v2: Number of concurrent relations between events to be captured (between 0.0 and 1.0) + epsilon: + - 0.05 + - 0.4 + # Only for Split Miner v1: Threshold for filtering the incoming and outgoing edges (between 0.0 and 1.0) + eta: + - 0.2 + - 0.7 + # Only for Split Miner v1: Whether to replace non-trivial OR joins or not (true or false) + replace_or_joins: + - true + - false + # Only for Split Miner v1: Whether to prioritize parallelism over loops or not (true or false) + prioritize_parallelism: + - true + - false + # Discover data-aware branching rules, i.e., BPMN decision points based on value of data attributes + discover_branch_rules: true + # Minimum f-score value to consider the discovered data-aware branching rules + f_score: + - 0.3 + - 0.9 + +################## +# Resource model # +################## +resource_model: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: circadian_emd + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Whether to discover prioritization or batching behavior + discover_prioritization_rules: false + discover_batching_rules: false + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type (fuzzy, differentiated, pool, undifferentiated) + discovery_type: differentiated + # Time granularity (in minutes) for the resource calendar (the higher the density of events in the log, the smaller the granularity can be) + granularity: + - 15 + - 60 + # Minimum confidence of the intervals in the discovered calendar of a resource or set of resources (between 0.0 and 1.0) + confidence: + - 0.5 + - 0.85 + # Minimum support of the intervals in the discovered calendar of a resource or set of resources (between 0.0 and 1.0) + support: + - 0.05 + - 0.5 + # Participation of a resource in the process to discover a calendar for them, gathered together otherwise (between 0.0 and 1.0) + participation: + - 0.2 + - 0.5 + # Angle of the fuzzy trapezoid when computing the availability probability for an activity (angle from start to end) + fuzzy_angle: + - 0.1 + - 0.9 + +##################### +# Extraneous delays # +##################### +extraneous_activity_delays: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: relative_emd + # Method to compute the extraneous delay (naive or eclipse-aware) + discovery_method: eclipse-aware + # Number of optimization iterations over the search space (1 = direct discovery, no optimization stage) + num_iterations: 1 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 diff --git a/docs/source/_static/configuration_example.yml b/docs/source/_static/configuration_example.yml new file mode 100644 index 00000000..186512ea --- /dev/null +++ b/docs/source/_static/configuration_example.yml @@ -0,0 +1,98 @@ +################################################################################################################# +# Simple configuration example with i) no evaluation of the final BPS model, ii) 20 iterations of control-flow # +# discovery, iii) 20 iterations of resource model (differentiated) discovery, and iv) direct discovery of # +# extraneous delays. # +################################################################################################################# +# - Increase the num_iterations to (potentially) improve the quality of that discovered model # +# - Visit 'complete_configuration.yml' example for a description of all configurable parameters # +################################################################################################################# +version: 5 +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically computed + start_time: "start_time" + end_time: "end_time" + # Whether to discover case attributes or not + discover_data_attributes: false +################# +# Preprocessing # +################# +preprocessing: + # Threshold to consider two activities as concurrent when computing the enabled time (if necessary) + enable_time_concurrency_threshold: 0.75 +################ +# Control-flow # +################ +control_flow: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: two_gram_distance + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Method for discovering gateway probabilities + gateway_probabilities: discovery + # Discover process model with SplitMiner v3 + mining_algorithm: sm1 + # Number of concurrent relations between events to be captured + epsilon: + - 0.05 + - 0.4 + # Threshold for filtering the incoming and outgoing edges + eta: + - 0.2 + - 0.7 + # Whether to replace non-trivial OR joins or not + replace_or_joins: + - true + - false + # Whether to prioritize parallelism over loops or not + prioritize_parallelism: + - true + - false +################## +# Resource model # +################## +resource_model: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: circadian_emd + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Whether to discover prioritization or batching behavior + discover_prioritization_rules: false + discover_batching_rules: false + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type + discovery_type: differentiated + # Time granularity (in minutes) for the resource calendar (the higher the density of events in the log, the smaller the granularity can be) + granularity: 60 + # Minimum confidence of the intervals in the discovered calendar (of a resource or set of resources) + confidence: + - 0.5 + - 0.85 + # Minimum support of the intervals in the discovered calendar (of a resource or set of resources) + support: + - 0.05 + - 0.5 + # Participation of a resource in the process to discover a calendar for them (gathered together otherwise) + participation: 0.4 +##################### +# Extraneous delays # +##################### +extraneous_activity_delays: + # Method to compute the extraneous delay + discovery_method: eclipse-aware + # Number of optimization iterations over the search space (1 = direct discovery, no optimization stage) + num_iterations: 1 diff --git a/docs/source/_static/configuration_example_data_aware.yml b/docs/source/_static/configuration_example_data_aware.yml new file mode 100644 index 00000000..aeaddffb --- /dev/null +++ b/docs/source/_static/configuration_example_data_aware.yml @@ -0,0 +1,104 @@ +################################################################################################################# +# Simple configuration example with i) no evaluation of the final BPS model, ii) 10 iterations of control-flow # +# discovery (BPMN model provided) with data-aware decision points, iii) 20 iterations of resource model # +# (differentiated) discovery, and iv) no discovery of extraneous delays. # +################################################################################################################# +# - Increase the num_iterations to (potentially) improve the quality of that discovered model # +# - Visit 'complete_configuration.yml' example for a description of all configurable parameters # +################################################################################################################# +version: 5 +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically computed + start_time: "start_time" + end_time: "end_time" + # Whether to discover case attributes or not + discover_data_attributes: true +################# +# Preprocessing # +################# +preprocessing: + # Threshold to consider two activities as concurrent when computing the enabled time (if necessary) + enable_time_concurrency_threshold: 0.75 +################ +# Control-flow # +################ +control_flow: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: two_gram_distance + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Method for discovering gateway probabilities + gateway_probabilities: discovery + # Discover process model with SplitMiner v3 + mining_algorithm: sm1 + # Number of concurrent relations between events to be captured + epsilon: + - 0.05 + - 0.4 + # Threshold for filtering the incoming and outgoing edges + eta: + - 0.2 + - 0.7 + # Whether to replace non-trivial OR joins or not + replace_or_joins: + - true + - false + # Whether to prioritize parallelism over loops or not + prioritize_parallelism: + - true + - false + # Discover data-aware branching rules, i.e., BPMN decision points based on value of data attributes + discover_branch_rules: true + # Minimum f-score value to consider the discovered data-aware branching rules + f_score: + - 0.3 + - 0.9 +################## +# Resource model # +################## +resource_model: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: circadian_emd + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Whether to discover prioritization or batching behavior + discover_prioritization_rules: false + discover_batching_rules: false + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type + discovery_type: differentiated + # Time granularity (in minutes) for the resource calendar (the higher the density of events in the log, the smaller the granularity can be) + granularity: 60 + # Minimum confidence of the intervals in the discovered calendar (of a resource or set of resources) + confidence: + - 0.5 + - 0.85 + # Minimum support of the intervals in the discovered calendar (of a resource or set of resources) + support: + - 0.05 + - 0.5 + # Participation of a resource in the process to discover a calendar for them (gathered together otherwise) + participation: 0.4 +##################### +# Extraneous delays # +##################### +extraneous_activity_delays: + # Method to compute the extraneous delay + discovery_method: eclipse-aware + # Number of optimization iterations over the search space (1 = direct discovery, no optimization stage) + num_iterations: 1 diff --git a/docs/source/_static/configuration_example_fuzzy.yml b/docs/source/_static/configuration_example_fuzzy.yml new file mode 100644 index 00000000..5e74418d --- /dev/null +++ b/docs/source/_static/configuration_example_fuzzy.yml @@ -0,0 +1,84 @@ +################################################################################################################# +# Simple configuration example with i) no evaluation of the final BPS model, ii) 20 iterations of control-flow # +# discovery, iii) 10 iterations of resource model (fuzzy availability) discovery, and iv) no discovery of # +# extraneous delays. # +################################################################################################################# +# - Increase the num_iterations to (potentially) improve the quality of that discovered model # +# - Visit 'complete_configuration.yml' example for a description of all configurable parameters # +################################################################################################################# +version: 5 +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically computed + start_time: "start_time" + end_time: "end_time" + # Whether to discover case attributes or not + discover_data_attributes: false +################# +# Preprocessing # +################# +preprocessing: + # Threshold to consider two activities as concurrent when computing the enabled time (if necessary) + enable_time_concurrency_threshold: 0.75 +################ +# Control-flow # +################ +control_flow: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: two_gram_distance + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Method for discovering gateway probabilities + gateway_probabilities: discovery + # Discover process model with SplitMiner v3 + mining_algorithm: sm1 + # Number of concurrent relations between events to be captured + epsilon: + - 0.05 + - 0.4 + # Threshold for filtering the incoming and outgoing edges + eta: + - 0.2 + - 0.7 + # Whether to replace non-trivial OR joins or not + replace_or_joins: + - true + - false + # Whether to prioritize parallelism over loops or not + prioritize_parallelism: + - true + - false +################## +# Resource model # +################## +resource_model: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: circadian_emd + # Number of optimization iterations over the search space + num_iterations: 10 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Whether to discover prioritization or batching behavior + discover_prioritization_rules: false + discover_batching_rules: false + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type + discovery_type: differentiated_fuzzy + # Duration of each granule in the resource calendar that will get its own probability + granularity: 60 + # Angle of the fuzzy trapezoid when computing the availability probability for an activity (angle from start to end) + fuzzy_angle: + - 0.1 + - 0.9 diff --git a/docs/source/_static/configuration_example_with_evaluation.yml b/docs/source/_static/configuration_example_with_evaluation.yml new file mode 100644 index 00000000..10f412a7 --- /dev/null +++ b/docs/source/_static/configuration_example_with_evaluation.yml @@ -0,0 +1,109 @@ +################################################################################################################# +# Same simple configuration as 'configuration_example.yml' but evaluation the quality of the final BPS model # +################################################################################################################# +# - Increase the num_iterations to (potentially) improve the quality of that discovered model # +# - Visit 'complete_configuration.yml' example for a description of all configurable parameters # +################################################################################################################# +version: 5 +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically computed + start_time: "start_time" + end_time: "end_time" + # Event log to evaluate the discovered BPS model with + test_log_path: ../event_logs/LoanApp_simplified_test.csv.gz + # Number of evaluations of the discovered BPS model + num_final_evaluations: 10 + # Metrics to evaluate the discovered BPS model + evaluation_metrics: + - 3_gram_distance + - 2_gram_distance + - absolute_event_distribution + - relative_event_distribution + - circadian_event_distribution + - arrival_event_distribution + - cycle_time_distribution + # Whether to discover case attributes or not + discover_data_attributes: false +################# +# Preprocessing # +################# +preprocessing: + # Threshold to consider two activities as concurrent when computing the enabled time (if necessary) + enable_time_concurrency_threshold: 0.75 +################ +# Control-flow # +################ +control_flow: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: two_gram_distance + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Methods for discovering gateway probabilities + gateway_probabilities: discovery + # Discover process model with SplitMiner v3 + mining_algorithm: sm1 + # Number of concurrent relations between events to be captured + epsilon: + - 0.05 + - 0.4 + # Threshold for filtering the incoming and outgoing edges + eta: + - 0.2 + - 0.7 + # Whether to replace non-trivial OR joins or not + replace_or_joins: + - true + - false + # Whether to prioritize parallelism over loops or not + prioritize_parallelism: + - true + - false +################## +# Resource model # +################## +resource_model: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: circadian_emd + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Whether to discover prioritization or batching behavior + discover_prioritization_rules: false + discover_batching_rules: false + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type + discovery_type: differentiated + # Time granularity (in minutes) for the resource calendar (the higher the density of events in the log, the smaller the granularity can be) + granularity: 60 + # Minimum confidence of the intervals in the discovered calendar (of a resource or set of resources) + confidence: + - 0.5 + - 0.85 + # Minimum support of the intervals in the discovered calendar (of a resource or set of resources) + support: + - 0.05 + - 0.5 + # Participation of a resource in the process to discover a calendar for them (gathered together otherwise) + participation: 0.4 +##################### +# Extraneous delays # +##################### +extraneous_activity_delays: + # Method to compute the extraneous delay + discovery_method: eclipse-aware + # Number of optimization iterations over the search space (1 = direct discovery, no optimization stage) + num_iterations: 1 diff --git a/docs/source/_static/configuration_example_with_provided_process_model.yml b/docs/source/_static/configuration_example_with_provided_process_model.yml new file mode 100644 index 00000000..b490161d --- /dev/null +++ b/docs/source/_static/configuration_example_with_provided_process_model.yml @@ -0,0 +1,80 @@ +################################################################################################################# +# Same simple configuration as 'configuration_example.yml' but providing the BPMN model # +################################################################################################################# +# - Increase the num_iterations to (potentially) improve the quality of that discovered model # +# - Visit 'complete_configuration.yml' example for a description of all configurable parameters # +################################################################################################################# +version: 5 +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically computed + start_time: "start_time" + end_time: "end_time" + # Use this process model and skip its discovery + process_model_path: ../models/LoanApp_simplified.bpmn + # Whether to discover case attributes or not + discover_data_attributes: false +################# +# Preprocessing # +################# +preprocessing: + # Threshold to consider two activities as concurrent when computing the enabled time (if necessary) + enable_time_concurrency_threshold: 0.75 +################ +# Control-flow # +################ +control_flow: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: two_gram_distance + # Number of optimization iterations over the search space + num_iterations: 1 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Methods for discovering gateway probabilities + gateway_probabilities: discovery +################## +# Resource model # +################## +resource_model: + # Metric to guide the optimization process (loss function to minimize) + optimization_metric: circadian_emd + # Number of optimization iterations over the search space + num_iterations: 20 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 3 + # Whether to discover prioritization or batching behavior + discover_prioritization_rules: false + discover_batching_rules: false + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type + discovery_type: pool + # Time granularity (in minutes) for the resource calendar (the higher the density of events in the log, the smaller the granularity can be) + granularity: 60 + # Minimum confidence of the intervals in the discovered calendar (of a resource or set of resources) + confidence: + - 0.5 + - 0.85 + # Minimum support of the intervals in the discovered calendar (of a resource or set of resources) + support: + - 0.05 + - 0.5 + # Participation of a resource in the process to discover a calendar for them (gathered together otherwise) + participation: 0.4 +##################### +# Extraneous delays # +##################### +extraneous_activity_delays: + # Method to compute the extraneous delay + discovery_method: eclipse-aware + # Number of optimization iterations over the search space (1 = direct discovery, no optimization stage) + num_iterations: 1 diff --git a/docs/source/_static/configuration_one_shot.yml b/docs/source/_static/configuration_one_shot.yml new file mode 100644 index 00000000..9c45b7e8 --- /dev/null +++ b/docs/source/_static/configuration_one_shot.yml @@ -0,0 +1,69 @@ +################################################################################################################# +# Simple configuration example for running SIMOD without parameter optimization steps. The defined parameters # +# should be individual values and not intervals, as there is no optimization. # +################################################################################################################# +# - Visit 'complete_configuration.yml' example for a description of all configurable parameters # +################################################################################################################# +version: 5 +########## +# Common # +########## +common: + # Path to the event log in CSV format + train_log_path: ../event_logs/LoanApp_simplified_train.csv.gz + # Specify the name for each of the columns in the CSV file (XES standard by default) + log_ids: + case: "case_id" + activity: "activity" + resource: "resource" + enabled_time: "enabled_time" # If not present in the log, automatically computed + start_time: "start_time" + end_time: "end_time" +################ +# Control-flow # +################ +control_flow: + # Number of optimization iterations over the search space + num_iterations: 1 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 1 + # Methods for discovering gateway probabilities + gateway_probabilities: discovery + # Discover process model with SplitMiner v3 + mining_algorithm: sm1 + # Number of concurrent relations between events to be captured + epsilon: 0.3 + # Threshold for filtering the incoming and outgoing edges + eta: 0.5 + # Whether to replace non-trivial OR joins or not + replace_or_joins: false + # Whether to prioritize parallelism over loops or not + prioritize_parallelism: true +################## +# Resource model # +################## +resource_model: + # Number of optimization iterations over the search space + num_iterations: 1 + # Number of times to evaluate each iteration (using the mean of all of them) + num_evaluations_per_iteration: 1 + # Resource profiles configuration + resource_profiles: + # Resource profile discovery type + discovery_type: differentiated + # Time granularity (in minutes) for the resource calendar (the higher the density of events in the log, the smaller the granularity can be) + granularity: 60 + # Minimum confidence of the intervals in the discovered calendar (of a resource or set of resources) + confidence: 0.6 + # Minimum support of the intervals in the discovered calendar (of a resource or set of resources) + support: 0.2 + # Participation of a resource in the process to discover a calendar for them (gathered together otherwise) + participation: 0.4 +##################### +# Extraneous delays # +##################### +extraneous_activity_delays: + # Method to compute the extraneous delay + discovery_method: eclipse-aware + # Number of optimization iterations over the search space (1 = direct discovery, no optimization stage) + num_iterations: 1 diff --git a/docs/source/_static/simod.png b/docs/source/_static/simod.png new file mode 100644 index 0000000000000000000000000000000000000000..722960448bcf6813e54fd66a70999d843211c5fb GIT binary patch literal 48295 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%0D)!0MA4ah<%0Y}D)1SS|NeEtc~ZK9whQGA<>#Vs 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 T!mbM+f}cx@>exJtneYDr>k>#T literal 0 HcmV?d00001 diff --git a/docs/source/api.rst b/docs/source/api.rst new file mode 100644 index 00000000..4272ae75 --- /dev/null +++ b/docs/source/api.rst @@ -0,0 +1,186 @@ +API Reference +============= + +This section provides an overview of the Simod API. + +Usage +----- +To use Simod in your Python code, import the main components: + +.. code-block:: python + + from pathlib import Path + + from simod.event_log.event_log import EventLog + from simod.settings.simod_settings import SimodSettings + from simod.simod import Simod + + # Initialize 'output' folder and read configuration file + output = Path("///") + configuration_path = Path("//.yml") + settings = SimodSettings.from_path(configuration_path) + + # Read and preprocess event log + event_log = EventLog.from_path( + log_ids=settings.common.log_ids, + train_log_path=settings.common.train_log_path, + test_log_path=settings.common.test_log_path, + preprocessing_settings=settings.preprocessing, + need_test_partition=settings.common.perform_final_evaluation, + ) + + # Instantiate and run SIMOD + simod = Simod(settings=settings, event_log=event_log, output_dir=output) + simod.run() + +Modules Overview +---------------- + +Simod's codebase is organized into several key modules: + +- **simod**: The main class that orchestrates the overall functionality. +- **settings**: Handles the parsing and validation of configuration files. +- **event_log**: Manages the IO operations of an event log as well as its preprocessing. +- **control_flow**: Utilities to discover and manage the control-flow model of a BPS model. +- **resource_model**: Utilities to discover and manage the resource model of a BPS model. +- **extraneous_delays**: Utilities to discover and manage the extraneous delays model of a BPS model. +- **simulation**: Manages the data model of a BPS model and its simulation and quality assessment. + +Detailed Module Documentation +----------------------------- + +Below is the detailed documentation for each module: + +SIMOD class +^^^^^^^^^^^ + +.. automodule:: simod.simod + :members: + :undoc-members: + :exclude-members: final_bps_model + +Settings Module +^^^^^^^^^^^^^^^ + +SIMOD settings +"""""""""""""" + +.. automodule:: simod.settings.simod_settings + :members: + :undoc-members: + :exclude-members: model_config, common, preprocessing, control_flow, resource_model, extraneous_activity_delays, version + +Common settings +""""""""""""""" + +.. automodule:: simod.settings.common_settings + :members: + :undoc-members: + :exclude-members: model_config, train_log_path, log_ids, test_log_path, process_model_path, perform_final_evaluation, num_final_evaluations, evaluation_metrics, use_observed_arrival_distribution, clean_intermediate_files, discover_data_attributes, DL, TWO_GRAM_DISTANCE, THREE_GRAM_DISTANCE, CIRCADIAN_EMD, CIRCADIAN_WORKFORCE_EMD, ARRIVAL_EMD, RELATIVE_EMD, ABSOLUTE_EMD, CYCLE_TIME_EMD + +Preprocessing settings +"""""""""""""""""""""" + +.. automodule:: simod.settings.preprocessing_settings + :members: + :undoc-members: + :exclude-members: model_config, multitasking, enable_time_concurrency_threshold, concurrency_thresholds + +Control-flow model settings +""""""""""""""""""""""""""" + +.. automodule:: simod.settings.control_flow_settings + :members: + :undoc-members: + :exclude-members: model_config, SPLIT_MINER_V1, SPLIT_MINER_V2, optimization_metric, num_iterations, num_evaluations_per_iteration, gateway_probabilities, mining_algorithm, epsilon, eta, discover_branch_rules, f_score, replace_or_joins, prioritize_parallelism + +Resource model settings +""""""""""""""""""""""" + +.. automodule:: simod.settings.resource_model_settings + :members: + :undoc-members: + :exclude-members: model_config, optimization_metric, num_iterations, num_evaluations_per_iteration, discovery_type, granularity, confidence, support, participation, discover_prioritization_rules, discover_batching_rules, fuzzy_angle + +Extraneous delays settings +"""""""""""""""""""""""""" + +.. automodule:: simod.settings.extraneous_delays_settings + :members: + :undoc-members: + :exclude-members: model_config, optimization_metric, discovery_method, num_iterations, num_evaluations_per_iteration + +Event Log Module +^^^^^^^^^^^^^^^^ + +.. automodule:: simod.event_log.event_log + :members: + :undoc-members: + :exclude-members: write_xes, train_partition, validation_partition, train_validation_partition, test_partition, log_ids, process_name + +.. automodule:: simod.event_log.preprocessor + :members: + :undoc-members: + :exclude-members: MultitaskingSettings, Settings + +Control-flow Model Module +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: simod.control_flow.settings + :members: + :undoc-members: + :exclude-members: output_dir, provided_model_path, project_name, optimization_metric, gateway_probabilities_method, mining_algorithm, epsilon, eta, replace_or_joins, prioritize_parallelism, f_score, from_hyperopt_dict + +.. automodule:: simod.control_flow.optimizer + :members: + :undoc-members: + :exclude-members: event_log, initial_bps_model, settings, base_directory, best_bps_model, evaluation_measurements, cleanup + +.. automodule:: simod.control_flow.discovery + :members: + :undoc-members: + :exclude-members: add_bpmn_diagram_to_model, SplitMinerV1Settings, SplitMinerV2Settings, discover_process_model_with_split_miner_v1, discover_process_model_with_split_miner_v2 + +Resource Model Module +^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: simod.resource_model.settings + :members: + :undoc-members: + :exclude-members: output_dir, process_model_path, project_name, optimization_metric, calendar_discovery_params, discover_prioritization_rules, discover_batching_rules, from_hyperopt_dict + +.. automodule:: simod.resource_model.optimizer + :members: + :undoc-members: + :exclude-members: event_log, initial_bps_model, settings, base_directory, best_bps_model, evaluation_measurements, cleanup + +Extraneous Delays Model Module +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. automodule:: simod.extraneous_delays.optimizer + :members: + :undoc-members: + :exclude-members: cleanup + +.. automodule:: simod.extraneous_delays.types + :members: + :undoc-members: + :exclude-members: activity_name, delay_id, duration_distribution + +.. automodule:: simod.extraneous_delays.utilities + :members: + :undoc-members: + :exclude-members: + +Simulation Module +^^^^^^^^^^^^^^^^^ + +.. automodule:: simod.simulation.parameters.BPS_model + :members: + :undoc-members: + :exclude-members: process_model, gateway_probabilities, case_arrival_model, resource_model, extraneous_delays, case_attributes, global_attributes, event_attributes, prioritization_rules, batching_rules, branch_rules, calendar_granularity + +.. automodule:: simod.simulation.prosimos + :members: + :undoc-members: + :exclude-members: simulate_in_parallel, evaluate_logs, bpmn_path, parameters_path, output_log_path, num_simulation_cases, simulation_start diff --git a/docs/source/citation.rst b/docs/source/citation.rst new file mode 100644 index 00000000..dead9db0 --- /dev/null +++ b/docs/source/citation.rst @@ -0,0 +1,45 @@ +Cite the Paper +============== + +When using SIMOD for a publication, please cite the following article in you paper: + +`[Citation pending] +`_ + +More References +^^^^^^^^^^^^^^^ + +`Camargo, M., Dumas, M., González, O., 2020. "Automated discovery of +business process simulation models from event logs". Decis. Support Syst. +134, 113284. +`_ + +`Chapela-Campa, D., Dumas, M., 2024. "Enhancing business process +simulation models with extraneous activity delays". Inf. Syst. 122, 102346. +`_ + +`Chapela-Campa, D., Benchekroun, I., Baron, O., Dumas, M., Krass, D., +Senderovich, A., 2025. "A framework for measuring the quality of business +process simulation models". Inf. Syst. 127, 102447. +`_ + +`Lashkevich, K., Milani, F., Chapela-Campa, D., Suvorau, I., Dumas, M., +2024. "Unveiling the causes of waiting time in business processes from event +logs". Inf. Syst. 126, 102434. +`_ + +`López-Pintado, O., Dumas, M., Berx, J., 2024a. "Discovery, simulation, and +optimization of business processes with differentiated resources". Inf. Syst. +120, 102289. +`_ + +`López-Pintado, O., Dumas, M., 2023. "Discovery and simulation of business +processes with probabilistic resource availability calendars", in: Proceedings +of the 5th International Conference on Process Mining (ICPM), IEEE. pp. +1–8. +`_ + +`López-Pintado, O., Murashko, S., Dumas, M., 2024b. "Discovery and +simulation of data-aware business processes", in: Proceedings of the 6th +International Conference on Process Mining (ICPM), IEEE. pp. 105–112. +`_ diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 00000000..7fee7355 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,47 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'SIMOD' +copyright = '2025, UT Information Systems Research Group' +author = 'UT Information Systems Research Group' +release = '5.1.2' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +import os +import sys + +# Get the absolute path of the project's root directory +sys.path.insert(0, os.path.abspath("../../src")) # Adjust if necessary + +extensions = [ + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx_autodoc_typehints" +] + +intersphinx_mapping = { + "python": ("https://docs.python.org/3.9", None), + "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None), +} + +templates_path = ['_templates'] +exclude_patterns = [] +autodoc_class_attributes = False + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] + +# Automatically generate summaries +autosummary_generate = True diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000..666f98f6 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,43 @@ +.. SIMOD documentation master file, created by + sphinx-quickstart on Mon Jan 27 16:09:16 2025. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +SIMOD: Automated discovery of business process simulation models +================================================================ + +SIMOD combines process mining and machine learning techniques to automate the discovery and tuning of Business Process +Simulation models from event logs extracted from enterprise information systems (ERPs, CRM, case management systems, +etc.). SIMOD takes as input an event log in CSV format, a configuration file, and (optionally) a BPMN process model, +and discovers a business process simulation model that can be simulated using the Prosimos simulator, which is embedded +in SIMOD. + + +.. _fig_simod: +.. figure:: _static/simod.png + :align: center + :scale: 60% + + SIMOD main workflow. + + +In its standard workflow, SIMOD receives an event log and a configuration file, and +runs an iterative process to discover the BPS model that bests reflect the behavior captured in the input event log. +This iterative process is designed as a pipeline-based architecture composed of multiple stages that run a +TPE-optimization process to obtain the parameters that lead to the most accurate model. + +Alternatively, SIMOD can additionally receive as input a BPMN model of the process. In this case, SIMOD skips the +corresponding discovery phase, and builds the BPS model over the input BPMN model. + +.. note:: + This project is under active development. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + installation + usage + api + citation diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 00000000..c1292089 --- /dev/null +++ b/docs/source/installation.rst @@ -0,0 +1,88 @@ +Installation Guide +================== + +This guide provides instructions on how to install SIMOD using **pip** (PyPI) or **Docker**. + +Prerequisites +------------- +Before installing SIMOD, ensure you have the following dependencies: + +Dependencies for local installation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **Python 3.9, 3.10, or 3.11**: The recommended version (extensively tested) is Python 3.9, however, it also works for + Python versions 3.10 and 3.11. +- **Java 1.8**: Ensure Java is installed and added to your system’s PATH (e.g., + `Java.com `_). +- **Rust and Cargo (\*)**: If you are on a system without precompiled dependencies, you may also need to compile Rust + and Cargo (install them using `rustup.rs `_). + +Dependencies for Docker installation +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +- **Docker**: If you want to run SIMOD without installing dependencies, you can use the official Docker image (install + Docker from `https://www.docker.com/get-started/ `_). + +Installation via PyPI +--------------------- +The simplest way to install SIMOD is via **pip** from PyPI (`simod project `_): + +.. code-block:: bash + + python -m pip install simod + +Running SIMOD after installation: + +.. code-block:: bash + + simod --help + +Installation via Docker +----------------------- +If you prefer running SIMOD inside a **Docker container**, in an isolated environment without requiring Python or Java +installations, use the following commands: + +.. code-block:: bash + + docker pull nokal/simod + +To start a container: + +.. code-block:: bash + + docker run -it -v /path/to/resources/:/usr/src/Simod/resources -v /path/to/output:/usr/src/Simod/outputs nokal/simod bash + +Use the `resources/` directory to store event logs and configuration files. The `outputs/` directory will contain the +results of SIMOD. + +From inside the container, you can run SIMOD with: + +.. code-block:: bash + + poetry run simod --help + +Docker images for different SIMOD versions are available at `https://hub.docker.com/r/nokal/simod/tags `_ + +Installation via source code +---------------------------- +If you prefer to download the source code and compile it directly (you would need to have `git`, `python`, and +`poetry` installed), use the following commands: + +.. code-block:: bash + + git clone https://github.com/AutomatedProcessImprovement/Simod.git + + cd Simod + + python -m venv simod-env + + # source ./simod-env/Scripts/activate # for Linux systems + .\simod-env\Scripts\activate.bat + + poetry install + +Running SIMOD after installation: + +.. code-block:: bash + + simod --help diff --git a/docs/source/usage.rst b/docs/source/usage.rst new file mode 100644 index 00000000..cf837735 --- /dev/null +++ b/docs/source/usage.rst @@ -0,0 +1,83 @@ +Usage Guide +=========== + +This guide provides instructions on how to use SIMOD from command line to discover a BPS model out of an event log in +CSV format. + +Running Simod +------------- + +Once Simod is installed (see `Installation `_), you can run it by specifying a configuration file. + +Installed via PyPI or source code +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + simod --configuration resources/config/configuration_example.yml + +Replace `resources/config/configuration_example.yml` with the path to your own configuration file. Paths can be +relative to the configuration file or absolute. + + +Installed via Docker +^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: bash + + poetry run simod --configuration resources/config/configuration_example.yml + +Replace `resources/config/configuration_example.yml` with the path to your own configuration file. Paths can be +relative to the configuration file or absolute. + +Configuration File +------------------ +The configuration file is a YAML file that specifies various parameters for Simod. Ensure that the path to your event +log is specified in the configuration file. Here are some configuration examples: + +- Basic configuration to discover the full BPS + model (`basic <_static/configuration_example.yml>`_). +- Basic configuration to discover the full BPS model using fuzzy (probabilistic) resource + calendars (`probabilistic <_static/configuration_example_fuzzy.yml>`_). +- Basic configuration to discover the full BPS model with data-aware branching rules + (`data-aware <_static/configuration_example_data_aware.yml>`_). +- Basic configuration to discover the full BPS model, and evaluate it with a specified event + log (`with evaluation <_static/configuration_example_with_evaluation.yml>`_). +- Basic configuration to discover a BPS model with a provided BPMN process model as starting + point (`with BPMN model <_static/configuration_example_with_provided_process_model.yml>`_). +- Basic configuration to discover a BPS model with no optimization process (one-shot) + (`one-shot <_static/configuration_one_shot.yml>`_). +- Complete configuration example with all the possible + parameters (`complete config <_static/complete_configuration.yml>`_). + +Event Log Format +---------------- +Simod takes as input an event log in CSV format. + +.. _tab_event_log: +.. table:: Sample of input event log format. + :align: center + + ======= =========== =================== =================== ======== + case_id activity start_time end_time resource + ======= =========== =================== =================== ======== + 512 Create PO 03/11/2021 08:00:00 03/11/2021 08:31:11 DIO + 513 Create PO 03/11/2021 08:34:21 03/11/2021 09:02:09 DIO + 514 Create PO 03/11/2021 09:11:11 03/11/2021 09:49:51 DIO + 512 Approve PO 03/11/2021 12:13:06 03/11/2021 12:44:21 Joseph + 513 Reject PO 03/11/2021 12:30:51 03/11/2021 13:15:50 Jolyne + 514 Approve PO 03/11/2021 12:59:11 03/11/2021 13:32:36 Joseph + 512 Check Stock 03/11/2021 14:22:10 03/11/2021 14:49:22 DIO + 514 Check Stock 03/11/2021 15:11:01 03/11/2021 15:46:12 DIO + 514 Order Goods 04/11/2021 09:46:12 04/11/2021 10:34:23 Joseph + 512 Pack Goods 04/11/2021 10:46:50 04/11/2021 11:18:02 Giorno + ======= =========== =================== =================== ======== + +The column names can be specified as part of the configuration file (`see here <_static/complete_configuration.yml>`_). + +Output +------ +Simod discovers a business process simulation model that can be simulated using the +`Prosimos simulator `_, which is embedded in Simod. + +Once SIMOD is finished, the discovered BPS model can be found in the `outputs` directory, under the folder `best_result`. From e0a232b901b871301a82fd60210fb0344b957c8a Mon Sep 17 00:00:00 2001 From: David Chapela de la Campa Date: Thu, 6 Feb 2025 18:23:39 +0200 Subject: [PATCH 13/13] Add 'Read the Docs' configuration file --- .readthedocs.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..b511ffe4 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.9" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt