jsongrapher 1.6__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 +2870 -227
- JSONGrapher/UnitPytesting.py +125 -0
- JSONGrapher/UnitpyCustomUnitsTesting.py +28 -0
- JSONGrapher/__init__.py +1 -1
- JSONGrapher/drag_and_drop_gui.py +109 -0
- 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/units_list.py +27 -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-3.7.dist-info/METADATA +128 -0
- jsongrapher-3.7.dist-info/RECORD +18 -0
- jsongrapher-1.6.data/data/LICENSE +0 -24
- jsongrapher-1.6.data/data/README.md +0 -79
- jsongrapher-1.6.dist-info/LICENSE +0 -24
- jsongrapher-1.6.dist-info/METADATA +0 -101
- jsongrapher-1.6.dist-info/RECORD +0 -10
- {jsongrapher-1.6.dist-info → jsongrapher-3.7.dist-info}/WHEEL +0 -0
- {jsongrapher-1.6.dist-info → jsongrapher-3.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
import unitpy
|
2
|
+
import unitpy.definitions
|
3
|
+
import unitpy.definitions.entry
|
4
|
+
|
5
|
+
print(unitpy.U("kg/m"))
|
6
|
+
|
7
|
+
|
8
|
+
print(unitpy.U("(((kg)/m))/s"))
|
9
|
+
|
10
|
+
units1 = unitpy.U("(((kg)/m))/s")
|
11
|
+
units2 = unitpy.U("(((g)/m))/s")
|
12
|
+
unitsRatio = units1/units2
|
13
|
+
print(unitsRatio)
|
14
|
+
|
15
|
+
print(units2)
|
16
|
+
units1multiplied =1*unitpy.U("(((kg)/m))/s")
|
17
|
+
print("line 14")
|
18
|
+
ratioWithUnits = units1multiplied.to("(((g)/m))/s")
|
19
|
+
print(ratioWithUnits)
|
20
|
+
print(str(ratioWithUnits).split(' '))
|
21
|
+
|
22
|
+
units1 = unitpy.Q("1 (((kg)/m))/s")
|
23
|
+
units2 = unitpy.Q("1 (((g)/m))/s")
|
24
|
+
print("line 22", units2.base_unit)
|
25
|
+
unitsRatio = units1/units2
|
26
|
+
print("line 23")
|
27
|
+
print(unitsRatio)
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
print(units2)
|
32
|
+
units1multiplied =1*unitpy.U("(((kg)/m))*(s**-1)")
|
33
|
+
print("line 14")
|
34
|
+
ratioWithUnits = units1multiplied.to("(((g)/m))/s")
|
35
|
+
print(ratioWithUnits)
|
36
|
+
print(str(ratioWithUnits).split(' '))
|
37
|
+
print(units1multiplied.base_unit)
|
38
|
+
|
39
|
+
units5 = unitpy.Q("1 (((g)/m))/s")
|
40
|
+
print(units5.base_unit)
|
41
|
+
|
42
|
+
|
43
|
+
def convert_inverse_units(expression, depth=100):
|
44
|
+
import re
|
45
|
+
# Patterns to match valid reciprocals while ignoring multiplied units
|
46
|
+
patterns = [r"1/\((1/.*?)\)", r"1/([a-zA-Z]+)"]
|
47
|
+
|
48
|
+
for _ in range(depth):
|
49
|
+
new_expression = expression
|
50
|
+
for pattern in patterns:
|
51
|
+
new_expression = re.sub(pattern, r"(\1)**(-1)", new_expression)
|
52
|
+
|
53
|
+
# Stop early if no more changes are made
|
54
|
+
if new_expression == expression:
|
55
|
+
break
|
56
|
+
expression = new_expression
|
57
|
+
return expression
|
58
|
+
|
59
|
+
|
60
|
+
expression_original = "1/(1/bar)"
|
61
|
+
expression_altered = convert_inverse_units(expression_original)
|
62
|
+
units6 = unitpy.Q('1*'+expression_altered)
|
63
|
+
print(units6.unit)
|
64
|
+
|
65
|
+
from unitpy import U, Unit
|
66
|
+
import unitpy
|
67
|
+
newunit = unitpy.Unit("meter")
|
68
|
+
from unitpy.definitions.entry import Entry
|
69
|
+
# new_entry = Entry("frog", "frog", "frog", 1.0)
|
70
|
+
# unitpy.ledger.add_unit(new_entry)
|
71
|
+
def add_custom_unit_to_unitpy(unit_string):
|
72
|
+
import unitpy
|
73
|
+
from unitpy.definitions.entry import Entry
|
74
|
+
#need to put an entry into "bases" because the BaseSet class will pull from that dictionary.
|
75
|
+
unitpy.definitions.unit_base.bases[unit_string] = unitpy.definitions.unit_base.BaseUnit(label=unit_string, abbr=unit_string,dimension=unitpy.definitions.dimensions.dimensions["amount_of_substance"])
|
76
|
+
#Then need to make a BaseSet object to put in. Confusingly, we *do not* put a BaseUnit object into the base_unit argument, below.
|
77
|
+
#We use "mole" to avoid conflicting with any other existing units.
|
78
|
+
base_unit =unitpy.definitions.unit_base.BaseSet(mole = 1)
|
79
|
+
#base_unit = unitpy.definitions.unit_base.BaseUnit(label=unit_string, abbr=unit_string,dimension=unitpy.definitions.dimensions.dimensions["amount_of_substance"])
|
80
|
+
new_entry = Entry(label = unit_string, abbr = unit_string, base_unit = base_unit, multiplier= 1)
|
81
|
+
#only add the entry if it is missing. A duplicate entry would cause crashing later.
|
82
|
+
if not unitpy.ledger.get_entry(new_entry):
|
83
|
+
unitpy.ledger.add_unit(new_entry) #implied return is here. No return needed.
|
84
|
+
|
85
|
+
add_custom_unit_to_unitpy("frog")
|
86
|
+
#TODO: now know one way how to add custom units to unitpy.
|
87
|
+
#Cannot put "<>" inside unitpy, but could filter those out, and then put them back. Would need to make a list of unique entries with <> because there could be more than one.
|
88
|
+
|
89
|
+
another_test = "1/bar/(1/bar)*bar*frog"
|
90
|
+
another_test = convert_inverse_units(another_test)
|
91
|
+
print("line 65", another_test)
|
92
|
+
units7 = unitpy.U(another_test)
|
93
|
+
print("line 66", units7)
|
94
|
+
print("line 86", 1*units7)
|
95
|
+
print(unitpy.ledger.get_entry("frog"))
|
96
|
+
units_string_1 = 'm*frog'
|
97
|
+
print("line 87", 1*unitpy.U(units_string_1))
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
print("line 94")
|
103
|
+
|
104
|
+
units_string_2 = 'm*frog'
|
105
|
+
|
106
|
+
units_string_1_multiplied = 1*unitpy.U(units_string_1 )
|
107
|
+
units_string_1_multiplied.to(units_string_2)
|
108
|
+
|
109
|
+
|
110
|
+
print(units2)
|
111
|
+
units1multiplied =1*unitpy.U("(((kg)/m))/s")
|
112
|
+
print("line 85")
|
113
|
+
string2 = "(((g)/m))*1/s"
|
114
|
+
string2 = convert_inverse_units(string2)
|
115
|
+
print(string2)
|
116
|
+
ratioWithUnits = units1multiplied.to(string2)
|
117
|
+
print(ratioWithUnits)
|
118
|
+
print(str(ratioWithUnits).split(' '))
|
119
|
+
|
120
|
+
|
121
|
+
#micrometer symbol, "μ" will result in error, also typing out "microm" will result in error, but "micrometer" works
|
122
|
+
units1 = unitpy.U("(((kg)/mm))/s")
|
123
|
+
#units2 = unitpy.U("(((g)/μm))/s")
|
124
|
+
#units2 = unitpy.U("(((g)/microm))/s")
|
125
|
+
units2 = unitpy.U("(((g)/micrometer))/s")
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import unitpy
|
2
|
+
import re
|
3
|
+
from unitpy.definitions.entry import Entry
|
4
|
+
|
5
|
+
def add_custom_unit(unit_string):
|
6
|
+
# Need to put an entry into "bases" because the BaseSet class will pull from that dictionary.
|
7
|
+
unitpy.definitions.unit_base.bases[unit_string] = unitpy.definitions.unit_base.BaseUnit(
|
8
|
+
label=unit_string, abbr=unit_string, dimension=unitpy.definitions.dimensions.dimensions["amount_of_substance"]
|
9
|
+
)
|
10
|
+
|
11
|
+
# Then need to make a BaseSet object to put in. Confusingly, we *do not* put a BaseUnit object into the base_unit argument, below.
|
12
|
+
# We use "mole" to avoid conflicting with any other existing units.
|
13
|
+
base_unit = unitpy.definitions.unit_base.BaseSet(mole=1)
|
14
|
+
|
15
|
+
new_entry = Entry(label=unit_string, abbr=unit_string, base_unit=base_unit, multiplier=1)
|
16
|
+
|
17
|
+
# Only add the entry if it is missing. A duplicate entry would cause crashing later.
|
18
|
+
if 'frog' not in unitpy.ledger._lookup:
|
19
|
+
unitpy.ledger.add_unit(new_entry) # Implied return is here. No return needed.
|
20
|
+
|
21
|
+
add_custom_unit("frog")
|
22
|
+
add_custom_unit("frog")
|
23
|
+
|
24
|
+
units_string_1 = 'm*frog'
|
25
|
+
units_string_2 = 'm*frog'
|
26
|
+
units_string_1_multiplied = 1*unitpy.U(units_string_1 )
|
27
|
+
print("line 25", type(units_string_1_multiplied))
|
28
|
+
units_string_1_multiplied.to(units_string_2)
|
JSONGrapher/__init__.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
#Try to do things a bit like https://github.com/python/cpython/blob/master/Lib/collections/__init__.py
|
2
2
|
#Except instead of putting things here directly in __init__, we'll import them so they are accessible by importing the module.
|
3
|
-
from
|
3
|
+
from JSONGrapher.JSONRecordCreator import *
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import os
|
2
|
+
import tkinter as tk
|
3
|
+
from tkinter import filedialog
|
4
|
+
from tkinterdnd2 import DND_FILES, TkinterDnD
|
5
|
+
|
6
|
+
|
7
|
+
#The below class creates a window for dragging and dropping or browsing and selecting files
|
8
|
+
#And each time one or more file is added, the full file list and most recently added files will be passed to
|
9
|
+
#The function supplied by the user (function_for_after_file_addition)
|
10
|
+
#with the two variables passed being all_selected_file_paths, newly_added_file_paths
|
11
|
+
#This class **cannot** be initiated directly, it should initiated using the
|
12
|
+
#companion function create_and_launch
|
13
|
+
class DragDropApp:
|
14
|
+
def __init__(self, root, app_name = '', function_for_after_file_addition = None):
|
15
|
+
self.root = root
|
16
|
+
self.root.title(app_name)
|
17
|
+
self.function_for_after_file_addition = function_for_after_file_addition
|
18
|
+
|
19
|
+
# Enable native drag-and-drop capability
|
20
|
+
self.root.drop_target_register(DND_FILES)
|
21
|
+
self.root.dnd_bind("<<Drop>>", self.drop_files)
|
22
|
+
|
23
|
+
# Create a drop zone
|
24
|
+
self.drop_frame = tk.Label(root, text="Drag and drop files here \n\n Click End When Finished", bg="lightgray", width=50, height=10)
|
25
|
+
self.drop_frame.pack(pady=10)
|
26
|
+
|
27
|
+
# Create a listbox to display selected files
|
28
|
+
self.file_listbox = tk.Listbox(root, width=60, height=10)
|
29
|
+
self.file_listbox.pack(pady=10)
|
30
|
+
|
31
|
+
# Buttons for manual selection and finalizing selection
|
32
|
+
self.select_button = tk.Button(root, text="Select Files By Browsing", command=self.open_file_dialog)
|
33
|
+
self.select_button.pack(pady=5)
|
34
|
+
|
35
|
+
# Create a frame for the middle buttons
|
36
|
+
button_frame_middle = tk.Frame(root)
|
37
|
+
button_frame_middle.pack(pady=5)
|
38
|
+
|
39
|
+
self.clear_button = tk.Button(button_frame_middle, text="Clear Files List", command=self.clear_file_list) # New "Clear" button
|
40
|
+
self.clear_button.pack(side = tk.LEFT, pady=5)
|
41
|
+
|
42
|
+
# "Download Output" button
|
43
|
+
self.download_button = tk.Button(button_frame_middle, text="Download Output", command=self.download_output)
|
44
|
+
self.download_button.pack(side = tk.RIGHT, pady=5)
|
45
|
+
|
46
|
+
self.done_button = tk.Button(root, text="End", command=self.finish_selection)
|
47
|
+
self.done_button.pack(pady=5)
|
48
|
+
|
49
|
+
# Store selected file paths
|
50
|
+
self.all_selected_file_paths = []
|
51
|
+
|
52
|
+
def clear_file_list(self):
|
53
|
+
"""Clears the listbox and resets selected files."""
|
54
|
+
self.file_listbox.delete(0, tk.END) # Clear listbox
|
55
|
+
self.all_selected_file_paths = [] # Reset file list
|
56
|
+
self.function_for_after_file_addition(all_selected_file_paths=[], newly_added_file_paths=[])
|
57
|
+
print("File list cleared!") # Optional debug message
|
58
|
+
|
59
|
+
def open_file_dialog(self):
|
60
|
+
"""Opens a file dialog to manually select files."""
|
61
|
+
newly_added_file_paths = self.root.tk.splitlist(tk.filedialog.askopenfilenames(title="Select files"))
|
62
|
+
if newly_added_file_paths:
|
63
|
+
self.all_selected_file_paths.extend(newly_added_file_paths)
|
64
|
+
self.update_file_list(newly_added_file_paths)
|
65
|
+
|
66
|
+
def drop_files(self, event):
|
67
|
+
"""Handles dropped files into the window."""
|
68
|
+
newly_added_file_paths = self.root.tk.splitlist(event.data)
|
69
|
+
if newly_added_file_paths:
|
70
|
+
self.all_selected_file_paths.extend(newly_added_file_paths)
|
71
|
+
self.update_file_list(newly_added_file_paths)
|
72
|
+
|
73
|
+
def update_file_list(self, newly_added_file_paths):
|
74
|
+
"""Updates the listbox with selected filenames."""
|
75
|
+
self.file_listbox.delete(0, tk.END) # Clear listbox
|
76
|
+
for filename_and_path in self.all_selected_file_paths:
|
77
|
+
self.file_listbox.insert(tk.END, os.path.basename(filename_and_path)) # Show filenames only
|
78
|
+
# If there is a function_for_after_file_addition, pass full list and newly added files into function_for_after_file_addition
|
79
|
+
if self.function_for_after_file_addition is not None:
|
80
|
+
output = self.function_for_after_file_addition(self.all_selected_file_paths, newly_added_file_paths)
|
81
|
+
self.output_for_download = output[0] #store the first part of the output for download.
|
82
|
+
|
83
|
+
def download_output(self):
|
84
|
+
"""Allows user to choose where to save the output."""
|
85
|
+
if hasattr(self, "output_for_download"):
|
86
|
+
file_path = filedialog.asksaveasfilename(filetypes=[("*.*", "*.txt")], title="Save Output As")
|
87
|
+
if file_path: # If a valid path is chosen
|
88
|
+
with open(file_path, "w") as file:
|
89
|
+
file.write(str(self.output_for_download))
|
90
|
+
print(f"Output saved as '{file_path}'!")
|
91
|
+
else:
|
92
|
+
print("File save operation canceled.")
|
93
|
+
else:
|
94
|
+
print("No output available to download.")
|
95
|
+
|
96
|
+
|
97
|
+
def finish_selection(self):
|
98
|
+
"""Closes the window and returns selected files."""
|
99
|
+
self.root.quit() # Close the window
|
100
|
+
|
101
|
+
# This function is a companion function to
|
102
|
+
# The class DragDropApp for creating a file selection and function call app
|
103
|
+
# The function_for_after_file_addition should return a list where the first item is something that can be downloaded.
|
104
|
+
def create_and_launch(app_name = '', function_for_after_file_addition=None):
|
105
|
+
"""Starts the GUI and returns selected files."""
|
106
|
+
root = TkinterDnD.Tk()
|
107
|
+
app = DragDropApp(root, app_name=app_name, function_for_after_file_addition=function_for_after_file_addition)
|
108
|
+
root.mainloop() # Runs the Tkinter event loop
|
109
|
+
return app.all_selected_file_paths # Returns selected files after the window closes
|
@@ -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))
|