powerconf 0.1.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.
- powerconf/__init__.py +0 -0
- powerconf/cli.py +51 -0
- powerconf/exceptions.py +8 -0
- powerconf/expressions.py +49 -0
- powerconf/graphs.py +17 -0
- powerconf/parsing.py +8 -0
- powerconf/readers.py +18 -0
- powerconf/rendering.py +327 -0
- powerconf/utils.py +24 -0
- powerconf-0.1.0.dist-info/METADATA +59 -0
- powerconf-0.1.0.dist-info/RECORD +13 -0
- powerconf-0.1.0.dist-info/WHEEL +4 -0
- powerconf-0.1.0.dist-info/entry_points.txt +3 -0
powerconf/__init__.py
ADDED
|
File without changes
|
powerconf/cli.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated, List
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
import typer
|
|
6
|
+
import yaml
|
|
7
|
+
from fspathtree import fspathtree
|
|
8
|
+
|
|
9
|
+
from . import rendering
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = rich.console.Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_config(config_file: Path):
|
|
16
|
+
"""load config from a file."""
|
|
17
|
+
configs = []
|
|
18
|
+
if not config_file.exists():
|
|
19
|
+
raise typer.Exit(f"File '{config_file}' not found.")
|
|
20
|
+
config_text = config_file.read_text()
|
|
21
|
+
config_docs = config_text.split("---")
|
|
22
|
+
for doc in config_docs:
|
|
23
|
+
config = fspathtree(yaml.safe_load(doc))
|
|
24
|
+
configs.append(config)
|
|
25
|
+
|
|
26
|
+
return configs
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command()
|
|
30
|
+
def render(config_file: Path, output: Path = None):
|
|
31
|
+
if output is None:
|
|
32
|
+
output = config_file.parent / (config_file.name + ".rendered")
|
|
33
|
+
configs = load_config(config_file)
|
|
34
|
+
config_renderer = rendering.ConfigRenderer()
|
|
35
|
+
rendered_configs = []
|
|
36
|
+
for config in configs:
|
|
37
|
+
config = config_renderer.expand_and_render(config)
|
|
38
|
+
rendered_configs += config
|
|
39
|
+
if len(rendered_configs) == 1:
|
|
40
|
+
output.write_text(yaml.dump(rendered_configs[0].tree))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def help():
|
|
45
|
+
print(
|
|
46
|
+
"""
|
|
47
|
+
The `powerconf` command is a CLI for the powerconf python module. It allows you to read
|
|
48
|
+
a configuration file, evaluate all expression, expand all batch nodes, etc, and write
|
|
49
|
+
the "rendered" configurations to a file(s).
|
|
50
|
+
"""
|
|
51
|
+
)
|
powerconf/exceptions.py
ADDED
powerconf/expressions.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from fspathtree import fspathtree
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExpressionEvaluator:
|
|
9
|
+
"""A class for evaluting expressions in a separate environement."""
|
|
10
|
+
|
|
11
|
+
def __init__(self):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
def start(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def eval(self, text: str):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
def stop(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExecExpressionEvaluator(ExpressionEvaluator):
|
|
25
|
+
"""A class for evaluating expressions using the builtin exec(). BE CAREFUL!"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
self.globals = {}
|
|
29
|
+
self.locals = {}
|
|
30
|
+
self.token = "_configurator_xyz"
|
|
31
|
+
|
|
32
|
+
import math
|
|
33
|
+
|
|
34
|
+
import numpy
|
|
35
|
+
|
|
36
|
+
self.add_global("numpy", numpy)
|
|
37
|
+
self.add_global("math", math)
|
|
38
|
+
|
|
39
|
+
def add_global(self, name, obj):
|
|
40
|
+
self.globals[name] = obj
|
|
41
|
+
|
|
42
|
+
def eval(self, text: str):
|
|
43
|
+
for forbidden_text in ["import", "open"]:
|
|
44
|
+
if forbidden_text in text.replace(" ", ""):
|
|
45
|
+
raise RuntimeError(
|
|
46
|
+
f"Expressions are not allowed to contain the text '{forbidden_text}'."
|
|
47
|
+
)
|
|
48
|
+
exec(f"{self.token}_result = " + text, self.globals, self.locals)
|
|
49
|
+
return self.locals[self.token + "_result"]
|
powerconf/graphs.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from fspathtree import fspathtree
|
|
2
|
+
import networkx as nx
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
DependencyGraph = nx.DiGraph
|
|
6
|
+
|
|
7
|
+
def get_dependency_chains(graph: DependencyGraph, node:fspathtree.PathType):
|
|
8
|
+
"""Given a node, return the chain of nodes that depend on it. If multiple paths exists, each is returned. If node does not have any predecessors, an empty list is returned."""
|
|
9
|
+
|
|
10
|
+
chains = []
|
|
11
|
+
for pred in graph.predecessors(node):
|
|
12
|
+
this_chain = []
|
|
13
|
+
this_chain.append(node)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
pass
|
powerconf/parsing.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from pyparsing import *
|
|
2
|
+
|
|
3
|
+
unbraced_variable = Combine(Literal("$") + Word(alphanums + "/_")("variable name"))
|
|
4
|
+
braced_variable = Combine(
|
|
5
|
+
Literal("$") + QuotedString(quote_char="{", end_quote_char="}")("variable name")
|
|
6
|
+
)
|
|
7
|
+
variable = unbraced_variable | braced_variable
|
|
8
|
+
expression = Combine(Literal("$") + original_text_for(nested_expr())("expression body"))
|
powerconf/readers.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
|
|
3
|
+
import yaml
|
|
4
|
+
from fspathtree import fspathtree
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load_yaml_docs(text_or_file: pathlib.Path | str):
|
|
8
|
+
"""Load all documents in YAML file."""
|
|
9
|
+
if type(text_or_file) == str:
|
|
10
|
+
text = text_or_file
|
|
11
|
+
else:
|
|
12
|
+
text = text_or_file.read_text()
|
|
13
|
+
|
|
14
|
+
configs = []
|
|
15
|
+
for doc in text.split("---"):
|
|
16
|
+
configs.append(fspathtree(yaml.safe_load(doc)))
|
|
17
|
+
|
|
18
|
+
return configs
|
powerconf/rendering.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import itertools
|
|
3
|
+
from os.path import normpath
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
import pint
|
|
7
|
+
from fspathtree import fspathtree
|
|
8
|
+
|
|
9
|
+
from . import expressions, graphs, parsing
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def expand_partial_configs(configs: List[fspathtree], include_base=False):
|
|
13
|
+
"""
|
|
14
|
+
Give a list of configuration trees, treat the frist tree
|
|
15
|
+
as a "base" configuration and all other trees as partial configurations
|
|
16
|
+
that should be merged with the base config to create instances.
|
|
17
|
+
|
|
18
|
+
@param include_base : if true, the base config will be included in the returned set.
|
|
19
|
+
"""
|
|
20
|
+
assert len(configs) > 0
|
|
21
|
+
|
|
22
|
+
if len(configs) == 1:
|
|
23
|
+
return configs
|
|
24
|
+
|
|
25
|
+
full_configs = []
|
|
26
|
+
if include_base:
|
|
27
|
+
full_configs.append(copy.deepcopy(configs[0]))
|
|
28
|
+
for c in configs[1:]:
|
|
29
|
+
full_configs.append(copy.deepcopy(configs[0]))
|
|
30
|
+
full_configs[-1].update(c)
|
|
31
|
+
|
|
32
|
+
return full_configs
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConfigRenderer:
|
|
36
|
+
def __init__(self, expression_evaluator=None):
|
|
37
|
+
if expression_evaluator is None:
|
|
38
|
+
expression_evaluator = expressions.ExecExpressionEvaluator()
|
|
39
|
+
|
|
40
|
+
self.expression_evaluator = expression_evaluator
|
|
41
|
+
self.ureg = pint.UnitRegistry()
|
|
42
|
+
self.Quantity = self.ureg.Quantity
|
|
43
|
+
|
|
44
|
+
def expand_and_render(self, config):
|
|
45
|
+
"""Expand batch configurations and render each instance."""
|
|
46
|
+
configs = self.expand_batch_nodes(config)
|
|
47
|
+
for i in range(len(configs)):
|
|
48
|
+
configs[i] = self.render(configs[i])
|
|
49
|
+
|
|
50
|
+
return configs
|
|
51
|
+
|
|
52
|
+
def expand_batch_nodes(self, config: fspathtree):
|
|
53
|
+
"""Expand @batch nodes in a configuration tree into multiple configuration trees."""
|
|
54
|
+
configs = []
|
|
55
|
+
|
|
56
|
+
batch_leaves = self._get_batch_leaves(config)
|
|
57
|
+
|
|
58
|
+
for vals in itertools.product(
|
|
59
|
+
*[config[leaf + "/@batch"] for leaf in batch_leaves.keys()]
|
|
60
|
+
):
|
|
61
|
+
instance = copy.deepcopy(config)
|
|
62
|
+
for i, leaf in enumerate(batch_leaves.keys()):
|
|
63
|
+
instance[leaf] = vals[i]
|
|
64
|
+
configs.append(instance)
|
|
65
|
+
|
|
66
|
+
return configs
|
|
67
|
+
|
|
68
|
+
def render(self, config: fspathtree, make_copy=True):
|
|
69
|
+
"""Render a configuration tree, constructing all quantities, expanding all variables, and evaluating all expressions."""
|
|
70
|
+
# create a graph of the dependencies
|
|
71
|
+
G = graphs.DependencyGraph()
|
|
72
|
+
# add all leaf node paths as nodes of the graph
|
|
73
|
+
G.add_nodes_from(config.get_all_leaf_node_paths())
|
|
74
|
+
# draw edges between nodes representing dependencies.
|
|
75
|
+
# edges point from a node to its dependency.
|
|
76
|
+
# if a node has incomming edges, other nodes depend on it
|
|
77
|
+
# if a node has outgoing edges, it depends on others.
|
|
78
|
+
for node in list(G.nodes):
|
|
79
|
+
variables = [
|
|
80
|
+
fspathtree.PathType(r["variable name"])
|
|
81
|
+
for r in parsing.variable.search_string(config[node])
|
|
82
|
+
]
|
|
83
|
+
for v in variables:
|
|
84
|
+
dep = fspathtree.PathType(
|
|
85
|
+
normpath(v if v.is_absolute() else node.parent / v)
|
|
86
|
+
)
|
|
87
|
+
G.add_edge(node, dep)
|
|
88
|
+
# detect circular dependencies
|
|
89
|
+
cycles = sorted(graphs.nx.simple_cycles(G))
|
|
90
|
+
if len(cycles) > 0:
|
|
91
|
+
msg = "Circular dependencies detected."
|
|
92
|
+
for cycle in cycles:
|
|
93
|
+
msg += "(" + " -> ".join(map(str, cycle)) + ")"
|
|
94
|
+
raise RuntimeError(msg)
|
|
95
|
+
|
|
96
|
+
# make a copy to work with
|
|
97
|
+
|
|
98
|
+
if make_copy:
|
|
99
|
+
rendered_config = copy.deepcopy(config)
|
|
100
|
+
else:
|
|
101
|
+
rendered_config = config
|
|
102
|
+
|
|
103
|
+
rendered_config = self._construct_all_quantities(rendered_config)
|
|
104
|
+
rendered_config = self._expand_all_variables(rendered_config)
|
|
105
|
+
rendered_config = self._evaluate_all_expressions(rendered_config, G)
|
|
106
|
+
|
|
107
|
+
return rendered_config
|
|
108
|
+
|
|
109
|
+
def _evaluate_all_expressions(
|
|
110
|
+
self, config: fspathtree, graph: graphs.DependencyGraph
|
|
111
|
+
):
|
|
112
|
+
"""Evaluate the expressions in a configuration tree, using a graph of the tree dependencies to determine the render order."""
|
|
113
|
+
# We need to determine which the order to evaluate evaluate the nodes of the config gree.
|
|
114
|
+
# We have a graph that describes the dependencies. each node is the graph is a leaf node
|
|
115
|
+
# in the tree, and the edges represent dependencies between nodes.
|
|
116
|
+
# Consider an example,
|
|
117
|
+
#
|
|
118
|
+
# A X
|
|
119
|
+
# / \ / \
|
|
120
|
+
# B C Y Z
|
|
121
|
+
# / \ /
|
|
122
|
+
# D E
|
|
123
|
+
#
|
|
124
|
+
# Assume all edgest are directed DOWN.
|
|
125
|
+
# B, D, E and Z then have no dependencies. All of their edges are "in" edges,
|
|
126
|
+
# they have no "out" edges.
|
|
127
|
+
#
|
|
128
|
+
# With networkx we can easily get a list of all nodes with no dependencies, first
|
|
129
|
+
# get the number of in and out edges for each node.
|
|
130
|
+
out_degrees = dict(graph.out_degree)
|
|
131
|
+
in_degrees = dict(graph.in_degree)
|
|
132
|
+
# any nodes that have zero out edges (out_degrees = 0) have no dependencies, and we can go ahead and render these.
|
|
133
|
+
config = self._evaluate_expressions(
|
|
134
|
+
config,
|
|
135
|
+
paths=map(
|
|
136
|
+
lambda item: item[0],
|
|
137
|
+
filter(lambda item: item[1] == 0, out_degrees.items()),
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
# Now we need to render nodes that have dependencies, but we need to determine the order that this can be
|
|
141
|
+
# done first. In the above example, we can evaluate A unit B and C are evaluated. We can evaluate C unilt D and E
|
|
142
|
+
# are evaluated.
|
|
143
|
+
#
|
|
144
|
+
# With networkx, we can easily get a list of all ancestors of a node. For node E, this would be [C,Y,A,X]
|
|
145
|
+
# Note: with the direction of our edges, an ancestor _depends_ on the node. So C is an ancestor of E, even
|
|
146
|
+
# though it seems more natural to consider E the ancestor of C. If we wanted E to be the ancestor of C,
|
|
147
|
+
# we would need to direct our edgest to point from dependencies to dependences.
|
|
148
|
+
|
|
149
|
+
# find all root dependencies. those nodes that don't depend on any others, but are depended on by others
|
|
150
|
+
# thse are all nodes with out_degrees == 0 and in_degrees > 0
|
|
151
|
+
root_dependencies = list(
|
|
152
|
+
filter(
|
|
153
|
+
lambda k: in_degrees[k] > 0,
|
|
154
|
+
filter(lambda k: out_degrees[k] == 0, out_degrees.keys()),
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# For each root node, get a list of all ancestors, and then find every path that connects the ancestor to the root.
|
|
159
|
+
# in this example above, node E would have 4 ancestors, [C,Y,A,X]. And the list of paths connecting E to its ancestors
|
|
160
|
+
# would be E -> C, E -> Y, E -> C -> A, and E -> Y -> X.
|
|
161
|
+
# However, we can't simply take one of these paths and start evaluating nodes. If we tried to evaluate E -> C -> A for example,
|
|
162
|
+
# we would get an error if B had not already been determined.
|
|
163
|
+
#
|
|
164
|
+
# What we really need to be able to do is find A and X, then work backward from there...
|
|
165
|
+
#
|
|
166
|
+
# Find all leaf node dependencies. These are nodes that only have dependencies, no dependents (out_degree > 0, in_nodes = 0)
|
|
167
|
+
leaf_dependencies = list(
|
|
168
|
+
filter(
|
|
169
|
+
lambda k: in_degrees[k] == 0,
|
|
170
|
+
filter(lambda k: out_degrees[k] > 0, out_degrees.keys()),
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
# now get a set of paths that connect each leaf node to its children
|
|
174
|
+
for leaf in leaf_dependencies:
|
|
175
|
+
all_paths = []
|
|
176
|
+
for d in graphs.nx.descendants(graph, leaf):
|
|
177
|
+
all_paths += graphs.nx.all_simple_paths(graph, leaf, d)
|
|
178
|
+
|
|
179
|
+
# node batches are sets of nodes that can all be rendered
|
|
180
|
+
# at the same time.
|
|
181
|
+
node_batches = [
|
|
182
|
+
set(filter(lambda n: n is not None, nodes))
|
|
183
|
+
for nodes in itertools.zip_longest(*all_paths)
|
|
184
|
+
]
|
|
185
|
+
node_batches.reverse()
|
|
186
|
+
for batch in node_batches[1:]:
|
|
187
|
+
config = self._evaluate_expressions(config, paths=batch)
|
|
188
|
+
|
|
189
|
+
return config
|
|
190
|
+
|
|
191
|
+
for root in root_dependencies:
|
|
192
|
+
all_chains = []
|
|
193
|
+
for a in graphs.nx.ancestors(graph, root):
|
|
194
|
+
all_chains += graphs.nx.all_simple_paths(graph, a, root)
|
|
195
|
+
nodes_to_evaluate = []
|
|
196
|
+
|
|
197
|
+
# get all paths in the graph that end on roots
|
|
198
|
+
all_chains = []
|
|
199
|
+
for root in root_dependencies:
|
|
200
|
+
for a in graphs.nx.ancestors(graph, root):
|
|
201
|
+
all_chains += graphs.nx.all_simple_paths(graph, a, root)
|
|
202
|
+
# prune paths that are included in ohters,
|
|
203
|
+
# i.e. we only want to keep paths from each root to it's
|
|
204
|
+
# oldest ancestors.
|
|
205
|
+
longest_chains = []
|
|
206
|
+
for p1 in all_chains:
|
|
207
|
+
save = True
|
|
208
|
+
for p2 in all_chains:
|
|
209
|
+
if p1 == p2:
|
|
210
|
+
continue
|
|
211
|
+
if p2[-len(p1) :] == p1:
|
|
212
|
+
save = False
|
|
213
|
+
break
|
|
214
|
+
if save:
|
|
215
|
+
longest_chains.append(p1)
|
|
216
|
+
|
|
217
|
+
# determine the order to evaluate dependencies.
|
|
218
|
+
|
|
219
|
+
for chain in longest_chains:
|
|
220
|
+
chain.reverse() # paths start with oldest ancestor and end with root node.
|
|
221
|
+
config = self._evaluate_expressions(config, paths=chain[1:])
|
|
222
|
+
|
|
223
|
+
return config
|
|
224
|
+
|
|
225
|
+
def _evaluate_expressions(self, config: fspathtree, paths: List[Any]):
|
|
226
|
+
"""Evaluate expression in the config at the paths listed. Uses as a utility function on _evaluate_all_expressions(...)"""
|
|
227
|
+
for path in paths:
|
|
228
|
+
if not self._contains_expression(config[path]):
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
self.expression_evaluator.globals["ctx"] = config[path.parent]
|
|
232
|
+
expressions = parsing.expression.search_string(config[path])
|
|
233
|
+
if (
|
|
234
|
+
len(expressions) == 1
|
|
235
|
+
and "$" + expressions[0]["expression body"] == config[path]
|
|
236
|
+
):
|
|
237
|
+
# the value of the element is a single expression with no surrounding text
|
|
238
|
+
# we want to replace the expression with the evaluation
|
|
239
|
+
e = expressions[0]
|
|
240
|
+
value = self.expression_evaluator.eval(e["expression body"][1:-1])
|
|
241
|
+
config[path] = value
|
|
242
|
+
else:
|
|
243
|
+
# we have more than one expression or the expression is surrounded by text
|
|
244
|
+
# we want to evaluate each expression and replace it with a str of its value
|
|
245
|
+
old_text = config[path]
|
|
246
|
+
new_text = ""
|
|
247
|
+
i = 0
|
|
248
|
+
for tokens, start, end in parsing.expression.scan_string(old_text):
|
|
249
|
+
new_text += old_text[i:start]
|
|
250
|
+
i = end
|
|
251
|
+
new_text += str(
|
|
252
|
+
self.expression_evaluator.eval(tokens["expression body"][1:-1])
|
|
253
|
+
)
|
|
254
|
+
new_text += old_text[i:]
|
|
255
|
+
config[path] = new_text
|
|
256
|
+
return config
|
|
257
|
+
|
|
258
|
+
def _construct_all_quantities(self, config: fspathtree):
|
|
259
|
+
"""Replace strings in the tree representing quantities with pint.Quantity objects."""
|
|
260
|
+
for path in config.get_all_leaf_node_paths():
|
|
261
|
+
if type(config[path]) == str:
|
|
262
|
+
if self._contains_expression(config[path]) or self._contains_variable(
|
|
263
|
+
config[path]
|
|
264
|
+
):
|
|
265
|
+
continue
|
|
266
|
+
try:
|
|
267
|
+
q = self.Quantity(config[path])
|
|
268
|
+
config[path] = q
|
|
269
|
+
except:
|
|
270
|
+
pass
|
|
271
|
+
|
|
272
|
+
return config
|
|
273
|
+
|
|
274
|
+
def _expand_all_variables(
|
|
275
|
+
self, config: fspathtree, template: str = "ctx['{name}']"
|
|
276
|
+
):
|
|
277
|
+
"""
|
|
278
|
+
Expand all shell-style variables into python variables in the entire tree.
|
|
279
|
+
"""
|
|
280
|
+
for path in config.get_all_leaf_node_paths():
|
|
281
|
+
if type(config[path]) == str:
|
|
282
|
+
config[path] = self._expand_variables(config[path])
|
|
283
|
+
|
|
284
|
+
return config
|
|
285
|
+
|
|
286
|
+
def _expand_variables(self, text: Any, template: str = "ctx['{name}']"):
|
|
287
|
+
"""
|
|
288
|
+
Expand shell-style variables into python variables
|
|
289
|
+
|
|
290
|
+
${x} -> c['x']
|
|
291
|
+
${/grid/x} -> c['/grid/x']
|
|
292
|
+
"""
|
|
293
|
+
i = 0
|
|
294
|
+
new_text = ""
|
|
295
|
+
for tokens, start, end in parsing.variable.scan_string(text):
|
|
296
|
+
new_text += text[i:start]
|
|
297
|
+
i = end
|
|
298
|
+
new_text += template.format(name=tokens["variable name"])
|
|
299
|
+
new_text += text[i:]
|
|
300
|
+
return new_text
|
|
301
|
+
|
|
302
|
+
def _contains_expression(self, text: Any):
|
|
303
|
+
if type(text) is not str:
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
results = parsing.expression.search_string(text)
|
|
307
|
+
return len(results) > 0
|
|
308
|
+
|
|
309
|
+
def _contains_variable(self, text: Any):
|
|
310
|
+
if type(text) is not str:
|
|
311
|
+
return False
|
|
312
|
+
|
|
313
|
+
results = parsing.variable.search_string(text)
|
|
314
|
+
return len(results) > 0
|
|
315
|
+
|
|
316
|
+
def _get_batch_leaves(self, config: fspathtree):
|
|
317
|
+
"""
|
|
318
|
+
Return a list of keys in a fpathtree (nested dict/list) that are marked
|
|
319
|
+
as batch.
|
|
320
|
+
"""
|
|
321
|
+
batch_leaves = dict()
|
|
322
|
+
for leaf in config.get_all_leaf_node_paths():
|
|
323
|
+
if leaf.parent.parts[-1] == "@batch":
|
|
324
|
+
batch_leaves[str(leaf.parent.parent)] = (
|
|
325
|
+
batch_leaves.get(str(leaf.parent.parent), 0) + 1
|
|
326
|
+
)
|
|
327
|
+
return batch_leaves
|
powerconf/utils.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from fspathtree import fspathtree
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_id(config: fspathtree, strip_keys=None):
|
|
8
|
+
"""Return a unique id for the given configuration object."""
|
|
9
|
+
if strip_keys is None:
|
|
10
|
+
strip_keys = []
|
|
11
|
+
# make a copy of the config with only keys not in the strip list
|
|
12
|
+
c = fspathtree()
|
|
13
|
+
|
|
14
|
+
def filt(path):
|
|
15
|
+
if str(path) in strip_keys:
|
|
16
|
+
return False
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
# we use a filter on the get_all_leaf_node_paths(...) here for more flexability
|
|
20
|
+
for p in config.get_all_leaf_node_paths(predicate=filt):
|
|
21
|
+
c[p] = str(config[p])
|
|
22
|
+
|
|
23
|
+
text = json.dumps(c.tree, sort_keys=True).replace(" ", "")
|
|
24
|
+
return hashlib.md5(text.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: powerconf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: CD Clark III
|
|
6
|
+
Author-email: clifton.clark@gmail.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Requires-Dist: fspathtree (>=0.9,<0.10)
|
|
13
|
+
Requires-Dist: networkx (>=3.3,<4.0)
|
|
14
|
+
Requires-Dist: numpy (>=2.0.0,<3.0.0)
|
|
15
|
+
Requires-Dist: pint (>=0.24,<0.25)
|
|
16
|
+
Requires-Dist: pyparsing (>=3.1.2,<4.0.0)
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# powerconf
|
|
20
|
+
|
|
21
|
+
Powerful configuration tools for numerical simulation.
|
|
22
|
+
|
|
23
|
+
`powerconf` allows you to write configuration files for things like physics simulations
|
|
24
|
+
with support for variable interpolation and expression evaluation. Consider a simulation
|
|
25
|
+
will solve some partial differential equation on a 2-dimensional Cartesian grid. Perhaps
|
|
26
|
+
the simulation itself requires us to set the min and max range and the number of points
|
|
27
|
+
to use along each axis. A simple YAML configuration for the simulation might look something
|
|
28
|
+
like this
|
|
29
|
+
|
|
30
|
+
```yaml
|
|
31
|
+
grid:
|
|
32
|
+
x:
|
|
33
|
+
min: 0 cm
|
|
34
|
+
max: 1.5 cm
|
|
35
|
+
N: 151
|
|
36
|
+
y:
|
|
37
|
+
min: 0 cm
|
|
38
|
+
max: 1.0 cm
|
|
39
|
+
N: 101
|
|
40
|
+
```
|
|
41
|
+
This is fine, but it might be useful to specify the resolution to use instead of the number of points.
|
|
42
|
+
With `powerconf`, we can write a configuration file that looks like this
|
|
43
|
+
|
|
44
|
+
```yaml
|
|
45
|
+
grid:
|
|
46
|
+
resolution: 1 um
|
|
47
|
+
x:
|
|
48
|
+
min: 0 cm
|
|
49
|
+
max: 1.5 cm
|
|
50
|
+
N: $( (${max} - ${min})/${../resolution} + 1)
|
|
51
|
+
y:
|
|
52
|
+
min: 0 cm
|
|
53
|
+
max: 1.0 cm
|
|
54
|
+
N: $( (${max} - ${min})/${../resolution} + 1)
|
|
55
|
+
```
|
|
56
|
+
In this example, we give a resolution to use for both x and y directions and then calculate the number
|
|
57
|
+
of points to use with an expression.
|
|
58
|
+
|
|
59
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
powerconf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
powerconf/cli.py,sha256=nGGxNrM9qmraMzHtx0E-trGOQzKhZDS-FS5kLN1QySc,1384
|
|
3
|
+
powerconf/exceptions.py,sha256=yw43FCBdvnNZJA_SLb2v1IVu4WggLluIOrikpjXjLgw,216
|
|
4
|
+
powerconf/expressions.py,sha256=gL6ShbFsz1qUXFTtsZeuS-Rzfkw7FLLDCeIvzrYo_f4,1186
|
|
5
|
+
powerconf/graphs.py,sha256=xIog0XKMpCskxZ7CNg8M9XFgKNyhWtIh02xRvPQEt6w,472
|
|
6
|
+
powerconf/parsing.py,sha256=zd7_6UNjRBesf9vwEhznGaPNygFXETQTHK1gBGXt41A,359
|
|
7
|
+
powerconf/readers.py,sha256=-rs3-eDUKhvFthVXNwTjnCgUrrdOgPBSgiiogAN3nL8,401
|
|
8
|
+
powerconf/rendering.py,sha256=7UK2_UrKQrl_SF6HLTfiGObQCY7mo-pQylpY9FbYdDM,13291
|
|
9
|
+
powerconf/utils.py,sha256=0aox4OgvKlI74lCXDXfE7nWcW7eFR_7_tJP5Bm76QvU,718
|
|
10
|
+
powerconf-0.1.0.dist-info/METADATA,sha256=6HCM2NNoITXlWvuh3nGz-NKufbTIa88m3yRfsuiQUvg,1805
|
|
11
|
+
powerconf-0.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
12
|
+
powerconf-0.1.0.dist-info/entry_points.txt,sha256=_W1WDvYgt6lCJXcTO6kjkEP2E7udwLYnS1C_5q7ncho,47
|
|
13
|
+
powerconf-0.1.0.dist-info/RECORD,,
|