engforge 0.3.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.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,811 @@
1
+ """This module defines Plot and Trace methods that allow the plotting of Statistical & Transient relationships of data in each system"""
2
+
3
+ import attrs
4
+ import uuid
5
+ import numpy as np
6
+ import seaborn as sns
7
+ import matplotlib.pylab as pylab
8
+ from matplotlib.font_manager import get_font_names
9
+ import inspect
10
+
11
+ import typing
12
+
13
+ # from engforge.configuration import forge
14
+ from engforge.properties import *
15
+ from engforge.logging import LoggingMixin
16
+ from engforge.env_var import EnvVariable
17
+ from engforge.attributes import ATTR_BASE, AttributeInstance
18
+ from matplotlib.backends.backend_pdf import PdfPages
19
+
20
+
21
+ class PlotLog(LoggingMixin):
22
+ pass
23
+
24
+
25
+ log = PlotLog()
26
+
27
+ # Seaborn Config Options
28
+ SEABORN_CONTEXTS = ["paper", "talk", "poster", "notebook"]
29
+ SEABORN_THEMES = ["darkgrid", "whitegrid", "dark", "white", "ticks"]
30
+
31
+
32
+ def conv_ctx(ctx):
33
+ if ctx.lower() not in SEABORN_CONTEXTS:
34
+ raise ValueError(f"theme must be one of {SEABORN_CONTEXTS}")
35
+ return ctx.lower()
36
+
37
+
38
+ def conv_theme(theme):
39
+ if theme.lower() not in SEABORN_THEMES:
40
+ raise ValueError(f"theme must be one of {SEABORN_THEMES}")
41
+ return theme.lower()
42
+
43
+
44
+ # Color Maps
45
+ SEABORN_COLORMAPS = ["deep", "musted", "bright", "pastel", "dark", "colorblind"]
46
+ SEABORN_COLORMAPS += list(pylab.colormaps.keys())
47
+ SEABORN_COLORMAPS += ["husl", "hls"]
48
+ # TODO: handle other seaboorn options
49
+
50
+
51
+ def conv_maps(map):
52
+ if map.lower() not in SEABORN_COLORMAPS:
53
+ raise ValueError(f"theme must be one of {SEABORN_COLORMAPS}")
54
+ return str(map.lower())
55
+
56
+
57
+ # SEABORN_FONTS = get_font_names()
58
+ # FONT_INX_LC = {k.lower():k for k in SEABORN_FONTS}
59
+ # def conv_font(fnt):
60
+ # if fnt.lower() not in FONT_INX_LC:
61
+ # raise ValueError(f'theme must be one of {SEABORN_FONTS}')
62
+ # act = FONT_INX_LC[str(fnt).lower()]
63
+ # return act
64
+
65
+
66
+ # Seaborn Config Via Env Var
67
+ _def_opts = {"obscure": False, "dontovrride": True}
68
+ SEABORN_CONTEXT = EnvVariable(
69
+ "SEABORN_CONTEXT",
70
+ default="paper",
71
+ type_conv=conv_ctx,
72
+ desc=f"choose one of: {SEABORN_CONTEXTS}",
73
+ **_def_opts,
74
+ )
75
+ SEABORN_THEME = EnvVariable(
76
+ "SEABORN_THEME",
77
+ default="darkgrid",
78
+ type_conv=conv_theme,
79
+ desc=f"choose one of: {SEABORN_THEMES}",
80
+ **_def_opts,
81
+ )
82
+ SEABORN_PALETTE = EnvVariable(
83
+ "SEABORN_PALETTE",
84
+ default="deep",
85
+ type_conv=conv_maps,
86
+ desc=f"choose one of: {SEABORN_COLORMAPS}",
87
+ **_def_opts,
88
+ )
89
+
90
+ # Figure Saving Config
91
+ # FIGURE_SAVE = EnvVariable(
92
+ # ""
93
+ # )
94
+
95
+ # TODO: fonts are platform dependent :(
96
+ # SEABORN_FONT = EnvVariable('SEABORN_FONT',default='sans-serif',**_def_opts)
97
+
98
+ # Staistical View Through Seaborn:
99
+ # takes a dataframe and displays it via several types of statisical views, the default is `relplot.scatterplot` to show first order trends.
100
+ PLOT_KINDS = {
101
+ "displot": ("histplot", "kdeplot", "ecdfplot", "rugplot"),
102
+ "relplot": ("scatterplot", "lineplot"),
103
+ "catplot": (
104
+ "stripplot",
105
+ "swarmplot",
106
+ "boxplot",
107
+ "violinplot",
108
+ "pointplot",
109
+ "barplot",
110
+ ),
111
+ }
112
+
113
+
114
+ # Install Seaborn Themes
115
+ def install_seaborn(rc_override=None, **kwargs):
116
+ default = dict(
117
+ context=SEABORN_CONTEXT.secret,
118
+ style=SEABORN_THEME.secret,
119
+ palette=SEABORN_PALETTE.secret,
120
+ )
121
+ default.update(**kwargs)
122
+
123
+ if rc_override:
124
+ sns.set_theme(**default, rc=rc_override)
125
+ else:
126
+ sns.set_theme(**default)
127
+
128
+
129
+ install_seaborn()
130
+
131
+
132
+ def save_all_figures_to_pdf(
133
+ filename, figs=None, dpi=200, close=True, pdf=None, return_pdf=False
134
+ ):
135
+ """
136
+ Save all figures to a PDF file.
137
+
138
+ :param filename: The name of the PDF file to save.
139
+ :param figs: List of figures to save. If None, all open figures will be saved.
140
+ :param dpi: The resolution of the saved figures in dots per inch.
141
+ :param close: Whether to close all figures after saving.
142
+ :param pdf: An existing PdfPages object to append the figures to. If None, a new PdfPages object will be created.
143
+ :param return_pdf: Whether to return the PdfPages object after saving.
144
+ :return: The PdfPages object if return_pdf is True, else None.
145
+ :rtype: PdfPages or None
146
+ """
147
+ if pdf is None:
148
+ pp = PdfPages(filename)
149
+ else:
150
+ pp = pdf
151
+
152
+ if figs is None:
153
+ figs = [pylab.figure(n) for n in pylab.get_fignums()]
154
+
155
+ for fig in figs:
156
+ fig.savefig(pp, format="pdf", dpi=dpi)
157
+
158
+ if close:
159
+ pylab.close("all")
160
+ if return_pdf:
161
+ return pp
162
+ else:
163
+ pp.close() # Don't keep the PDF file open
164
+
165
+
166
+ class PlottingMixin:
167
+ """Inherited by Systems and Analyses to provide common interface for plotting"""
168
+
169
+ _stored_plots: dict
170
+
171
+ @instance_cached
172
+ def plots(self):
173
+ return {k: getattr(self, k) for k in self.plot_attributes()}
174
+
175
+ @property
176
+ def stored_plots(self):
177
+ if not hasattr(self, "_stored_plots"):
178
+ self._stored_plots = {}
179
+ return self._stored_plots
180
+
181
+ @instance_cached
182
+ def traces(self):
183
+ if not self.transients_attributes():
184
+ return {} # not a transient system
185
+ return {k: getattr(self, k) for k in self.trace_attributes()}
186
+
187
+ def make_plots(
188
+ self, analysis: "Analysis" = None, store_figures: bool = True, pre=None
189
+ ):
190
+ """makes plots and traces of all on this instance, and if a system is
191
+ subsystems. Analysis should call make plots however it can be called on a system as well
192
+ :param analysis: the analysis that has triggered this plot
193
+ :param store_figure: a boolean or dict, if neither a dictionary will be created and returend from this function
194
+ :returns: the dictionary from store_figures logic
195
+ """
196
+ if not pre:
197
+ pre = f"{self.classname}"
198
+ else:
199
+ pre = f"{pre}.{self.classname}"
200
+
201
+ if analysis and store_figures is True:
202
+ imgstore = analysis._stored_plots
203
+ elif store_figures == True:
204
+ imgstore = {} # gotta store somewhere
205
+ elif isinstance(store_figures, dict):
206
+ imgstore = store_figures
207
+ else:
208
+ imgstore = None
209
+
210
+ # Announce
211
+ log.info(f"Plotting {pre}")
212
+
213
+ # Traces
214
+ for plotnm, plot in self.plots.items():
215
+ try:
216
+ log.info(f"{self.identity} plotting {pre}.{plotnm} | {plot}")
217
+ fig, ax = plot()
218
+ if isinstance(fig, pylab.Figure):
219
+ pylab.close(fig)
220
+ if imgstore is not None:
221
+ imgstore[f"{pre}.{plotnm}"] = fig
222
+ except Exception as e:
223
+ log.error(e, f"issue in plot {plot}")
224
+
225
+ # Traces
226
+ for plotnm, plot in self.traces.items():
227
+ try:
228
+ log.info(f"{self.identity} tracing {pre}.{plotnm} | {plot}")
229
+ fig, ax = plot()
230
+ if isinstance(fig, pylab.Figure):
231
+ pylab.close(fig)
232
+ if imgstore is not None:
233
+ imgstore[f"{pre}.{plotnm}"] = fig
234
+ except Exception as e:
235
+ log.error(e, f"issue in trace {plot}")
236
+
237
+ # Sub Systems
238
+ for confnm, conf in self.internal_configurations().items():
239
+ if isinstance(conf, PlottingMixin):
240
+ log.info(f"{self.identity} system plotting {confnm} | {conf}")
241
+ conf.make_plots(analysis, store_figures=store_figures, pre=pre)
242
+
243
+ return imgstore
244
+
245
+
246
+ class PlotInstance(AttributeInstance):
247
+ """combine plotclass vars with system info"""
248
+
249
+ plot_cls: "Plot"
250
+ system: "System"
251
+
252
+ refs = None
253
+
254
+ def __init__(self, system: "System", plot_cls: "Plot"):
255
+ self.plot_cls = plot_cls
256
+ self.system = system
257
+
258
+ _sys_refs = self.system.system_references()
259
+ sys_refs = {k: v for atr, grp in _sys_refs.items() for k, v in grp.items()}
260
+
261
+ diff = set()
262
+ varss = set()
263
+ for k, vers in self.plot_cls.plot_vars().items():
264
+ if isinstance(vers, list):
265
+ for v in vers:
266
+ if v not in sys_refs:
267
+ diff.add(v)
268
+ else:
269
+ varss.add(v)
270
+ elif vers not in sys_refs:
271
+ diff.add(vers)
272
+ else:
273
+ varss.add(vers)
274
+
275
+ if self.system.log_level < 10:
276
+ log.debug(f"system references: {sys_refs}")
277
+ if diff:
278
+ log.warning(f"has diff {diff}| found: {varss}| possible: {sys_refs}")
279
+
280
+ if diff:
281
+ # raise KeyError(f"has system diff: {diff} found: {vars}| from: {sys_ref}")
282
+ log.warning(f"has system diff: {diff} found: {vars}| from: {sys_refs}")
283
+
284
+ self.refs = {k: sys_refs[k] for k in varss}
285
+
286
+ def plot(self, **kwargs):
287
+ """applies the system dataframe to the plot"""
288
+ return self(**kwargs)
289
+
290
+ def __call__(self, **override_kw):
291
+ """
292
+ method allowing a similar type.kind(**override_kw,**default) (ie. relplot.scatterplot(x=different var))
293
+ #TODO: override strategy
294
+ """
295
+ if not self.system.solved:
296
+ raise ValueError(f"not solved yet!")
297
+
298
+ PC = self.plot_cls
299
+ f = self.plot_cls.plot_func
300
+
301
+ # Defaults
302
+ args = PC.plot_vars()
303
+ if hasattr(PC, "kind"):
304
+ args["kind"] = kind = PC.kind.replace("plot", "")
305
+
306
+ # these go in plot
307
+ extra = PC.plot_extra()
308
+
309
+ # Parse title
310
+ title = self.plot_cls.title
311
+ if "title" in override_kw:
312
+ title = override_kw.pop("title")
313
+
314
+ # Announce Override
315
+ if override_kw:
316
+ log.debug(f"overriding vars {override_kw}")
317
+ args.update(**override_kw)
318
+
319
+ log.info(f"plotting {self.system.identity}| {self.identity} with {args}")
320
+ fig = ax = f(data=self.system.dataframe, **args, **extra)
321
+
322
+ return self.process_fig(fig, title)
323
+
324
+ def process_fig(self, fig, title):
325
+ if isinstance(fig, pylab.Axes):
326
+ ax = fig
327
+ fig = fig.fig
328
+ elif isinstance(fig, sns.FacetGrid):
329
+ ax = fig
330
+ fig = fig.fig
331
+ else:
332
+ ax = fig
333
+
334
+ # Polish Fig Args
335
+ if title:
336
+ fig.subplots_adjust(top=0.9)
337
+ fig.suptitle(title)
338
+
339
+ return fig, ax
340
+
341
+ @property
342
+ def identity(self) -> str:
343
+ return f"{self.plot_cls.type}.{self.plot_cls.kind}"
344
+
345
+
346
+ class PlotBase(ATTR_BASE):
347
+ """base class for plot attributes"""
348
+
349
+ name: str
350
+ config_cls: "System"
351
+ title: str = None
352
+ kind: str
353
+ cls_vars = {"x", "y"}
354
+ instance_class = PlotInstance
355
+
356
+ @classmethod
357
+ def plot_vars(cls) -> dict:
358
+ """gathers seaborn plot vars that will scope from system.dataframe"""
359
+ p = {}
360
+ p["x"] = cls.x
361
+ p["y"] = cls.y
362
+ if cls.hue:
363
+ p["hue"] = cls.hue
364
+ if cls.col:
365
+ p["col"] = cls.col
366
+ if cls.row:
367
+ p["row"] = cls.row
368
+
369
+ # Add the options
370
+ var_opts = cls.type_var_options[cls.type]
371
+ for k, arg in cls.plot_args.items():
372
+ if k in var_opts:
373
+ p[k] = arg
374
+ return p
375
+
376
+ @classmethod
377
+ def plot_extra(cls):
378
+ plot_vars = cls.plot_vars()
379
+ out = {}
380
+ for k, arg in cls.plot_args.items():
381
+ if k not in plot_vars:
382
+ out[k] = arg
383
+ return out
384
+
385
+ @classmethod
386
+ def validate_plot_args(cls, system: "System"):
387
+ """Checks system.system_references that cls.plot_vars exists"""
388
+ log.info(f"validating: {system}")
389
+ sys_ref = system.system_references()
390
+ attr_keys = set(sys_ref["attributes"].keys())
391
+ prop_keys = set(sys_ref["properties"].keys())
392
+ valid = attr_keys.union(prop_keys)
393
+
394
+ diff = set()
395
+ for k, vers in cls.plot_vars().items():
396
+ if isinstance(vers, list):
397
+ for v in vers:
398
+ if v not in valid:
399
+ diff.add(v)
400
+ elif vers not in valid:
401
+ diff.add(vers)
402
+
403
+ if log.log_level <= 10:
404
+ log.debug(f"{cls.__name__} has vars: {attr_keys} and bad input: {diff}")
405
+
406
+ if diff:
407
+ log.warning(f"bad plot vars: {diff} do not exist in system: {valid}")
408
+ # TODO: fix time being defined on components
409
+ # raise KeyError(
410
+ # f"bad plot vars: {diff} do not exist in system: {valid}"
411
+ # )
412
+
413
+ @classmethod
414
+ def handle_instance(cls, canidate):
415
+ """no interacion with system, reporing only"""
416
+ return None
417
+
418
+ @classmethod
419
+ def create_instance(cls, system: "System") -> PlotInstance:
420
+ cls.validate_plot_args(system)
421
+ return PlotInstance(system, cls)
422
+
423
+
424
+ trace_type = typing.Union[str, list]
425
+
426
+
427
+ class TraceInstance(PlotInstance):
428
+ @classmethod
429
+ def plot_extra(cls) -> dict:
430
+ return cls.plot_cls.extra_args
431
+
432
+ def __call__(self, **override_kw):
433
+ """
434
+ method allowing a similar type.kind(**override_kw,**default) (ie. relplot.scatterplot(x=different var))
435
+ #TODO: override strategy
436
+ """
437
+ if not self.system.solved:
438
+ raise ValueError(f"not solved yet!")
439
+
440
+ PC = self.plot_cls
441
+
442
+ type = PC.type
443
+ types = self.plot_cls.types
444
+ if "type" in override_kw and override_kw["type"] in types:
445
+ type = override_kw.pop("type")
446
+ elif "type" in override_kw:
447
+ raise KeyError(f"invalid trace type, must be in {types}")
448
+
449
+ if type == "scatter":
450
+ f = lambda ax, *args, **kwargs: ax.scatter(*args, **kwargs)
451
+ elif type == "line":
452
+ f = lambda ax, *args, **kwargs: ax.plot(*args, **kwargs)
453
+
454
+ # Defaults
455
+ args = PC.plot_args.copy()
456
+
457
+ # these go in plot
458
+ extra = PC.plot_extra()
459
+
460
+ # Parse title
461
+ title = PC.title
462
+ if "title" in override_kw:
463
+ title = override_kw.pop("title")
464
+
465
+ # Announce Override
466
+ if override_kw:
467
+ log.debug(f"overriding vars {override_kw}")
468
+ args.update(**override_kw)
469
+
470
+ log.info(f"plotting {self.system.identity}| {self.identity} with {args}")
471
+
472
+ # PLOTTING
473
+ # Make the axes and plot
474
+ # Get The MVPS
475
+ x = args.pop("x")
476
+ if "x" in override_kw:
477
+ x = override_kw["x"]
478
+ y = args.pop("y")
479
+ if "y" in override_kw:
480
+ y = override_kw["y"]
481
+ if not isinstance(y, list):
482
+ y = list(y)
483
+
484
+ # secondary plot
485
+ if "y2" in args and args["y2"]:
486
+ y2 = args.pop("y2")
487
+ if "y2" in override_kw:
488
+ y2 = override_kw["y2"]
489
+ if y2 is None:
490
+ y2 = []
491
+ elif not isinstance(y2, list):
492
+ y2 = list(y2)
493
+ else:
494
+ y2 = []
495
+
496
+ # TODO: insert marker, color ect per group, ensure no overlap
497
+ fig, axes = pylab.subplots(2 if y2 else 1, 1)
498
+ if y2:
499
+ ax, ax2 = axes[0], axes[1]
500
+ else:
501
+ ax = axes
502
+ ax2 = None
503
+
504
+ # Loop over y1
505
+ yleg = []
506
+ for y in y:
507
+ f(ax, x, y, data=self.system.dataframe, label=y, **args, **extra)
508
+ else:
509
+ # The only specificity of the code is when plotting the legend
510
+ h, l = np.hstack(
511
+ [
512
+ l.get_legend_handles_labels()
513
+ for l in ax.figure.axes
514
+ if l.bbox.bounds == ax.bbox.bounds
515
+ ]
516
+ ).tolist()
517
+ ax.legend(handles=h, labels=l, loc="upper right")
518
+
519
+ # Loop over y2
520
+ for y in y2:
521
+ f(ax2, x, y, data=self.system.dataframe, label=y, **args, **extra)
522
+ else:
523
+ # The only specificity of the code is when plotting the legend
524
+ h, l = np.hstack(
525
+ [
526
+ l.get_legend_handles_labels()
527
+ for l in ax2.figure.axes
528
+ if l.bbox.bounds == ax2.bbox.bounds
529
+ ]
530
+ ).tolist()
531
+ ax2.legend(handles=h, labels=l, loc="upper right")
532
+
533
+ return self.process_fig(fig, title)
534
+
535
+
536
+ class Trace(PlotBase):
537
+ """trace is a plot for transients, with y and y2 axes which can have multiple vars each"""
538
+
539
+ types = ["scatter", "line"]
540
+ type = "scatter"
541
+ instance_class = TraceInstance
542
+
543
+ # mainem
544
+ y2: trace_type
545
+ y: trace_type
546
+ x: str
547
+
548
+ plot_args: dict
549
+ extra_args: dict
550
+
551
+ # Extended vars per option
552
+ type_var_options = {
553
+ "scatter": ("size", "color"),
554
+ "line": ("color", "marker", "linestyle"),
555
+ }
556
+
557
+ type_options = {
558
+ "scatter": ("vmax", "vmin", "marker", "alpha", "cmap"),
559
+ "line": ("linewidth",),
560
+ }
561
+ all_options = (
562
+ "xlabel",
563
+ "ylabel",
564
+ "y2label",
565
+ "title",
566
+ "xticks",
567
+ "yticks",
568
+ "alpha",
569
+ )
570
+
571
+ always = ("x", "y", "y2")
572
+
573
+ @classmethod
574
+ def define(cls, x="time", y: trace_type = None, y2=None, kind="line", **kwargs):
575
+ """Defines a plot that will be matplotlib, with validation happening as much as possible in the define method
576
+
577
+ #Plot Choice
578
+ :param kind: specify the kind of type of plot scatter or line, with the default being line
579
+
580
+ # Dependents & Independents:
581
+ :param x: the x var for each plot, by default 'time'
582
+ :param y: the y var for each plot, required
583
+ :param y2: the y2 var for each plot, optional
584
+
585
+ # Additional Parameters:
586
+ :param title: this title will be applied to the figure.suptitle()
587
+ :param xlabel: the x label, by default the capitalized var
588
+ :param ylabel: the x label, by default the capitalized var
589
+ :param y2label: the x label, by default the capitalized var
590
+ """
591
+
592
+ if x is None or y is None:
593
+ raise ValueError(f"x and y must both be input")
594
+
595
+ if not isinstance(x, str):
596
+ raise ValueError(f"x must be string")
597
+
598
+ if not isinstance(y, (list, str)):
599
+ raise ValueError(f"y must be string or list")
600
+
601
+ if not any([y2 is None, isinstance(y2, (list, str))]):
602
+ raise ValueError(f"y2 must be string or list")
603
+
604
+ # Validate Plot
605
+ assert kind in cls.types, f"invalid kind not in {cls.types}"
606
+ if kind == "line":
607
+ pfunc = pylab.plot
608
+ elif kind == "scatter":
609
+ pfunc = pylab.scatter
610
+ else:
611
+ raise KeyError(f"bad plot kind: {kind}")
612
+
613
+ # Remove special args
614
+ title = None
615
+ if "title" in kwargs:
616
+ title = kwargs.pop("title")
617
+
618
+ # Remove special args
619
+ non_var_args = cls.valid_non_vars(kind)
620
+ var_args = cls.valid_vars_options(kind)
621
+ extra = {}
622
+
623
+ for k in list(kwargs.keys()):
624
+ if k in non_var_args:
625
+ extra[k] = kwargs.pop(k)
626
+
627
+ # Validate Args
628
+ assert set(kwargs).issubset(var_args), f"invalid plot args {kwargs}"
629
+ plot_args = kwargs
630
+ plot_args["x"] = x
631
+ plot_args["y"] = y
632
+
633
+ if "y2":
634
+ plot_args["y2"] = y2
635
+
636
+ log.info(f"adding Trace|{kind} {x},{y},{y2},{kwargs}")
637
+
638
+ # Create A New Signals Class
639
+ new_name = f"Trace_x_{x}_y_{y}".replace(".", "_").replace("-", "")
640
+ new_dict = dict(
641
+ name=new_name,
642
+ x=x,
643
+ y=y,
644
+ y2=y2,
645
+ plot_func=pfunc,
646
+ plot_args=plot_args,
647
+ extra_args=extra,
648
+ title=title,
649
+ kind=kind,
650
+ )
651
+ new_plot = cls._setup_cls(new_name, new_dict)
652
+ return new_plot
653
+
654
+ @classmethod
655
+ def valid_non_vars(cls, type) -> set:
656
+ s = set(cls.all_options)
657
+ s = s.union(set(cls.type_options[type]))
658
+ return s
659
+
660
+ @classmethod
661
+ def valid_vars_options(cls, type) -> set:
662
+ s = set(cls.always)
663
+ s = s.union(set(cls.type_var_options[type]))
664
+ return s
665
+
666
+ @classmethod
667
+ def plot_vars(cls) -> set:
668
+ pa = cls.plot_args.copy()
669
+
670
+ y1 = pa.pop("y")
671
+ c = set()
672
+ if isinstance(y1, list):
673
+ for y in y1:
674
+ c.add(y)
675
+ else:
676
+ c.add(y1)
677
+ pa["y"] = list(c)
678
+
679
+ if "y2" in c:
680
+ c = set()
681
+ y2 = pa.pop("y2")
682
+ if isinstance(y2, list):
683
+ for y in y2:
684
+ c.add(y)
685
+ else:
686
+ c.add(y2)
687
+
688
+ pa["y2"] = list(c)
689
+ return pa
690
+
691
+
692
+ class Plot(PlotBase):
693
+ """Plot is a conveinence method"""
694
+
695
+ types: tuple = ("displot", "relplot", "catplot")
696
+ std_fields: tuple = ("x", "y", "col", "hue", "row")
697
+
698
+ # These options must be filled out
699
+ type: str
700
+ kind: str
701
+ x: str
702
+ y: str
703
+
704
+ # optional, must be var
705
+ hue: str
706
+ col: str
707
+ row: str
708
+
709
+ # Extended vars per option
710
+ type_var_options = {
711
+ "displot": (),
712
+ "relplot": ("style", "shape"),
713
+ "catplot": (),
714
+ }
715
+
716
+ # These capture the function and extra keywords
717
+ plot_func: None
718
+ plot_args: dict
719
+
720
+ @classmethod
721
+ def define(
722
+ cls,
723
+ x,
724
+ y,
725
+ _type="relplot",
726
+ kind="scatterplot",
727
+ row=None,
728
+ col=None,
729
+ hue=None,
730
+ **kwargs,
731
+ ):
732
+ """Defines a plot that will be rendered in seaborn, with validation happening as much as possible in the define method
733
+
734
+ #Plot Choice
735
+ :param _type: the type of seaborn plot (relplot,displot,catplot)
736
+ :param kind: specify the kind of type of plot (ie. scatterplot of relplot)
737
+
738
+ # Dependents & Independents:
739
+ :param x: the x var for each plot
740
+ :param y: the y var for each plot
741
+
742
+ # Additional Parameters:
743
+ :param row: create a grid of data with row var
744
+ :param col: create a grid of data with column var
745
+ :param hue: provide an additional dimension of color based on this var
746
+ :param title: this title will be applied to the figure.suptitle()
747
+ """
748
+
749
+ # Validate Plot
750
+ assert _type in PLOT_KINDS, f"type {_type} must be in {PLOT_KINDS.keys()}"
751
+ kinds = PLOT_KINDS[_type]
752
+ assert kind in kinds, f"plot kind {kind} not in {kinds}"
753
+
754
+ # Remove special args
755
+ title = None
756
+ if "title" in kwargs:
757
+ title = kwargs.pop("title")
758
+
759
+ # Validate Args
760
+ pfunc = getattr(sns, _type)
761
+ kfunc = getattr(sns, kind)
762
+ args = set(inspect.signature(kfunc).parameters.keys())
763
+ assert set(kwargs).issubset(args), f"only {args} allowed for kw"
764
+ plot_args = kwargs
765
+
766
+ log.info(
767
+ f"adding PLOT|{_type}.{kind}({x},{y},hue={hue},c:[{col}],r:[{row}],{kwargs}"
768
+ )
769
+
770
+ # Create A New Signals Class
771
+ new_name = f"PLOT_x_{x}_y_{y}".replace(".", "_").replace("-", "")
772
+ new_dict = dict(
773
+ name=new_name,
774
+ x=x,
775
+ y=y,
776
+ hue=hue,
777
+ row=row,
778
+ col=col,
779
+ type=_type,
780
+ kind=kind,
781
+ plot_func=pfunc,
782
+ plot_args=plot_args,
783
+ title=title,
784
+ )
785
+ new_plot = cls._setup_cls(new_name, new_dict)
786
+ return new_plot
787
+
788
+ @classmethod
789
+ def plot_vars(cls) -> dict:
790
+ """gathers seaborn plot vars that will scope from system.dataframe"""
791
+ p = {}
792
+ p["x"] = cls.x
793
+ p["y"] = cls.y
794
+ if cls.hue:
795
+ p["hue"] = cls.hue
796
+ if cls.col:
797
+ p["col"] = cls.col
798
+ if cls.row:
799
+ p["row"] = cls.row
800
+
801
+ # Add the options
802
+ var_opts = cls.type_var_options[cls.type]
803
+ for k, arg in cls.plot_args.items():
804
+ if k in var_opts:
805
+ p[k] = arg
806
+ return p
807
+
808
+
809
+ ### Support Previous API
810
+ PLOT = Plot
811
+ Trace = Trace