Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 55 additions & 16 deletions tmll/common/models/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,26 +35,36 @@ def from_tsp_experiment(cls, tsp_experiment) -> 'Experiment':
def assign_outputs(self, outputs: List[Output]) -> None:
self.outputs = outputs

# If there is multiple outputs with the same name, use number to differentiate them
name_counts = {}
for output in self.outputs:
name = output.name
count = name_counts.get(name, 0) + 1
name_counts[name] = count

if count > 1 or name in [o.name for o in self.outputs if o != output]:
output.name = f"{name} ({count})"

def __repr__(self) -> str:
return f"Experiment(name={self.name}, uuid={self.uuid}, start={self.start}, end={self.end}, num_events={self.num_events}, indexing={self.indexing}, traces={self.traces}, outputs={self.outputs})"

def find_outputs(self, keyword: Optional[Union[str, List[str]]] = None, type: Optional[Union[str, List[str]]] = None, match_any: bool = False) -> List[Output]:
"""
Find outputs based on various criteria with flexible matching logic.

Examples:
# AND logic (default)
find_outputs(keyword="cpu", type="time_series") # Must match both

# OR logic
find_outputs(keyword="cpu", type="event", match_any=True) # Can match either

# Multiple values for each criteria
find_outputs(
keyword=["cpu", "memory"], # With match_any=False: must contain both words
type=["time_graph", "xy"], # With match_any=True: can contain either word
)

:param keyword: The keyword(s) to search for in the output name, description, and id
:type keyword: str or List[str], optional
:param type: The type(s) to search for in the output type
Expand All @@ -67,7 +77,7 @@ def find_outputs(self, keyword: Optional[Union[str, List[str]]] = None, type: Op
# Convert single strings to lists for consistent processing
keywords = [keyword] if isinstance(keyword, str) else keyword
types = [type] if isinstance(type, str) else type

# If no criteria provided, return all outputs
if not any([keywords, types]):
return self.outputs
Expand All @@ -79,7 +89,7 @@ def matches_keywords(text: str, keys: List[str], match_any: bool) -> bool:

if not keys:
return True

if match_any:
return any(k in text for k in keys)
return all(k in text for k in keys)
Expand All @@ -89,25 +99,54 @@ def matches_keywords(text: str, keys: List[str], match_any: bool) -> bool:
# Check keywords in name, description, and id
search_text = f"{output.name} {output.description} {output.id}"
keywords_match = matches_keywords(search_text, keywords or [], match_any)

# Check type
type_match = matches_keywords(output.type, types or [], match_any)

if keywords_match and type_match:
matches.append(output)

return sorted(matches, key=lambda x: x.name)
def get_output(self, output_id: str) -> Optional[Output]:

def get_output_by_id(self, output_id: str) -> Optional[Output]:
"""
Get the output with the given ID.

:param output_id: The ID of the output to get
:type output_id: str
:return: The output with the given ID, or None if not found
:rtype: Optional[Output]
"""
for output in self.outputs:
if output.id == output_id:
return output
return None
return next((o for o in self.outputs if o.id == output_id), None)

def get_output_by_name(self, output_name: str, partial_match: bool = False) -> Optional[Output]:
"""
Get the output with the given name (case insensitive).

:param output_name: The name of the output to get
:type output_name: str
:param partial_match: Whether to allow partial matches
:type partial_match: bool, optional
:return: The output with the given name, or None if not found
:rtype: Optional[Output]
"""
if partial_match:
return next((o for o in self.outputs if output_name.lower() in o.name.lower()), None)

return next((o for o in self.outputs if o.name.lower() == output_name.lower()), None)

def get_outputs_by_name(self, output_name: str, partial_match: bool = False) -> List[Output]:
"""
Get all outputs with the given name (case insensitive).

:param output_name: The name of the output to get
:type output_name: str
:param partial_match: Whether to allow partial matches
:type partial_match: bool, optional
:return: The list of outputs with the given name
:rtype: List[Output]
"""
if partial_match:
return [o for o in self.outputs if output_name.lower() in o.name.lower()]

return [o for o in self.outputs if o.name.lower() == output_name.lower()]
13 changes: 7 additions & 6 deletions tmll/ml/modules/anomaly_detection/anomaly_detection_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ def plot_anomalies(self, anomaly_detection_results: Optional[AnomalyDetectionRes
fig_dpi = kwargs.get("fig_dpi", 500)
colors = plt.colormaps.get_cmap("tab10")

for idx, (name, dataframe) in enumerate(self.dataframes.items()):
for idx, (id, dataframe) in enumerate(self.dataframes.items()):
output = self.experiment.get_output_by_id(id)
name = output.name if output else id
plots = []
# Plot the original data
plots.append({
Expand All @@ -158,8 +160,7 @@ def plot_anomalies(self, anomaly_detection_results: Optional[AnomalyDetectionRes
})

# Append the anomaly periods to the plots as span plot
for start, end in anomaly_detection_results.anomaly_periods[name]:
# print(f"Anomaly detected from {start} to {end}")
for start, end in anomaly_detection_results.anomaly_periods[id]:
plots.append({
"label": "Anomaly Period",
"plot_type": "span",
Expand All @@ -172,13 +173,13 @@ def plot_anomalies(self, anomaly_detection_results: Optional[AnomalyDetectionRes
})

anomaly_points_list = []
for point in anomaly_detection_results.anomalies[name].index:
if not anomaly_detection_results.anomalies[name].loc[point].any():
for point in anomaly_detection_results.anomalies[id].index:
if not anomaly_detection_results.anomalies[id].loc[point].any():
continue

# Check if the point is within any anomaly period
in_anomaly_period = False
for start, end in anomaly_detection_results.anomaly_periods[name]:
for start, end in anomaly_detection_results.anomaly_periods[id]:
if start <= point <= end:
in_anomaly_period = True
break
Expand Down
Loading