Skip to content

Multilog Analysis (Logx) - #52

Open
Jeffrey-Moon wants to merge 12 commits into
mainfrom
multilog_analysis
Open

Multilog Analysis (Logx)#52
Jeffrey-Moon wants to merge 12 commits into
mainfrom
multilog_analysis

Conversation

@Jeffrey-Moon

@Jeffrey-Moon Jeffrey-Moon commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Added tests to test the features but they were AI generated as a result of me being lazy... If you can think of edge cases, let me know.

Zero-Copy Parser Optimization

  • Replaced Polars group_by loops with an $O(N)$ contiguous boundary detector.
  • Slices telemetry values into zero-copy NumPy array views of master columns, dropping parsing time to 60ms and saving 280MB of RAM per log for a test log with size $736.9\text{MB}$.
  • Parses log start timestamps from the header.

Run Catalog & Scanner

  • Indexes directory log headers in under 1ms per file.
  • Implemented date filtering and lazy evaluation in check_any with automatic garbage collection to prevent OOM leaks.
  • Added unified path resolution for directories, wildcards, and list inputs.

Overlay Graphing & Downsampling

  • Added plot_comparison and compare_summary to overlay relative time coordinates
  • Implemented max_points parameter using zero-copy step-slicing.
  • Fixed step calculation using ceiling division (n_points + max_points - 1) // max_points to enforce a strict upper bound.

Hybrid Semantic Search

  • Added optional SentenceTransformer vector search
  • Caches embeddings in-memory with automatic invalidation.
  • Lazy-loads imports to execute on keyword search if PyTorch/models are missing offline

@Jeffrey-Moon
Jeffrey-Moon force-pushed the multilog_analysis branch 2 times, most recently from 710b482 to 5623020 Compare July 17, 2026 23:37
Comment thread perda/analyzer/__init__.py

@alex-yang-upenn alex-yang-upenn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass

Comment thread notebooks/Tutorial_[simple].ipynb Outdated
"source": [
"#### Parameterized Downsampling\n",
"\n",
"To speed up rendering or save network bandwidth, you can optionally restrict the number of points plotted using the `max_points` parameter. This applies a memory-efficient zero-copy step downsampling."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this about network bandwidth? Not sure that this is accurate.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was more from the MCP perspective, I'll take that out

Comment thread notebooks/Tutorial_[simple].ipynb
Comment thread perda/analyzer/analyzer.py Outdated
parsing_errors_limit=parsing_errors_limit,
verbose=verbose,
)
self.file_path = filepath

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's only for the sake of the run collection, I suspect your metadata Pydantic model can cover this functionality.

Comment thread perda/analyzer/comparison.py Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file needs to be split up.

Make (or reuse!) a generic plotting function in the plotting module, then whatever lives in the Analyzer is a wrapper around the generic plotting function.

Try not to let plotting logic leak into the Analyzer, and vice versa, try not to write plotting functions that are application specific. It really blows up the size of our codebase for no good reason

Comment thread perda/analyzer/run_collection.py Outdated

class RunMetadata(BaseModel):
"""Internal model for telemetry run index metadata."""
file_path: str = Field(description="Absolute path to the log file")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a string object. Python should have Path objects right?

Comment thread perda/analyzer/run_collection.py Outdated
def __getitem__(self, index: int) -> Analyzer:
return self.get_run(index)

def filter(self, predicate: Callable[[RunMetadata], bool]) -> "RunCollection":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

future annotations should fix this quotation marks

Comment thread perda/analyzer/run_collection.py Outdated
Comment on lines +153 to +161
def predicate(item: RunMetadata) -> bool:
d = item.date
if d is None:
return False
if start_date and d < start_date:
return False
if end_date and d > end_date:
return False
return True

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lambda function please since you're only using it once / immediately passing it in.

Also, can the if statements be a bit more condensed and easier to reason with? Something like

return (
d is not None
and (start_date is None or d >= start_date)
and (end_date is None or d <= end_date)
)

Usually easier to think about the positive criteria rather than the negative criteria

Comment thread perda/analyzer/run_collection.py Outdated
Comment on lines +165 to +197
def check_any(self, cpp_name: str, condition_fn: Callable[[np.ndarray], bool]) -> List[str]:
"""Find log filenames in the collection where a condition holds for a given variable.

Evaluates runs lazily to keep memory usage bounded.

Parameters
----------
cpp_name : str
C++ name of the variable to check.
condition_fn : Callable[[np.ndarray], bool]
Boolean condition function operating on the variable's numpy array values.

Returns
-------
List[str]
List of filenames where the condition evaluated to True.
"""
matching_filenames = []
for i in range(len(self._items)):
item = self._items[i]
if item.analyzer is not None:
aly = item.analyzer
else:
try:
aly = Analyzer(item.file_path, verbose=0)
except Exception:
continue # Skip unreadable file

if cpp_name in aly.data:
di = aly.data[cpp_name]
if condition_fn(di.value_np):
matching_filenames.append(item.filename)
return matching_filenames

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Im tweaking, but is this not just a filter function with a more powerful predicate? Surely you can accomplish all of this with filter, if you just write a predicate that actually accesses data from the Analyzer?

Comment thread perda/analyzer/run_collection.py Outdated
go.Figure
Plotly Figure with overlaid traces from all runs.
"""
return plot_comparison(self.runs, cpp_name, max_points=max_points)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reiterating my point above. I think you should make the sacrifice of greater complexity here in exchange for being able to have a generic plotting function (and perhaps even reuse existing generics)

Comment thread perda/analyzer/run_collection.py Outdated

def compare_summary(self, cpp_name: str) -> Dict[str, Dict[str, float]]:
"""Compare variable statistics across all runs in the collection."""
return compare_summary(self.runs, cpp_name)

@alex-yang-upenn alex-yang-upenn Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reiterating what I said above about consolidation around Analyzer or SingleRunData having full ownership over how to generate a summary for a single cpp_name, and then everyone else constructs their summaries using that canonical summary as a building block

Comment thread perda/analyzer/comparison.py Outdated
Comment on lines +119 to +124
comparison[run_label] = {
"min": float(np.min(vals)),
"max": float(np.max(vals)),
"mean": float(np.mean(vals)),
"std": float(np.std(vals)),
"len": int(len(vals)),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clear spot where you would want a strongly typed pydantic model

Comment thread perda/analyzer/csv.py
Parameters
----------
header_line : str
First line of the log file, e.g. ``"PER Log: Thu Jun 11 17:06:37 2026 v2.0"``.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the csv_utils.py and moved the helpers to here. Also renamed the function names to be more specific.

COMPARISON_TIMESTAMP_UNIT = Timescale.US


class RunMetadata(BaseModel):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stored the filepaths in the RunMetaData

)
)

return plot_single_axis(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reused plot_single_axis :)

continue
yield run, analyzer

def filter(self, predicate: Callable[[RunMetadata], bool]) -> RunCollection:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to properly represent what the functions are doing: filtering

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried to follow the format of analyzer.py and analyzer_factory.py

from .integrate import average_over_time_range


class DataInstanceSummary(BaseModel):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added new pydantic model for DI level summary as discussed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just edited minor points corresponding to the change of data_instance_summary

Comment thread pyproject.toml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global path insert to fix pytest issue

Comment thread CLAUDE.md
- All numpy typing must be precise (e.g. `np.ndarray` is not allowed; use `NDArray[Float64]` instead). Use `numpy.typing` for this purpose.

- DO NOT use scoped imports, only use imports at the top of the file. When you have many imports, use asterisk to avoid long import lists
- DO NOT use scoped imports, only use imports at the top of the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this because it contrasts the precommit formatting.

Comment thread .pre-commit-config.yaml
rev: v2.3.1
hooks:
- id: autoflake
exclude: conftest\.py$

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was bc it kept removing the "unused imports" in conftest.py files

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants