jsongrapher 2.8__py3-none-any.whl → 3.7__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.
- JSONGrapher/JSONRecordCreator.py +2205 -375
- JSONGrapher/equation_creator.py +374 -0
- JSONGrapher/equation_evaluator.py +670 -0
- JSONGrapher/styles/__init__.py +0 -0
- JSONGrapher/styles/layout_styles_library.py +68 -0
- JSONGrapher/styles/trace_styles_collection_library.py +194 -0
- jsongrapher-3.7.data/data/LICENSE +9 -0
- jsongrapher-3.7.data/data/README.md +101 -0
- jsongrapher-3.7.dist-info/LICENSE +9 -0
- {jsongrapher-2.8.dist-info → jsongrapher-3.7.dist-info}/METADATA +29 -14
- jsongrapher-3.7.dist-info/RECORD +18 -0
- jsongrapher-2.8.data/data/LICENSE +0 -24
- jsongrapher-2.8.data/data/README.md +0 -88
- jsongrapher-2.8.dist-info/LICENSE +0 -24
- jsongrapher-2.8.dist-info/RECORD +0 -13
- {jsongrapher-2.8.dist-info → jsongrapher-3.7.dist-info}/WHEEL +0 -0
- {jsongrapher-2.8.dist-info → jsongrapher-3.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,374 @@
|
|
1
|
+
import re
|
2
|
+
import json
|
3
|
+
|
4
|
+
try:
|
5
|
+
from json_equationer.equation_evaluator import evaluate_equation_dict
|
6
|
+
except ImportError:
|
7
|
+
try:
|
8
|
+
from .equation_evaluator import evaluate_equation_dict
|
9
|
+
except ImportError:
|
10
|
+
from equation_evaluator import evaluate_equation_dict
|
11
|
+
|
12
|
+
|
13
|
+
class Equation:
|
14
|
+
"""
|
15
|
+
A class to manage mathematical equations with units and to evaluate them.
|
16
|
+
Provides utilities for evaluating, formatting, exporting, and printing.
|
17
|
+
|
18
|
+
Initialization:
|
19
|
+
- Normally, should be initialized as a blank dict object like example_Arrhenius = Equation().
|
20
|
+
- Defaults to an empty equation with predefined structure.
|
21
|
+
- Accepts an optional dictionary (`initial_dict`) to prepopulate the equation dictionary.
|
22
|
+
|
23
|
+
Example structure:
|
24
|
+
```
|
25
|
+
custom_dict = {
|
26
|
+
'equation_string': "k = A * (e ** (-Ea / (R * T)))",
|
27
|
+
'x_variable': "T (K)",
|
28
|
+
'y_variable': "k (s**-1)",
|
29
|
+
'constants': {"Ea": "30000 J/mol", "R": "8.314 J/(mol*K)", "A": "1*10**13 (s**-1)", "e": "2.71828"},
|
30
|
+
'num_of_points': 10,
|
31
|
+
'x_range_default': [200, 500],
|
32
|
+
'x_range_limits': [None, 600],
|
33
|
+
'points_spacing': "Linear"
|
34
|
+
'graphical_dimensionality': 2
|
35
|
+
}
|
36
|
+
|
37
|
+
#The reason we use 'graphical_dimensionality' rather than 'dimensionality' is that mathematicians define the dimensionality in terms of independent variables.
|
38
|
+
#But here, we are usually expecting users who are concerned with 2D or 3D graphing.
|
39
|
+
|
40
|
+
equation_instance = Equation(initial_dict=custom_dict)
|
41
|
+
```
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, initial_dict=None):
|
45
|
+
"""Initialize an empty equation dictionary."""
|
46
|
+
if initial_dict==None:
|
47
|
+
initial_dict = {}
|
48
|
+
self.equation_dict = {
|
49
|
+
'equation_string': '',
|
50
|
+
'x_variable': '',
|
51
|
+
'y_variable': '',
|
52
|
+
'constants': {},
|
53
|
+
'num_of_points': None, # Expected: Integer, defines the minimum number of points to be calculated for the range.
|
54
|
+
'x_range_default': [0, 1], # Default to [0,1] instead of an empty list.
|
55
|
+
'x_range_limits': [None, None], # Allows None for either limit.
|
56
|
+
'x_points_specified': [],
|
57
|
+
'points_spacing': '',
|
58
|
+
'reverse_scaling': False,
|
59
|
+
}
|
60
|
+
|
61
|
+
# If a dictionary is provided, update the default values
|
62
|
+
if len(initial_dict)>0:
|
63
|
+
if isinstance(initial_dict, dict):
|
64
|
+
self.equation_dict.update(initial_dict)
|
65
|
+
else:
|
66
|
+
raise TypeError("initial_dict must be a dictionary.")
|
67
|
+
|
68
|
+
def validate_unit(self, value):
|
69
|
+
"""Ensure that the value is either a pure number or contains a unit."""
|
70
|
+
unit_pattern = re.compile(r"^\d+(\.\d+)?(.*)?$")
|
71
|
+
if not unit_pattern.match(value):
|
72
|
+
raise ValueError(f"Invalid format: '{value}'. Expected a numeric value, optionally followed by a unit.")
|
73
|
+
|
74
|
+
def add_constants(self, constants):
|
75
|
+
"""Add constants to the equation dictionary, supporting both single and multiple additions."""
|
76
|
+
if isinstance(constants, dict): # Single constant case
|
77
|
+
for name, value in constants.items():
|
78
|
+
self.validate_unit(value)
|
79
|
+
self.equation_dict['constants'][name] = value
|
80
|
+
elif isinstance(constants, list): # Multiple constants case
|
81
|
+
for constant_dict in constants:
|
82
|
+
if isinstance(constant_dict, dict):
|
83
|
+
for name, value in constant_dict.items():
|
84
|
+
self.validate_unit(value)
|
85
|
+
self.equation_dict['constants'][name] = value
|
86
|
+
else:
|
87
|
+
raise ValueError("Each item in the list must be a dictionary containing a constant name-value pair.")
|
88
|
+
else:
|
89
|
+
raise TypeError("Expected a dictionary for one constant or a list of dictionaries for multiple constants.")
|
90
|
+
|
91
|
+
def set_x_variable(self, x_variable):
|
92
|
+
"""
|
93
|
+
Set the x-variable in the equation dictionary.
|
94
|
+
Expected format: A descriptive string including the variable name and its unit.
|
95
|
+
Example: "T (K)" for temperature in Kelvin.
|
96
|
+
"""
|
97
|
+
self.equation_dict["x_variable"] = x_variable
|
98
|
+
|
99
|
+
def set_y_variable(self, y_variable):
|
100
|
+
"""
|
101
|
+
Set the y-variable in the equation dictionary.
|
102
|
+
Expected format: A descriptive string including the variable name and its unit.
|
103
|
+
Example: "k (s**-1)" for a rate constant with inverse seconds as the unit.
|
104
|
+
"""
|
105
|
+
self.equation_dict["y_variable"] = y_variable
|
106
|
+
|
107
|
+
def set_z_variable(self, z_variable):
|
108
|
+
"""
|
109
|
+
Set the z-variable in the equation dictionary.
|
110
|
+
Expected format: A descriptive string including the variable name and its unit.
|
111
|
+
Example: "E (J)" for energy with joules as the unit.
|
112
|
+
"""
|
113
|
+
self.equation_dict["z_variable"] = z_variable
|
114
|
+
|
115
|
+
def set_x_range_default(self, x_range):
|
116
|
+
"""
|
117
|
+
Set the default x range.
|
118
|
+
Expected format: A list of two numeric values representing the range boundaries.
|
119
|
+
Example: set_x_range([200, 500]) for temperatures between 200K and 500K.
|
120
|
+
"""
|
121
|
+
if not (isinstance(x_range, list) and len(x_range) == 2 and all(isinstance(i, (int, float)) for i in x_range)):
|
122
|
+
raise ValueError("x_range must be a list of two numeric values.")
|
123
|
+
self.equation_dict['x_range_default'] = x_range
|
124
|
+
|
125
|
+
def set_x_range_limits(self, x_limits):
|
126
|
+
"""
|
127
|
+
Set the hard limits for x values.
|
128
|
+
Expected format: A list of two values (numeric or None) defining absolute boundaries.
|
129
|
+
Example: set_x_range_limits([100, 600]) to prevent x values outside this range.
|
130
|
+
Example: set_x_range_limits([None, 500]) allows an open lower limit.
|
131
|
+
"""
|
132
|
+
if not (isinstance(x_limits, list) and len(x_limits) == 2):
|
133
|
+
raise ValueError("x_limits must be a list of two elements (numeric or None).")
|
134
|
+
if not all(isinstance(i, (int, float)) or i is None for i in x_limits):
|
135
|
+
raise ValueError("Elements in x_limits must be numeric or None.")
|
136
|
+
self.equation_dict['x_range_limits'] = x_limits
|
137
|
+
|
138
|
+
def set_y_range_default(self, y_range):
|
139
|
+
"""
|
140
|
+
Set the default y range.
|
141
|
+
Expected format: A list of two numeric values representing the range boundaries.
|
142
|
+
Example: set_y_range([0, 100]) for a percentage scale.
|
143
|
+
"""
|
144
|
+
if not (isinstance(y_range, list) and len(y_range) == 2 and all(isinstance(i, (int, float)) for i in y_range)):
|
145
|
+
raise ValueError("y_range must be a list of two numeric values.")
|
146
|
+
self.equation_dict['y_range_default'] = y_range
|
147
|
+
|
148
|
+
def set_y_range_limits(self, y_limits):
|
149
|
+
"""
|
150
|
+
Set the hard limits for y values.
|
151
|
+
Expected format: A list of two values (numeric or None) defining absolute boundaries.
|
152
|
+
Example: set_y_range_limits([None, 50]) allows an open lower limit but restricts the upper limit.
|
153
|
+
"""
|
154
|
+
if not (isinstance(y_limits, list) and len(y_limits) == 2):
|
155
|
+
raise ValueError("y_limits must be a list of two elements (numeric or None).")
|
156
|
+
if not all(isinstance(i, (int, float)) or i is None for i in y_limits):
|
157
|
+
raise ValueError("Elements in y_limits must be numeric or None.")
|
158
|
+
self.equation_dict['y_range_limits'] = y_limits
|
159
|
+
|
160
|
+
def set_z_range_default(self, z_range):
|
161
|
+
"""
|
162
|
+
Set the default z range.
|
163
|
+
Expected format: A list of two numeric values representing the range boundaries.
|
164
|
+
Example: set_z_range([0, 5000]) for energy values in Joules.
|
165
|
+
"""
|
166
|
+
if not (isinstance(z_range, list) and len(z_range) == 2 and all(isinstance(i, (int, float)) for i in z_range)):
|
167
|
+
raise ValueError("z_range must be a list of two numeric values.")
|
168
|
+
self.equation_dict['z_range_default'] = z_range
|
169
|
+
|
170
|
+
def set_z_range_limits(self, z_limits):
|
171
|
+
"""
|
172
|
+
Set the hard limits for z values.
|
173
|
+
Expected format: A list of two values (numeric or None) defining absolute boundaries.
|
174
|
+
Example: set_z_range_limits([100, None]) allows an open upper limit but restricts the lower boundary.
|
175
|
+
"""
|
176
|
+
if not (isinstance(z_limits, list) and len(z_limits) == 2):
|
177
|
+
raise ValueError("z_limits must be a list of two elements (numeric or None).")
|
178
|
+
if not all(isinstance(i, (int, float)) or i is None for i in z_limits):
|
179
|
+
raise ValueError("Elements in z_limits must be numeric or None.")
|
180
|
+
self.equation_dict['z_range_limits'] = z_limits
|
181
|
+
|
182
|
+
def get_z_matrix(self, x_points=None, y_points=None, z_points=None, return_as_list=False):
|
183
|
+
"""
|
184
|
+
Constructs a Z matrix mapping unique (x, y) values to corresponding z values.
|
185
|
+
|
186
|
+
Parameters:
|
187
|
+
- x_points (list): List of x coordinates.
|
188
|
+
- y_points (list): List of y coordinates.
|
189
|
+
- z_points (list): List of z values.
|
190
|
+
- return_as_list (bool, optional): Whether to return the matrix as a list. Defaults to False (returns NumPy array).
|
191
|
+
|
192
|
+
Returns:
|
193
|
+
- z_matrix (2D list or numpy array): Matrix of z values.
|
194
|
+
- unique_x (list): Sorted unique x values.
|
195
|
+
- unique_y (list): Sorted unique y values.
|
196
|
+
"""
|
197
|
+
if x_points == None:
|
198
|
+
x_points = self.equation_dict['x_points']
|
199
|
+
if y_points == None:
|
200
|
+
y_points = self.equation_dict['y_points']
|
201
|
+
if z_points == None:
|
202
|
+
z_points = self.equation_dict['z_points']
|
203
|
+
|
204
|
+
import numpy as np
|
205
|
+
# Get unique x and y values
|
206
|
+
unique_x = sorted(set(x_points))
|
207
|
+
unique_y = sorted(set(y_points))
|
208
|
+
|
209
|
+
# Create an empty matrix filled with NaNs
|
210
|
+
z_matrix = np.full((len(unique_x), len(unique_y)), np.nan)
|
211
|
+
|
212
|
+
# Map z values to corresponding x, y indices
|
213
|
+
for x, y, z in zip(x_points, y_points, z_points):
|
214
|
+
x_idx = unique_x.index(x)
|
215
|
+
y_idx = unique_y.index(y)
|
216
|
+
z_matrix[x_idx, y_idx] = z
|
217
|
+
|
218
|
+
# Convert to a list if requested
|
219
|
+
if return_as_list:
|
220
|
+
z_matrix = z_matrix.tolist()
|
221
|
+
|
222
|
+
return z_matrix
|
223
|
+
|
224
|
+
|
225
|
+
|
226
|
+
|
227
|
+
|
228
|
+
def set_num_of_points(self, num_points):
|
229
|
+
"""
|
230
|
+
Set the number of calculation points.
|
231
|
+
Expected format: Integer, specifies the number of discrete points for calculations.
|
232
|
+
Example: set_num_of_points(10) for ten data points.
|
233
|
+
"""
|
234
|
+
if not isinstance(num_points, int) or num_points <= 0:
|
235
|
+
raise ValueError("Number of points must be a positive integer.")
|
236
|
+
self.equation_dict["num_of_points"] = num_points
|
237
|
+
|
238
|
+
def set_equation(self, equation_string):
|
239
|
+
"""Modify the equation string."""
|
240
|
+
self.equation_dict['equation_string'] = equation_string
|
241
|
+
|
242
|
+
def get_equation_dict(self):
|
243
|
+
"""Return the complete equation dictionary."""
|
244
|
+
return self.equation_dict
|
245
|
+
|
246
|
+
def evaluate_equation(self, remove_equation_fields= False, verbose=False):
|
247
|
+
evaluated_dict = evaluate_equation_dict(self.equation_dict, verbose=verbose) #this function is from the evaluator module
|
248
|
+
if "graphical_dimensionality" in evaluated_dict:
|
249
|
+
graphical_dimensionality = evaluated_dict["graphical_dimensionality"]
|
250
|
+
else:
|
251
|
+
graphical_dimensionality = 2
|
252
|
+
self.equation_dict["x_units"] = evaluated_dict["x_units"]
|
253
|
+
self.equation_dict["y_units"] = evaluated_dict["y_units"]
|
254
|
+
self.equation_dict["x_points"] = evaluated_dict["x_points"]
|
255
|
+
self.equation_dict["y_points"] = evaluated_dict["y_points"]
|
256
|
+
if graphical_dimensionality == 3:
|
257
|
+
self.equation_dict["z_points"] = evaluated_dict["z_points"]
|
258
|
+
if remove_equation_fields == True:
|
259
|
+
#we'll just make a fresh dictionary for simplicity, in this case.
|
260
|
+
equation_dict = {}
|
261
|
+
equation_dict["x_units"] = self.equation_dict["x_units"]
|
262
|
+
equation_dict["y_units"] = self.equation_dict["y_units"]
|
263
|
+
equation_dict["x_points"] = self.equation_dict["x_points"]
|
264
|
+
equation_dict["y_points"] = self.equation_dict["y_points"]
|
265
|
+
if graphical_dimensionality == 3:
|
266
|
+
equation_dict["z_units"] = self.equation_dict["z_units"]
|
267
|
+
equation_dict["z_points"] = self.equation_dict["z_points"]
|
268
|
+
print("line 223", equation_dict["z_points"])
|
269
|
+
self.equation_dict = equation_dict
|
270
|
+
return self.equation_dict
|
271
|
+
|
272
|
+
def print_equation_dict(self, pretty_print=True, evaluate_equation = True, remove_equation_fields = False):
|
273
|
+
equation_dict = self.equation_dict #populate a variable internal to this function.
|
274
|
+
#if evaluate_equation is true, we'll try to simulate any series that need it, then clean the simulate fields out if requested.
|
275
|
+
if evaluate_equation == True:
|
276
|
+
evaluated_dict = self.evaluate_equation(remove_equation_fields = remove_equation_fields) #For this function, we don't want to remove equation fields from the object, just the export.
|
277
|
+
equation_dict = evaluated_dict
|
278
|
+
if remove_equation_fields == True:
|
279
|
+
equation_dict = {}
|
280
|
+
equation_dict["x_units"] = self.equation_dict["x_units"]
|
281
|
+
equation_dict["y_units"] = self.equation_dict["y_units"]
|
282
|
+
equation_dict["x_points"] = self.equation_dict["x_points"]
|
283
|
+
equation_dict["y_points"] = self.equation_dict["y_points"]
|
284
|
+
if pretty_print == False:
|
285
|
+
print(equation_dict)
|
286
|
+
if pretty_print == True:
|
287
|
+
equation_json_string = json.dumps(equation_dict, indent=4)
|
288
|
+
print(equation_json_string)
|
289
|
+
|
290
|
+
def export_to_json_file(self, filename, evaluate_equation = True, remove_equation_fields= False):
|
291
|
+
"""
|
292
|
+
writes the json to a file
|
293
|
+
returns the json as a dictionary.
|
294
|
+
update_and_validate function will clean for plotly. One can alternatively only validate.
|
295
|
+
optionally simulates all series that have a simulate field (does so by default)
|
296
|
+
optionally removes simulate filed from all series that have a simulate field (does not do so by default)
|
297
|
+
optionally removes hints before export and return.
|
298
|
+
"""
|
299
|
+
equation_dict = self.equation_dict #populate a variable internal to this function.
|
300
|
+
#if evaluate_equation is true, we'll try to simulate any series that need it, then clean the simulate fields out if requested.
|
301
|
+
if evaluate_equation == True:
|
302
|
+
evaluated_dict = self.evaluate_equation(remove_equation_fields = remove_equation_fields) #For this function, we don't want to remove equation fields from the object, just the export.
|
303
|
+
equation_dict = evaluated_dict
|
304
|
+
if remove_equation_fields == True:
|
305
|
+
equation_dict = {}
|
306
|
+
equation_dict["x_units"] = self.equation_dict["x_units"]
|
307
|
+
equation_dict["y_units"] = self.equation_dict["y_units"]
|
308
|
+
equation_dict["x_points"] = self.equation_dict["x_points"]
|
309
|
+
equation_dict["y_points"] = self.equation_dict["y_points"]
|
310
|
+
# filepath: Optional, filename with path to save the JSON file.
|
311
|
+
if len(filename) > 0: #this means we will be writing to file.
|
312
|
+
# Check if the filename has an extension and append `.json` if not
|
313
|
+
if '.json' not in filename.lower():
|
314
|
+
filename += ".json"
|
315
|
+
#Write to file using UTF-8 encoding.
|
316
|
+
with open(filename, 'w', encoding='utf-8') as f:
|
317
|
+
json.dump(equation_dict, f, indent=4)
|
318
|
+
return equation_dict
|
319
|
+
|
320
|
+
|
321
|
+
|
322
|
+
if __name__ == "__main__":
|
323
|
+
# Create an instance of Equation
|
324
|
+
example_Arrhenius = Equation()
|
325
|
+
example_Arrhenius.set_equation("k = A * (e ** (-Ea / (R * T)))")
|
326
|
+
example_Arrhenius.set_x_variable("T (K)") # Temperature in Kelvin
|
327
|
+
example_Arrhenius.set_y_variable("k (s**-1)") # Rate constant in inverse seconds
|
328
|
+
|
329
|
+
# Add a constants one at a time, or through a list.
|
330
|
+
example_Arrhenius.add_constants({"Ea": "30000 J/mol"})
|
331
|
+
example_Arrhenius.add_constants([
|
332
|
+
{"R": "8.314 J/(mol*K)"},
|
333
|
+
{"A": "1*10**13 (s**-1)"},
|
334
|
+
{"e": "2.71828"} # No unit required
|
335
|
+
])
|
336
|
+
|
337
|
+
# Optinally, set minimum number of points and limits for calculations.
|
338
|
+
example_Arrhenius.set_num_of_points(10)
|
339
|
+
example_Arrhenius.set_x_range_default([200, 500])
|
340
|
+
example_Arrhenius.set_x_range_limits([None, 600])
|
341
|
+
|
342
|
+
# Define additional properties.
|
343
|
+
example_Arrhenius.equation_dict["points_spacing"] = "Linear"
|
344
|
+
|
345
|
+
# Retrieve and display the equation dictionary
|
346
|
+
example_equation_dict = example_Arrhenius.get_equation_dict()
|
347
|
+
print(example_equation_dict)
|
348
|
+
|
349
|
+
example_Arrhenius.evaluate_equation()
|
350
|
+
example_Arrhenius.print_equation_dict()
|
351
|
+
|
352
|
+
|
353
|
+
#Now for a 3D example.
|
354
|
+
example_Arrhenius_3D_dict = {
|
355
|
+
'equation_string': 'k = A*(e**((-Ea)/(R*T)))',
|
356
|
+
'graphical_dimensionality' : 3,
|
357
|
+
'x_variable': 'T (K)',
|
358
|
+
'y_variable': 'Ea (J)*(mol^(-1))',
|
359
|
+
'z_variable': 'k (s**(-1))',
|
360
|
+
'constants': {'R': '8.314 (J)*(mol^(-1))*(K^(-1))' , 'A': '1*10^13 (s^-1)', 'e': '2.71828'},
|
361
|
+
'num_of_points': 10,
|
362
|
+
'x_range_default': [200, 500],
|
363
|
+
'x_range_limits' : [],
|
364
|
+
'y_range_default': [30000, 50000],
|
365
|
+
'y_range_limits' : [],
|
366
|
+
'x_points_specified' : [],
|
367
|
+
'points_spacing': 'Linear',
|
368
|
+
'reverse_scaling' : False
|
369
|
+
}
|
370
|
+
|
371
|
+
example_Arrhenius_3D_equation = Equation(initial_dict=example_Arrhenius_3D_dict)
|
372
|
+
evaluated_output = example_Arrhenius_3D_equation.evaluate_equation()
|
373
|
+
#print(evaluated_output)
|
374
|
+
#print(example_Arrhenius_3D_equation.get_z_matrix(return_as_list=True))
|