jsongrapher 2.8__py3-none-any.whl → 3.8__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.
@@ -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))