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.
- engforge/__init__.py +26 -0
- engforge/_testing_components.py +864 -0
- engforge/analysis.py +257 -0
- engforge/attr_dynamics.py +222 -0
- engforge/attr_plotting.py +811 -0
- engforge/attr_signals.py +116 -0
- engforge/attr_slots.py +211 -0
- engforge/attr_solver.py +443 -0
- engforge/attributes.py +326 -0
- engforge/common.py +76 -0
- engforge/component_collections.py +276 -0
- engforge/components.py +55 -0
- engforge/configuration.py +673 -0
- engforge/dataframe.py +185 -0
- engforge/datastores/__init__.py +116 -0
- engforge/datastores/data.py +519 -0
- engforge/datastores/gdocs.py +2650 -0
- engforge/datastores/reporting.py +935 -0
- engforge/datastores/secrets.py +549 -0
- engforge/dynamics.py +662 -0
- engforge/eng/__init__.py +0 -0
- engforge/eng/costs.py +1459 -0
- engforge/eng/fluid_material.py +279 -0
- engforge/eng/geometry.py +1060 -0
- engforge/eng/pipes.py +1211 -0
- engforge/eng/prediction.py +366 -0
- engforge/eng/solid_materials.py +445 -0
- engforge/eng/structure.py +2228 -0
- engforge/eng/structure_beams.py +821 -0
- engforge/eng/thermodynamics.py +271 -0
- engforge/engforge_attributes.py +380 -0
- engforge/env_var.py +214 -0
- engforge/locations.py +18 -0
- engforge/logging.py +290 -0
- engforge/patterns.py +449 -0
- engforge/problem_context.py +2251 -0
- engforge/properties.py +501 -0
- engforge/reporting.py +595 -0
- engforge/solveable.py +964 -0
- engforge/solver.py +316 -0
- engforge/solver_utils.py +726 -0
- engforge/system.py +143 -0
- engforge/system_reference.py +299 -0
- engforge/tabulation.py +288 -0
- engforge/test/__init__.py +2 -0
- engforge/test/_pre_test_structures.py +221 -0
- engforge/test/report_testing.py +158 -0
- engforge/test/test_airfilter.py +91 -0
- engforge/test/test_analysis.py +3 -0
- engforge/test/test_comp_iter.py +196 -0
- engforge/test/test_composition.py +139 -0
- engforge/test/test_costs.py +338 -0
- engforge/test/test_dynamics.py +109 -0
- engforge/test/test_dynamics_spaces.py +39 -0
- engforge/test/test_four_bar.py +97 -0
- engforge/test/test_modules.py +72 -0
- engforge/test/test_performance.py +93 -0
- engforge/test/test_pipes.py +49 -0
- engforge/test/test_problem.py +411 -0
- engforge/test/test_problem_deepscoping.py +68 -0
- engforge/test/test_slider_crank.py +51 -0
- engforge/test/test_solver.py +343 -0
- engforge/test/test_tabulation.py +233 -0
- engforge/typing.py +92 -0
- engforge-0.3.0.dist-info/METADATA +392 -0
- engforge-0.3.0.dist-info/RECORD +69 -0
- engforge-0.3.0.dist-info/WHEEL +5 -0
- engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
- 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
|