cicwave 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cicwave/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """
2
+ cicwave - Advanced waveform viewer with PyQtGraph backend
3
+
4
+ A standalone waveform viewer extracted from cicsim, focused on providing
5
+ high-performance visualization of simulation data with PyQtGraph and Qt6.
6
+
7
+ Supports multiple file formats:
8
+ - ngspice .raw files
9
+ - CSV, TSV, Excel
10
+ - Parquet, HDF5, Feather
11
+ - VCD digital waveforms
12
+ - LitePoint .iqvsa IQ capture files
13
+
14
+ Features:
15
+ - GPU-accelerated rendering with PyQtGraph
16
+ - Multi-dimensional data pivoting
17
+ - Session save/restore
18
+ - Export to PDF/PNG/SVG
19
+ - Digital waveform support
20
+ - Engineering unit formatting
21
+ """
22
+
23
+ __version__ = "1.0.0"
24
+ __author__ = "Carsten Wulff"
25
+ __email__ = "carsten@wulff.no"
cicwave/cli.py ADDED
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env python3
2
+ ######################################################################
3
+ ## Copyright (c) 2020 Carsten Wulff Software, Norway
4
+ ## ###################################################################
5
+ ## Created : wulff at 2020-10-23
6
+ ## ###################################################################
7
+ ## The MIT License (MIT)
8
+ ##
9
+ ## Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ ## of this software and associated documentation files (the "Software"), to deal
11
+ ## in the Software without restriction, including without limitation the rights
12
+ ## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ ## copies of the Software, and to permit persons to whom the Software is
14
+ ## furnished to do so, subject to the following conditions:
15
+ ##
16
+ ## The above copyright notice and this permission notice shall be included in all
17
+ ## copies or substantial portions of the Software.
18
+ ##
19
+ ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ ## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ ## SOFTWARE.
26
+ ##
27
+ ######################################################################
28
+
29
+ import os
30
+ import sys
31
+ import glob as _glob
32
+ import click
33
+ from .command import setup_logging
34
+
35
+
36
+ def _resolve_wave_x_from_cli_env(cli_x):
37
+ """Apply --x, else CICWAVE_X environment variable."""
38
+ if cli_x:
39
+ return cli_x
40
+ v = os.environ.get("CICWAVE_X")
41
+ if v and str(v).strip():
42
+ return str(v).strip()
43
+ return None
44
+
45
+
46
+ def _expand_glob_patterns(files, patterns):
47
+ """Merge positional ``files`` with files matched by --glob ``patterns``.
48
+
49
+ Each pattern is expanded with ``glob.glob(..., recursive=True)`` so that
50
+ ``**`` works. Order: positional first, then each pattern in order. Files
51
+ are de-duplicated while preserving first-seen order. Patterns that match
52
+ nothing are reported on stderr but not fatal.
53
+ """
54
+ out = []
55
+ seen = set()
56
+ for f in files:
57
+ if f not in seen:
58
+ seen.add(f)
59
+ out.append(f)
60
+ for pat in patterns or ():
61
+ matches = sorted(_glob.glob(pat, recursive=True))
62
+ if not matches:
63
+ print("warning: --glob '%s' matched no files" % pat,
64
+ file=sys.stderr)
65
+ continue
66
+ for m in matches:
67
+ if m not in seen:
68
+ seen.add(m)
69
+ out.append(m)
70
+ return tuple(out)
71
+
72
+
73
+ def _run_wave_pg(files, x, sheet, pivot_spec=None,
74
+ pivot_info_flag=False, session_path=None, export_path=None):
75
+ """Run the PyQtGraph waveform viewer."""
76
+ x = _resolve_wave_x_from_cli_env(x)
77
+
78
+ if pivot_spec:
79
+ from .pivot import load_spec, pivot_info, apply_pivot
80
+ from .wavefiles import WaveFile
81
+ spec = load_spec(pivot_spec)
82
+
83
+ if pivot_info_flag:
84
+ for f in files:
85
+ wf = WaveFile(f, x or "")
86
+ print("--- %s ---" % f)
87
+ print(pivot_info(wf.df, spec))
88
+ print()
89
+ return
90
+
91
+ if not x and spec.get('columns'):
92
+ x = spec['columns']
93
+
94
+ # Import the PyQtGraph backend
95
+ try:
96
+ from .wave_pg import CmdWavePg
97
+ except ImportError as e:
98
+ print("Error: PyQtGraph backend requires PySide6 and pyqtgraph")
99
+ print("Install with: pip install PySide6 pyqtgraph")
100
+ print(f" ({e})")
101
+ sys.exit(1)
102
+
103
+ c = CmdWavePg(x)
104
+
105
+ if session_path:
106
+ c.win.applySession(session_path)
107
+
108
+ if pivot_spec:
109
+ for f in files:
110
+ wf = WaveFile(f, x or "")
111
+ pivoted = apply_pivot(wf.df, spec)
112
+ name = "pivot(%s)" % os.path.basename(f)
113
+ c.openDataFrame(pivoted, name,
114
+ pivot_spec_path=os.path.abspath(pivot_spec),
115
+ original_path=os.path.abspath(f))
116
+ else:
117
+ for f in files:
118
+ c.openFile(f, sheet_name=sheet)
119
+
120
+ if export_path:
121
+ c.exportAndExit(export_path)
122
+ else:
123
+ c.run()
124
+
125
+
126
+ @click.command()
127
+ @click.argument("files", nargs=-1)
128
+ @click.option("--glob", "globs", multiple=True,
129
+ help="Glob pattern (repeatable). Supports ** for recursion. "
130
+ "Useful on shells like PowerShell that don't auto-expand.")
131
+ @click.option("--x", default=None,
132
+ help="X-axis column; else CICWAVE_X; else saved default; else auto")
133
+ @click.option("--sheet", default=None, help="Sheet name for Excel files (default: first sheet)")
134
+ @click.option("--pivot", default=None, help="Pivot spec file (YAML/JSON)")
135
+ @click.option("--pivot-info", is_flag=True, default=False, help="Print pivot dimensions and exit")
136
+ @click.option("--session", default=None, help="Load session file (.cicwave.yaml)")
137
+ @click.option("--export", default=None, help="Export plot to file (PDF/PNG/SVG) and exit")
138
+ @click.option("--color/--no-color", default=True, help="Enable/Disable color output")
139
+ @click.option("--debug", is_flag=True, default=False, help="Enable debug logging")
140
+ def main(files, globs, x, sheet, pivot, pivot_info, session, export, color, debug):
141
+ """cicwave: Advanced waveform viewer with PyQtGraph backend.
142
+
143
+ A high-performance waveform viewer focused on PyQtGraph and Qt6 for
144
+ advanced visualization of simulation data.
145
+
146
+ Supports: .raw, .csv, .tsv, .xlsx, .json, .parquet, .feather, .h5,
147
+ .pkl, .vcd (digital), .iqvsa (LitePoint), and more.
148
+
149
+ \b
150
+ Pivot:
151
+ --pivot spec.yaml Reshape data using pivot spec before viewing
152
+ --pivot-info Print unique values per pivot dimension and exit
153
+
154
+ \b
155
+ Session:
156
+ --session plot.cicwave.yaml Load saved session
157
+ --export plot.pdf Export to file and exit (no GUI)
158
+ --session s.yaml --export out.pdf Restore session and export
159
+
160
+ \b
161
+ Globs:
162
+ --glob 'data/*.csv' Repeatable, supports ** for recursion
163
+ --glob '**/*.raw' Useful on PowerShell which doesn't
164
+ auto-expand patterns
165
+ """
166
+ # Set up logging
167
+ import logging
168
+ level = logging.DEBUG if debug else logging.INFO
169
+ setup_logging(color=color, level=level)
170
+
171
+ files = _expand_glob_patterns(files, globs)
172
+ _run_wave_pg(files, x, sheet, pivot, pivot_info, session, export)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ main()
cicwave/command.py ADDED
@@ -0,0 +1,71 @@
1
+ ######################################################################
2
+ ## Copyright (c) 2020 Carsten Wulff Software, Norway
3
+ ## ###################################################################
4
+ ## Created : wulff at 2020-10-24
5
+ ## ###################################################################
6
+ ## The MIT License (MIT)
7
+ ##
8
+ ## Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ ## of this software and associated documentation files (the "Software"), to deal
10
+ ## in the Software without restriction, including without limitation the rights
11
+ ## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ ## copies of the Software, and to permit persons to whom the Software is
13
+ ## furnished to do so, subject to the following conditions:
14
+ ##
15
+ ## The above copyright notice and this permission notice shall be included in all
16
+ ## copies or substantial portions of the Software.
17
+ ##
18
+ ## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ ## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ ## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ ## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ ## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ ## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ ## SOFTWARE.
25
+ ##
26
+ ######################################################################
27
+
28
+ import logging
29
+
30
+ logger = logging.getLogger("cicwave")
31
+
32
+
33
+ class ColoredFormatter(logging.Formatter):
34
+ COLORS = {
35
+ logging.DEBUG: '\033[36m',
36
+ logging.INFO: '\033[32m',
37
+ logging.WARNING: '\033[93m',
38
+ logging.ERROR: '\033[31m',
39
+ logging.CRITICAL: '\033[31m',
40
+ }
41
+ RESET = '\033[0m'
42
+
43
+ def format(self, record):
44
+ msg = super().format(record)
45
+ color = self.COLORS.get(record.levelno, '')
46
+ return f"{color}{msg}{self.RESET}"
47
+
48
+
49
+ def setup_logging(color=True, level=logging.INFO):
50
+ """Configure the cicwave logger with optional colored output."""
51
+ log = logging.getLogger("cicwave")
52
+ log.setLevel(level)
53
+ log.handlers.clear()
54
+
55
+ handler = logging.StreamHandler()
56
+ handler.setLevel(level)
57
+
58
+ if color:
59
+ formatter = ColoredFormatter("%(message)s")
60
+ else:
61
+ formatter = logging.Formatter("%(levelname)-7s | %(message)s")
62
+
63
+ handler.setFormatter(formatter)
64
+ log.addHandler(handler)
65
+
66
+
67
+ class Command:
68
+ """Minimal base command class for cicwave."""
69
+
70
+ def __init__(self):
71
+ pass
cicwave/ngraw.py ADDED
@@ -0,0 +1,105 @@
1
+ # MIT license: https://opensource.org/licenses/MIT
2
+ # See https://github.com/Isotel/mixedsim/blob/master/python/ngspice_read.py
3
+ # for a more complete library. Isotel's version is GPL licensed
4
+ from __future__ import division
5
+ #import division
6
+ import numpy as np
7
+ import pandas as pd
8
+ BSIZE_SP = 512 # Max size of a line of data; we don't want to read the
9
+ # whole file to find a line, in case file does not have
10
+ # expected structure.
11
+ MDATA_LIST = [b'title', b'date', b'plotname', b'flags', b'no. variables',
12
+ b'no. points', b'dimensions', b'command', b'option']
13
+ def ngRawRead(fname: str):
14
+ """Read ngspice binary raw files. Return tuple of the data, and the
15
+ plot metadata. The dtype of the data contains field names. This is
16
+ not very robust yet, and only supports ngspice.
17
+ >>> darr, mdata = rawread('test.py')
18
+ >>> darr.dtype.names
19
+ >>> plot(np.real(darr['frequency']), np.abs(darr['v(out)']))
20
+ """
21
+ # Example header of raw file
22
+ # Title: rc band pass example circuit
23
+ # Date: Sun Feb 21 11:29:14 2016
24
+ # Plotname: AC Analysis
25
+ # Flags: complex
26
+ # No. Variables: 3
27
+ # No. Points: 41
28
+ # Variables:
29
+ # 0 frequency frequency grid=3
30
+ # 1 v(out) voltage
31
+ # 2 v(in) voltage
32
+ # Binary:
33
+ with open(fname, 'rb') as fp:
34
+ plot = {}
35
+ arrs = []
36
+ plots = []
37
+ names = dict()
38
+ ind = 0
39
+ while (True):
40
+ mdata = fp.readline(BSIZE_SP).split(b':', maxsplit=1)
41
+ if len(mdata) == 2:
42
+ if mdata[0].lower() in MDATA_LIST:
43
+ plot[mdata[0].lower()] = mdata[1].strip()
44
+ if mdata[0].lower() == b'variables':
45
+ nvars = int(plot[b'no. variables'])
46
+ npoints = int(plot[b'no. points'])
47
+ plot['varnames'] = []
48
+ plot['varunits'] = []
49
+ for varn in range(nvars):
50
+
51
+ varspec = (fp.readline(BSIZE_SP).strip()
52
+ .decode('ascii').split())
53
+ if varn != int(varspec[0]):
54
+ raise ValueError(f"Variable index mismatch in {fname}: expected {varn}, got {varspec[0]}")
55
+
56
+ #- Skip duplicated variables
57
+ if(varspec[1] not in names):
58
+ names[varspec[1]] = 1
59
+ else:
60
+ varspec[1] += str(ind)
61
+ ind +=1
62
+ plot['varnames'].append(varspec[1])
63
+ plot['varunits'].append(varspec[2])
64
+ if mdata[0].lower() == b'binary':
65
+ rowdtype = np.dtype({'names': plot['varnames'],
66
+ 'formats': [np.complex128 if b'complex'
67
+ in plot[b'flags']
68
+ else np.float64]*nvars})
69
+ arrs.append(np.fromfile(fp, dtype=rowdtype, count=npoints))
70
+ plots.append(plot)
71
+ fp.readline()
72
+ else:
73
+ break
74
+
75
+ return (arrs, plots)
76
+
77
+ def toDataFrames(ngarr):
78
+ (arrs,plots) = ngarr
79
+
80
+ dfs = list()
81
+ for i in range(0,len(plots)):
82
+ df = pd.DataFrame(data=arrs[i],columns=plots[i]['varnames'])
83
+ dfs.append(df)
84
+ return dfs
85
+
86
+ def toDataFrame(fraw):
87
+ (arrs,plots) = ngRawRead(fraw)
88
+
89
+ dfs = list()
90
+ for i in range(0,len(plots)):
91
+ df = pd.DataFrame(data=arrs[i],columns=plots[i]['varnames'])
92
+ dfs.append(df)
93
+
94
+ if(len(dfs)> 0):
95
+ return dfs[0]
96
+ else:
97
+ return None
98
+
99
+
100
+
101
+
102
+
103
+ if __name__ == '__main__':
104
+ arrs, plots = ngRawRead('test.raw')
105
+ print(arrs)
cicwave/pivot.py ADDED
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Pivot spec loader for cicwave.
5
+
6
+ A pivot spec (YAML or JSON) defines how to reshape a flat DataFrame
7
+ into a wide format suitable for waveform plotting.
8
+
9
+ Spec format::
10
+
11
+ index: Parameter # split by — each unique value becomes a wave
12
+ columns: Frequency # (optional) x-axis; rows with None/NaN dropped
13
+ values: Measurement # y-axis values
14
+ conditions: # further split waves by these; each unique combo
15
+ - Temp # of (index x conditions) becomes its own wave
16
+ - Config
17
+ aliases: # optional: short names for long condition values
18
+ Config:
19
+ c0: "LV"
20
+
21
+ Use ``--pivot-info`` to discover the cN keys for each condition column.
22
+ """
23
+
24
+ import json
25
+ import yaml
26
+ import pandas as pd
27
+
28
+
29
+ def load_spec(path):
30
+ with open(path) as f:
31
+ if path.endswith('.json'):
32
+ return json.load(f)
33
+ return yaml.safe_load(f)
34
+
35
+
36
+ def _is_json_kv_array(val):
37
+ if not isinstance(val, str):
38
+ return False
39
+ val = val.strip()
40
+ if not (val.startswith('[') and val.endswith(']')):
41
+ return False
42
+ try:
43
+ arr = json.loads(val)
44
+ return (isinstance(arr, list) and len(arr) > 0
45
+ and all(isinstance(d, dict) and 'value' in d for d in arr))
46
+ except (json.JSONDecodeError, TypeError):
47
+ return False
48
+
49
+
50
+ def _is_kv_semicolon(val):
51
+ """Detect 'KEY=VAL;KEY=VAL;...' patterns."""
52
+ if not isinstance(val, str):
53
+ return False
54
+ parts = [p for p in val.strip().split(';') if p]
55
+ return len(parts) >= 2 and all('=' in p for p in parts)
56
+
57
+
58
+ def _is_structured_value(val):
59
+ s = str(val)
60
+ return _is_json_kv_array(s) or _is_kv_semicolon(s)
61
+
62
+
63
+ def _shorten_value(val):
64
+ """Auto-shorten a condition value for column names."""
65
+ try:
66
+ if isinstance(val, float) and val == int(val):
67
+ return str(int(val))
68
+ except (ValueError, OverflowError):
69
+ pass
70
+ s = str(val)
71
+ if _is_json_kv_array(s):
72
+ arr = json.loads(s)
73
+ return '_'.join(str(d.get('value', '')) for d in arr)
74
+ if _is_kv_semicolon(s):
75
+ parts = [p.split('=', 1)[1] for p in s.strip().split(';') if p and '=' in p]
76
+ return '_'.join(parts)
77
+ return s
78
+
79
+
80
+ def _condition_prefix(name):
81
+ return name[0].upper()
82
+
83
+
84
+ def _build_alias_map(unique_vals, aliases_section):
85
+ """Map cN keys to user-defined aliases, falling back to auto-shorten."""
86
+ indexed = {("c%d" % i): v for i, v in enumerate(unique_vals)}
87
+ result = {}
88
+ for ckey, orig_val in indexed.items():
89
+ if aliases_section and ckey in aliases_section:
90
+ result[orig_val] = str(aliases_section[ckey])
91
+ else:
92
+ result[orig_val] = _shorten_value(orig_val)
93
+ return result
94
+
95
+
96
+ def pivot_info(df, spec):
97
+ """Return a human-readable summary of pivot dimensions and unique values."""
98
+ index_col = spec['index']
99
+ columns_col = spec.get('columns')
100
+ values_col = spec['values']
101
+ conditions = spec.get('conditions', [])
102
+
103
+ available = set(df.columns)
104
+ lines = []
105
+
106
+ lines.append("available columns: %s" % ', '.join(sorted(available)))
107
+ lines.append("")
108
+
109
+ if index_col in available:
110
+ lines.append("index: %s (%d unique)" % (index_col, df[index_col].nunique()))
111
+ for v in sorted(df[index_col].dropna().unique(), key=str):
112
+ lines.append(" %s" % v)
113
+ else:
114
+ lines.append("index: %s ** NOT FOUND **" % index_col)
115
+
116
+ if columns_col:
117
+ lines.append("")
118
+ if columns_col in available:
119
+ lines.append("columns: %s (%d unique)" % (
120
+ columns_col, df[columns_col].nunique()))
121
+ for v in sorted(df[columns_col].unique(), key=lambda x: str(x)):
122
+ lines.append(" %s" % v)
123
+ else:
124
+ lines.append("columns: %s ** NOT FOUND (will be skipped) **" % columns_col)
125
+
126
+ lines.append("")
127
+ if values_col in available:
128
+ lines.append("values: %s" % values_col)
129
+ else:
130
+ lines.append("values: %s ** NOT FOUND **" % values_col)
131
+
132
+ if conditions:
133
+ lines.append("")
134
+ lines.append("conditions:")
135
+ json_cols = []
136
+ for cond in conditions:
137
+ unique_vals = sorted(df[cond].dropna().unique(), key=str)
138
+ lines.append("")
139
+ lines.append(" %s (%d unique)" % (cond, len(unique_vals)))
140
+ has_json = any(_is_structured_value(v) for v in unique_vals)
141
+ for i, v in enumerate(unique_vals):
142
+ if has_json:
143
+ lines.append(" c%d: %s" % (i, _shorten_value(v)))
144
+ else:
145
+ lines.append(" %s" % _shorten_value(v))
146
+ if has_json:
147
+ json_cols.append(cond)
148
+
149
+ if json_cols:
150
+ lines.append("")
151
+ lines.append("Suggested aliases for your pivot spec:")
152
+ lines.append("aliases:")
153
+ for cond in json_cols:
154
+ lines.append(" %s:" % cond)
155
+ unique_vals = sorted(df[cond].dropna().unique(), key=str)
156
+ for i, v in enumerate(unique_vals):
157
+ short = _shorten_value(v)
158
+ lines.append(' c%d: "%s"' % (i, short))
159
+
160
+ return "\n".join(lines)
161
+
162
+
163
+ def apply_pivot(df, spec):
164
+ """Reshape *df* according to *spec*, return a wide DataFrame.
165
+
166
+ ``index`` values become wave names (one column per unique value).
167
+ ``columns`` (if present) becomes the x-axis — rows with None/NaN
168
+ in that column are dropped. ``conditions`` further split waves so
169
+ each unique (index × conditions) combo is its own wave.
170
+ """
171
+ index_col = spec['index']
172
+ columns_col = spec.get('columns')
173
+ values_col = spec['values']
174
+ conditions = spec.get('conditions', [])
175
+ aliases = spec.get('aliases', {})
176
+
177
+ available = set(df.columns)
178
+ missing = []
179
+ if index_col not in available:
180
+ missing.append("index '%s'" % index_col)
181
+ if values_col not in available:
182
+ missing.append("values '%s'" % values_col)
183
+ for cond in conditions:
184
+ if cond not in available:
185
+ missing.append("condition '%s'" % cond)
186
+ if missing:
187
+ raise KeyError(
188
+ "Pivot spec references missing columns: %s\n"
189
+ "Available columns: %s" % (
190
+ ', '.join(missing), ', '.join(sorted(available))))
191
+
192
+ has_xaxis = columns_col and columns_col in available
193
+
194
+ alias_maps = {}
195
+ for cond in conditions:
196
+ unique_vals = sorted(df[cond].dropna().unique(), key=str)
197
+ alias_maps[cond] = _build_alias_map(unique_vals, aliases.get(cond))
198
+
199
+ keep = [index_col, values_col] + conditions
200
+ if has_xaxis:
201
+ keep.insert(1, columns_col)
202
+ sub = df[keep].copy()
203
+
204
+ if has_xaxis:
205
+ sub = sub.dropna(subset=[columns_col])
206
+
207
+ wave_col = sub[index_col].astype(str)
208
+ for cond in conditions:
209
+ amap = alias_maps.get(cond, {})
210
+ labels = sub[cond].map(
211
+ lambda v: amap.get(v, _shorten_value(v)))
212
+ wave_col = wave_col + '_' + _condition_prefix(cond) + labels.astype(str)
213
+ sub['_wave'] = wave_col
214
+
215
+ if has_xaxis:
216
+ result = sub.pivot_table(
217
+ index=columns_col,
218
+ columns='_wave',
219
+ values=values_col,
220
+ aggfunc='mean',
221
+ )
222
+ result.columns.name = None
223
+ result = result.reset_index()
224
+ # Try to make x-axis numeric
225
+ try:
226
+ result[columns_col] = pd.to_numeric(result[columns_col])
227
+ result = result.sort_values(columns_col)
228
+ except (ValueError, TypeError):
229
+ pass
230
+ else:
231
+ result = sub.pivot_table(
232
+ index='_wave',
233
+ columns=index_col,
234
+ values=values_col,
235
+ aggfunc='mean',
236
+ )
237
+ result.columns.name = None
238
+ result = result.reset_index().rename(columns={'_wave': 'condition'})
239
+
240
+ return result