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
engforge/dataframe.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Dataframe Module:
|
|
2
|
+
|
|
3
|
+
Store data in dataframes and provide a simple interface to manipulate it.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
import attr
|
|
8
|
+
|
|
9
|
+
from engforge.common import inst_vectorize, chunks
|
|
10
|
+
from engforge.properties import engforge_prop
|
|
11
|
+
|
|
12
|
+
# from engforge.configuration import Configuration, forge
|
|
13
|
+
from engforge.logging import LoggingMixin
|
|
14
|
+
from engforge.typing import *
|
|
15
|
+
from engforge.properties import *
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
import numpy
|
|
19
|
+
import pandas
|
|
20
|
+
import os
|
|
21
|
+
import collections
|
|
22
|
+
import uuid
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class DataFrameLog(LoggingMixin):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
log = DataFrameLog()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Dataframe interrogation functions
|
|
33
|
+
def is_uniform(s: pandas.Series):
|
|
34
|
+
a = s.to_numpy() # s.values (pandas<0.24)
|
|
35
|
+
|
|
36
|
+
if numpy.all(a == a[0]):
|
|
37
|
+
return True
|
|
38
|
+
try:
|
|
39
|
+
#
|
|
40
|
+
if not numpy.isfinite(a).any():
|
|
41
|
+
return True
|
|
42
|
+
except:
|
|
43
|
+
pass
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# key_func = lambda kv: len(kv[0].split('.'))*len(kv[1])
|
|
48
|
+
# length of matches / length of key
|
|
49
|
+
key_func = lambda kv: len(kv[1]) / len(kv[0].split("."))
|
|
50
|
+
# key_func = lambda kv: len(kv[1])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# TODO: remove duplicate columns
|
|
54
|
+
# mtches = collections.defaultdict(set)
|
|
55
|
+
# dfv = ecs.dataframe_variants[0]
|
|
56
|
+
# for v1,v2 in itertools.combinations(dfv.columns,2):
|
|
57
|
+
# if numpy.all(dfv[v1]==dfv[v2]):
|
|
58
|
+
#
|
|
59
|
+
# mtches[v1].add(v2)
|
|
60
|
+
# mtches[v2].add(v1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# TODO: integrate statistical output of dataframe, if at all in problem domain
|
|
64
|
+
# 1. stats_mode: mean, median,min,max mode, std, var, skew, kurtosis
|
|
65
|
+
# 2. min_mode: mean,median,std,min,max
|
|
66
|
+
# 3. sub_mode: store the dataframe completely separately
|
|
67
|
+
class dataframe_property(engforge_prop):
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# aliases
|
|
72
|
+
dataframe_prop = dataframe_property
|
|
73
|
+
df_prop = dataframe_property
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def determine_split(raw, top: int = 1, key_f=key_func):
|
|
77
|
+
parents = {}
|
|
78
|
+
|
|
79
|
+
for rw in raw:
|
|
80
|
+
grp = rw.split(".")
|
|
81
|
+
for i in range(len(grp)):
|
|
82
|
+
tkn = ".".join(grp[0 : i + 1])
|
|
83
|
+
parents[tkn] = set()
|
|
84
|
+
|
|
85
|
+
for rw in raw:
|
|
86
|
+
for par in parents:
|
|
87
|
+
if rw.startswith(par):
|
|
88
|
+
parents[par].add(rw)
|
|
89
|
+
|
|
90
|
+
grps = sorted(parents.items(), key=key_f, reverse=True)[:top]
|
|
91
|
+
return [g[0] for g in grps]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def split_dataframe(df: pandas.DataFrame) -> tuple:
|
|
95
|
+
"""split dataframe into a dictionary of invariants and a dataframe of variable values
|
|
96
|
+
|
|
97
|
+
:returns tuple: constants,dataframe
|
|
98
|
+
"""
|
|
99
|
+
uniform = {}
|
|
100
|
+
for s in df:
|
|
101
|
+
c = df[s]
|
|
102
|
+
if is_uniform(c):
|
|
103
|
+
uniform[s] = c.iloc[0]
|
|
104
|
+
|
|
105
|
+
df_unique = df.copy().drop(columns=list(uniform))
|
|
106
|
+
return uniform, df_unique if len(df_unique) > 0 else df_unique
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class DataframeMixin:
|
|
110
|
+
dataframe: pandas.DataFrame
|
|
111
|
+
|
|
112
|
+
_split_dataframe_func = staticmethod(split_dataframe)
|
|
113
|
+
_determine_split_func = staticmethod(determine_split)
|
|
114
|
+
|
|
115
|
+
def smart_split_dataframe(self, df=None, split_groups=0, key_f=key_func):
|
|
116
|
+
"""splits dataframe between constant values and variants"""
|
|
117
|
+
if df is None:
|
|
118
|
+
df = self.dataframe
|
|
119
|
+
out = {}
|
|
120
|
+
const, vardf = split_dataframe(df)
|
|
121
|
+
out["constants"] = const
|
|
122
|
+
columns = set(vardf.columns)
|
|
123
|
+
split_groups = min(split_groups, len(columns) - 1)
|
|
124
|
+
if split_groups == 0:
|
|
125
|
+
out["variants"] = vardf
|
|
126
|
+
else:
|
|
127
|
+
nconst = {}
|
|
128
|
+
cgrp = determine_split(const, min(split_groups, len(const) - 1))
|
|
129
|
+
for i, grp in enumerate(sorted(cgrp, reverse=True)):
|
|
130
|
+
columns = set(const)
|
|
131
|
+
bad_columns = [c for c in columns if not c.startswith(grp)]
|
|
132
|
+
good_columns = [c for c in columns if c.startswith(grp)]
|
|
133
|
+
nconst[grp] = {c: const[c] for c in good_columns}
|
|
134
|
+
for c in good_columns:
|
|
135
|
+
if c in columns:
|
|
136
|
+
columns.remove(c)
|
|
137
|
+
out["constants"] = nconst
|
|
138
|
+
|
|
139
|
+
raw = sorted(set(df.columns))
|
|
140
|
+
grps = determine_split(raw, split_groups, key_f=key_f)
|
|
141
|
+
|
|
142
|
+
for i, grp in enumerate(sorted(grps, reverse=True)):
|
|
143
|
+
columns = set(vardf.columns)
|
|
144
|
+
bad_columns = [c for c in columns if not c.startswith(grp)]
|
|
145
|
+
good_columns = [c for c in columns if c.startswith(grp)]
|
|
146
|
+
out[grp] = vardf.copy().drop(columns=bad_columns)
|
|
147
|
+
# remove columns from vardf
|
|
148
|
+
vardf = vardf.drop(columns=good_columns)
|
|
149
|
+
if vardf.size > 0:
|
|
150
|
+
out["misc"] = vardf
|
|
151
|
+
return out
|
|
152
|
+
|
|
153
|
+
@solver_cached
|
|
154
|
+
def _split_dataframe(self):
|
|
155
|
+
"""splits dataframe between constant values and variants"""
|
|
156
|
+
return split_dataframe(self.dataframe)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def dataframe_constants(self):
|
|
160
|
+
return self._split_dataframe[0]
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def dataframe_variants(self):
|
|
164
|
+
o = self._split_dataframe[1:]
|
|
165
|
+
if isinstance(o, (list, tuple)):
|
|
166
|
+
if len(o) == 1:
|
|
167
|
+
return o[0]
|
|
168
|
+
return o
|
|
169
|
+
|
|
170
|
+
def format_columns(self, dataframe: pandas.DataFrame):
|
|
171
|
+
# replace(".", "_")
|
|
172
|
+
dataframe.rename(lambda x: x.lower(), axis="columns", inplace=True)
|
|
173
|
+
|
|
174
|
+
# Plotting Interface
|
|
175
|
+
@property
|
|
176
|
+
def skip_plot_vars(self) -> list:
|
|
177
|
+
"""accesses '_skip_plot_vars' if it exists, otherwise returns empty list"""
|
|
178
|
+
if hasattr(self, "_skip_plot_vars"):
|
|
179
|
+
return [var.lower() for var in self._skip_plot_vars]
|
|
180
|
+
return []
|
|
181
|
+
|
|
182
|
+
@property
|
|
183
|
+
def dataframe(self):
|
|
184
|
+
"""returns the dataframe"""
|
|
185
|
+
raise NotImplementedError("must be implemented in subclass")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Auto-install datastores dependencies from pyproject.toml
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import importlib.util
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_project_root():
|
|
9
|
+
"""Find the project root containing pyproject.toml"""
|
|
10
|
+
current = Path(__file__).parent
|
|
11
|
+
while current.parent != current:
|
|
12
|
+
if (current / "pyproject.toml").exists():
|
|
13
|
+
return current
|
|
14
|
+
current = current.parent
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def install_optional_dependencies(group_name="database"):
|
|
19
|
+
"""Install optional dependencies for a specific group"""
|
|
20
|
+
project_root = get_project_root()
|
|
21
|
+
if not project_root:
|
|
22
|
+
print("Could not find pyproject.toml. Manual installation required:")
|
|
23
|
+
print(f"pip install engforge[{group_name}]")
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# Try to import tomllib (Python 3.11+) or tomli (fallback)
|
|
28
|
+
try:
|
|
29
|
+
import tomllib
|
|
30
|
+
except ImportError:
|
|
31
|
+
import tomli as tomllib
|
|
32
|
+
|
|
33
|
+
# Read pyproject.toml
|
|
34
|
+
with open(project_root / "pyproject.toml", "rb") as f:
|
|
35
|
+
config = tomllib.load(f)
|
|
36
|
+
|
|
37
|
+
optional_deps = config.get("project", {}).get("optional-dependencies", {})
|
|
38
|
+
if group_name not in optional_deps:
|
|
39
|
+
print(f"No optional dependency group '{group_name}' found")
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
# Install the optional dependencies
|
|
43
|
+
package_name = config.get("project", {}).get("name", "engforge")
|
|
44
|
+
install_spec = f"{package_name}[{group_name}]"
|
|
45
|
+
|
|
46
|
+
print(f"Installing {install_spec}...")
|
|
47
|
+
result = subprocess.run(
|
|
48
|
+
[sys.executable, "-m", "pip", "install", install_spec],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if result.returncode == 0:
|
|
54
|
+
print(f"✅ Successfully installed {install_spec}")
|
|
55
|
+
return True
|
|
56
|
+
else:
|
|
57
|
+
print(f"❌ Failed to install {install_spec}")
|
|
58
|
+
print(f"Error: {result.stderr}")
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Error during auto-installation: {e}")
|
|
63
|
+
print(f"Please install manually: pip install engforge[{group_name}]")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_and_install_datastores():
|
|
68
|
+
"""Check if datastores dependencies are available, auto-install if needed"""
|
|
69
|
+
try:
|
|
70
|
+
import engforge.datastores.data
|
|
71
|
+
|
|
72
|
+
return True
|
|
73
|
+
except ImportError as e:
|
|
74
|
+
print(f"Datastores dependencies not available: {e}")
|
|
75
|
+
print("")
|
|
76
|
+
|
|
77
|
+
# Check if we're in development mode (editable install) and interactive
|
|
78
|
+
project_root = get_project_root()
|
|
79
|
+
is_interactive = hasattr(sys, "ps1") or sys.stdout.isatty()
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
project_root
|
|
83
|
+
and (project_root / "pyproject.toml").exists()
|
|
84
|
+
and is_interactive
|
|
85
|
+
):
|
|
86
|
+
try:
|
|
87
|
+
answer = (
|
|
88
|
+
input("Auto-install database dependencies? (y/N): ").strip().lower()
|
|
89
|
+
)
|
|
90
|
+
if answer in ["y", "yes"]:
|
|
91
|
+
if install_optional_dependencies("database"):
|
|
92
|
+
print(
|
|
93
|
+
"Please restart your Python session to use the newly installed dependencies."
|
|
94
|
+
)
|
|
95
|
+
return False
|
|
96
|
+
else:
|
|
97
|
+
print("Auto-installation failed. Install manually with:")
|
|
98
|
+
print("pip install engforge[database]")
|
|
99
|
+
return False
|
|
100
|
+
else:
|
|
101
|
+
print("Install database dependencies with:")
|
|
102
|
+
print("pip install engforge[database]")
|
|
103
|
+
return False
|
|
104
|
+
except (EOFError, KeyboardInterrupt):
|
|
105
|
+
print("Install database dependencies with:")
|
|
106
|
+
print("pip install engforge[database]")
|
|
107
|
+
return False
|
|
108
|
+
else:
|
|
109
|
+
print("Install database dependencies with:")
|
|
110
|
+
print("pip install engforge[database]")
|
|
111
|
+
return False
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Auto-check and install on import
|
|
115
|
+
if not check_and_install_datastores():
|
|
116
|
+
pass # Dependencies not available, but user has been informed
|