Multilog Analysis (Logx) - #52
Conversation
710b482 to
5623020
Compare
5623020 to
ecedc94
Compare
ecedc94 to
01f280b
Compare
| "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." |
There was a problem hiding this comment.
What is this about network bandwidth? Not sure that this is accurate.
There was a problem hiding this comment.
That was more from the MCP perspective, I'll take that out
| parsing_errors_limit=parsing_errors_limit, | ||
| verbose=verbose, | ||
| ) | ||
| self.file_path = filepath |
There was a problem hiding this comment.
Is this used anywhere?
There was a problem hiding this comment.
If it's only for the sake of the run collection, I suspect your metadata Pydantic model can cover this functionality.
There was a problem hiding this comment.
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
|
|
||
| class RunMetadata(BaseModel): | ||
| """Internal model for telemetry run index metadata.""" | ||
| file_path: str = Field(description="Absolute path to the log file") |
There was a problem hiding this comment.
Why is this a string object. Python should have Path objects right?
| def __getitem__(self, index: int) -> Analyzer: | ||
| return self.get_run(index) | ||
|
|
||
| def filter(self, predicate: Callable[[RunMetadata], bool]) -> "RunCollection": |
There was a problem hiding this comment.
future annotations should fix this quotation marks
| 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 |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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?
| go.Figure | ||
| Plotly Figure with overlaid traces from all runs. | ||
| """ | ||
| return plot_comparison(self.runs, cpp_name, max_points=max_points) |
There was a problem hiding this comment.
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)
|
|
||
| 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) |
There was a problem hiding this comment.
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
| 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)), |
There was a problem hiding this comment.
This is a clear spot where you would want a strongly typed pydantic model
| Parameters | ||
| ---------- | ||
| header_line : str | ||
| First line of the log file, e.g. ``"PER Log: Thu Jun 11 17:06:37 2026 v2.0"``. |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Stored the filepaths in the RunMetaData
| ) | ||
| ) | ||
|
|
||
| return plot_single_axis( |
There was a problem hiding this comment.
reused plot_single_axis :)
| continue | ||
| yield run, analyzer | ||
|
|
||
| def filter(self, predicate: Callable[[RunMetadata], bool]) -> RunCollection: |
There was a problem hiding this comment.
renamed to properly represent what the functions are doing: filtering
There was a problem hiding this comment.
Tried to follow the format of analyzer.py and analyzer_factory.py
| from .integrate import average_over_time_range | ||
|
|
||
|
|
||
| class DataInstanceSummary(BaseModel): |
There was a problem hiding this comment.
Added new pydantic model for DI level summary as discussed
There was a problem hiding this comment.
Just edited minor points corresponding to the change of data_instance_summary
There was a problem hiding this comment.
The global path insert to fix pytest issue
| - 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. |
There was a problem hiding this comment.
I changed this because it contrasts the precommit formatting.
| rev: v2.3.1 | ||
| hooks: | ||
| - id: autoflake | ||
| exclude: conftest\.py$ |
There was a problem hiding this comment.
This was bc it kept removing the "unused imports" in conftest.py files
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
Run Catalog & Scanner
Overlay Graphing & Downsampling
Hybrid Semantic Search