bibun 0.0.0

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,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibun
4
+ # Class that stores the data relative to the Butcher table used to perform the integration.
5
+ class DifferentialButcherTable
6
+ # The Butcher table nodes or c values. At each stage ti = tn + ch.
7
+ # @return [Array<Numeric>]
8
+ attr_accessor :nodes
9
+ # The Butcher node coefficients or a values. At each stage yi = y1ai1 + y2ai2 + y3ai3...y[i-1]ai[i-1]
10
+ # @return [Array<Numeric>]
11
+ attr_accessor :matrix_coefficients
12
+ # The weights by which each k_step value is multiplied before summing them into the final variable change.
13
+ # They are the Butcher table b values such as k = k1b1 + k2b2...
14
+ # @return [[Array<Numeric>]]
15
+ attr_accessor :weights
16
+ # For embedded methods they are another set of weights which give an estimate of the change for the variable of
17
+ # lesser order and against which the reference calculation is compared for estimating the error..
18
+ # They are the Butcher table b* values such as k = k1b*1 + k2b*2...
19
+ # @return [[Array<Numeric>]]
20
+ attr_accessor :alternate_weights
21
+ # @return [Boolean] Indicates whether the butcher table method is adaptive or not.
22
+ attr_reader :adaptive
23
+ # @return [Integer] The order of the method.
24
+ attr_reader :order
25
+ # @return [Hash{String => Numeric}] Constants for polynomomial interpolation.
26
+ attr_reader :interpolation_constants
27
+ # @return [Hash{String => String}] Formulas to calculate coefficients for polynomomial interpolation.
28
+ attr_reader :interpolation_formulas
29
+ alias adaptive? adaptive
30
+
31
+ # List of methods which can be preloaded. rk4 is the default one for non adaptive and dopr45 for adaptive.
32
+ DIFFERENTIATION_METHODS = %i[rk4 rk3 midpoint dopr45].freeze
33
+
34
+ # Initialization.
35
+ def initialize
36
+ @weights, @nodes, @matrix_coefficients = [], [], []
37
+ end
38
+
39
+ # Because a Butcher Table might be provided by the user, evaluates if the arrays are empty to choose whether to
40
+ # load a default or not.
41
+ # @return [Boolean]
42
+ def empty?
43
+ weights.union(nodes, matrix_coefficients).empty?
44
+ end
45
+
46
+ # @return [Integer] The number of stages for array indexing.
47
+ def stages
48
+ weights.size - 1
49
+ end
50
+
51
+ # Loads a preset Butcher Table defined in the inner TOML file.
52
+ def load_preset(method:)
53
+ data_path = File.expand_path('../data/butcher_tables.toml', __dir__)
54
+ rationalize = ->(s) { s.is_a?(String) ? s.to_r : s }
55
+ rationalize_array = ->(ar) { ar.map! { |val| rationalize.call(val) } }
56
+ toml = Tomlib.load File.read(data_path)
57
+ toml = toml[method.to_s]
58
+ @order = toml['order']
59
+ @nodes = rationalize_array.call(toml['nodes'])
60
+ @weights = rationalize_array.call(toml['weights'])
61
+ @matrix_coefficients = toml['matrix_coefficients']
62
+ matrix_coefficients.each { |ar| rationalize_array.call(ar) }
63
+ @adaptive = toml['adaptive']
64
+ if adaptive?
65
+ @alternate_weights = rationalize_array.call(toml['alternate_weights'])
66
+ @interpolation_constants = toml['interpolation']['constants'].transform_values { |v| rationalize.call(v) }
67
+ @interpolation_formulas = toml['interpolation']['coefficients']
68
+ interpolation_formulas.store 'final', toml['interpolation']['formula']['final']
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Bibun
6
+ class DifferentialEquationSystem
7
+ # Class to handle the logging of values calculated through the simulation of differential equations.
8
+ # This class is not meant to be used or instantiated independently, the DES instance already creates one when
9
+ # initialized. Should be always accesed through the DifferentialEquationSystem#logger getter.
10
+ class DifferentialEquationLogger
11
+ # @return [Hash{String => LogEntry}] Each of the entries or columns to register in the log.
12
+ attr_accessor :entries
13
+ # @return [SymbolGroup] Accessor to the SymbolGroup of the DES
14
+ attr_reader :syms
15
+ attr_accessor :buffer, :has_expressions, :system, :tracker
16
+ attr_reader :logging_interval
17
+
18
+ def initialize(system, tracker, syms)
19
+ @entries = {}
20
+ @logs = {}
21
+ @order = []
22
+ @buffer = {}
23
+ @has_expressions = false
24
+ @system = system
25
+ @syms = syms
26
+ @tracker = tracker
27
+ end
28
+
29
+ # Direct access in a hash style to the entries' arrays of values.
30
+ # @return [Hash{String => Array<Numeric>}]
31
+ def logs = entries.transform_values(&:values)
32
+
33
+ # Adds an arbitrary number of variables to the logger.
34
+ def add_variables(vars_decimals)
35
+ vars_decimals.each { |k, v| entries.store k, VariableLogEntry.new(self, v, k) }
36
+ end
37
+
38
+ # Method to add an specific additive term pertaining an specific variable.
39
+ def add_rate_term(variable_name, term_name, decimals)
40
+ entries.store "#{variable_name}:#{term_name}", TermLogEntry.new(self, decimals, variable_name, term_name)
41
+ end
42
+
43
+ # The equivalent of a database ROW_NUMBER, for those cases where time is not an integer.
44
+ def add_step_state
45
+ entries.store 'step', StepLogEntry.new(self, 0)
46
+ end
47
+
48
+ # Way to add custom expressions whose calculation is desired to be tracked and logged.
49
+ def add_expression(name, formula, decimals)
50
+ entries.store name, ExpressionLogEntry.new(self, decimals, name, formula)
51
+ @has_expressions = true
52
+ end
53
+
54
+ # Adds a single variable to the logger along its additive terms. All terms are added by default, but this
55
+ # behaviour can be overriden by passing a list including only the target terms.
56
+ def add_variable_with_terms(variable, decimals, *only)
57
+ add_variables({ variable => decimals })
58
+ stream_array = only.empty? ? syms.variables[variable].terms.keys : only
59
+ stream_array.each { |s| add_rate_term(variable, s, decimals) }
60
+ end
61
+
62
+ # To avoid having to write a decimal specification for each variable, this methods allows to add several variables
63
+ # simultaneously while only passing a single decimal specification for all of them.
64
+ def add_same_decimals_to_variables(decimals, *variable_group)
65
+ variable_group.each { |v| add_variables({ v => decimals }) }
66
+ end
67
+
68
+ # Quickest way to set a logger. All is needed is to provide the decimals once. The with_streams: keyword argument
69
+ # allows to have all terms from all variables logged.
70
+ def add_all(decimals, with_terms: false)
71
+ if with_terms
72
+ add_variables({ syms.ind_var.name => decimals })
73
+ syms.rate_variables.each_key do |k|
74
+ add_variable_with_terms(k, decimals)
75
+ end
76
+ else
77
+ add_variables(syms.variables.transform_values { |_| decimals })
78
+ end
79
+ end
80
+
81
+ # Quick method to check if there is any rate additive term to log.
82
+ def log_terms? = entries.values.any? { |le| le.instance_of?(TermLogEntry) }
83
+
84
+ # Quick way to access the terms.
85
+ # @return [Array<TermLogEntry>] The array with the log entries.
86
+ def term_entries = entries.values.select { |et| et.instance_of?(TermLogEntry) }
87
+
88
+ # Preparation routine. Logs the initial values of the variables.
89
+ def prepare
90
+ start_logs
91
+ log_start
92
+ end
93
+
94
+ # Caches the parameters for expression calculations
95
+ def start_logs
96
+ @parameters = syms.pars_values_hash if @has_expressions
97
+ end
98
+
99
+ # Registering the log interval. Method is placed here because it is intuitive since it pertains the logging.
100
+ def logging_interval=(independent_variable_interval)
101
+ tracker.logging_interval = independent_variable_interval
102
+ end
103
+
104
+ # Adds the first row to the log comprised of the starting values and expressions calculated from them.
105
+ def log_start
106
+ entries.values.reject {|v| v.instance_of?(TermLogEntry) }.each do |ls|
107
+ ls.buffer_hash = syms.syms_values_hash
108
+ if ls.instance_of?(VariableLogEntry)
109
+ ls.register_value
110
+ else
111
+ ls.register # Step and Expressions
112
+ end
113
+ end
114
+ end
115
+
116
+ # Core method that adds a row to the logs and gets called once per each step walked.
117
+ # The marks are the logging points by the walking step which are 'marked' to be recorded.
118
+ def log_forward
119
+ marks = tracker.marks_to_advance
120
+ marks.each_index do |i|
121
+ tracker.log_forward marks[i]
122
+ prepare_expression_buffers(marks, i) if @has_expressions
123
+ entries.each_value { |e| e.register(marks, i) }
124
+ end
125
+ if tracker.dense_output?
126
+ term_entries.each do |et|
127
+ already_covered = marks.empty? ? 0 : et.term_obj.last_interpol
128
+ et.term_obj.lagging_change += et.term_obj.step_change - already_covered
129
+ end
130
+ end
131
+ tracker.last_mark = tracker.current_logging_point
132
+ end
133
+
134
+ # Right now I have no way to get the variables from a Keisan AST so if there is an expression then I calculate
135
+ # the values of all variables and put them in the buffer for the correct expression evaluation.
136
+ # TODO: Find a way to extract the variables of the AST to limit only to the ones necessary.
137
+ def prepare_expression_buffers(marks, index)
138
+ syms.rate_variables.each_value do |v|
139
+ v.buffer_value = system.stepper.variable_log_value(v, marks, index)
140
+ end
141
+ hash = syms.syms_buffers_hash
142
+ entries.values.select { |le| le.instance_of?(ExpressionLogEntry) }
143
+ .each { |le| le.buffer_hash = hash }
144
+ end
145
+
146
+ # Once simulation is over, records all loggable elements to a csv file in the order they were added.
147
+ def save_to_file(file_name)
148
+ CSV.open(file_name, 'w', headers: true) do |csv|
149
+ csv << entries.keys
150
+ (0..tracker.current_logging_step).each do |row|
151
+ csv << entries.map { |_, v| v[row] }
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibun
4
+ # TODO
5
+ # Add more functionality to put the time steps in actual time units
6
+ # Add capability to detect and stop the simulation when a variable becomes stationary.
7
+ # Use Ractors in the Stepper to optimize
8
+
9
+ # Class to integrate systems of ordinary differential equations by numerical methods.
10
+ # Used mostly to integrate runs using Runge-Kutta or Dormand-Prince methods.
11
+ # From here on DES => Differential Equation System.
12
+ # @author Javier Oswaldo Hernández Batis
13
+ class DifferentialEquationSystem
14
+ # Holds the SymbolGroup object which contains the variables and parameters of the DES.
15
+ attr_accessor :syms
16
+ # Holds the stepper object which executes the integration steps.
17
+ attr_accessor :stepper
18
+ # Holds the logger objects which is in charge of logging the variable and other values taken at each step.
19
+ attr_accessor :logger
20
+ # Holds the step tracker object which keeps track of the independent variable and step positions through the
21
+ # integration
22
+ attr_accessor :tracker
23
+ # Global boolean which can display useful data for debugging.
24
+ attr_accessor :debug_mode
25
+ alias debug_mode? debug_mode
26
+
27
+ # Adds the four inner objects for reference and manipulation and time as the default independent variable.
28
+ def initialize
29
+ @tracker = StepTracker.new
30
+ @syms = SymbolGroup.new(self)
31
+ @logger = DifferentialEquationLogger.new(self, @tracker, @syms)
32
+ @stepper = DifferentialStepper.new(self, @tracker, @syms)
33
+
34
+ @debug_mode = false
35
+ add_independent 'time', 't', 'Time'
36
+ end
37
+
38
+ # Basic method to add a dependent variable to the DES.
39
+ # @param name [String] Name to use for accessing the variable in the variables hash and through the generated
40
+ # accessor method. Use caveats for method naming (no dashes or spaces, etc). If only one ordered argument is
41
+ # passed, the symbol will get the same value as the variable name and no accessor method will be left.
42
+ # @param symbol [String] String used in formulas for Keisan. Recommended to be short as in 't' for time.
43
+ # @param title [String] Longer name which could be used for presentation and printing which unlike name has no
44
+ # restrictions.
45
+ # @param unit [String] The units of the variable, for printing and reporting.
46
+ # @param type [Symbol] A custom type that can be added for variable grouping; for example :concentration for
47
+ # substance concentrations in reactor systems.
48
+ # @param rate_function [String] The string that contains the mathematical formula for the derivative of the variable.
49
+ # Will be used by Keisan. Use regular Ruby syntax. For example if the differential equation is
50
+ # dq / dt = 2 t**2 - 3 * q, introduce '2 t**2 - 3 * q'
51
+ # @param atol [Float] The absolute tolerance for error to be used in adaptive method. At least one dependant variable
52
+ # must have a nonzero value for either absolute or relative tolerance.
53
+ # @param rtol [Float] The relative tolerance for error to be used in adaptive method.
54
+ # @param accessor [Boolean] Controls if an accessor method is created for the variable using its name argument.
55
+ # @return [DifferentialSystemVariable] The variable created.
56
+ # @example Add a Volume variable to represent a basin volume
57
+ # add_variable 'volume', 'V', 'Basin Volume', 'm3', rate_function: 'F - eV'
58
+ def add_variable(name, symbol = nil, title = nil, unit = nil, type: nil, rate_function: nil, atol: 0, rtol: 0,
59
+ accessor: true)
60
+
61
+ symbol_provided = !symbol.nil?
62
+ symbol = name unless symbol_provided
63
+ var = DifferentialSystemVariable.new(name: name, symbol: symbol, title: title,unit: unit, type:, rate_function:,
64
+ atol:, rtol:)
65
+ variables.store name.downcase, var
66
+ return unless accessor && symbol_provided
67
+
68
+ instance_variable_set "@#{name.downcase}", var
69
+ define_singleton_method name.downcase do
70
+ instance_variable_get "@#{name.downcase}"
71
+ end
72
+ var
73
+ end
74
+
75
+ # Method to edit the independent variable, which is created automatically.
76
+ # @param var_name [String] Name to use for accessing the variable in the variables hash and through the generated
77
+ # accessor method. Use caveats for method naming (no dashes or spaces, etc). If only one ordered argument is
78
+ # passed, the symbol will get the same value as the variable name and no accessor method will be left.
79
+ # @param symbol [String] String used in formulas for Keisan. Recommended to be short as in 't' for time.
80
+ # @param title [String] Longer name which could be used for presentation and printing which unlike name has no
81
+ # restrictions.
82
+ # @param unit [String] The units of the variable, for printing and reporting.
83
+ # @param accessor [Boolean] If false, removes the accesor method.
84
+ # @example Changing to the usual x for x and y differential equations.
85
+ # edit_independent_variable 'x'
86
+ def edit_independent_variable(var_name, symbol = nil, title = nil, unit = nil, accessor: true)
87
+ symbol_provided = !symbol.nil?
88
+ ind = syms.ind_var
89
+ old_key = variables.key(ind)
90
+ symbol = var_name unless symbol_provided
91
+ ind.name, ind.symbol, ind.title, ind.unit = var_name, symbol, title, unit
92
+ variables[var_name] = variables.delete old_key
93
+ singleton_class.undef_method(old_key)
94
+ return unless accessor && symbol_provided
95
+
96
+ instance_variable_set "@#{var_name.downcase}", ind
97
+ define_singleton_method var_name.downcase do
98
+ instance_variable_get "@#{var_name.downcase}"
99
+ end
100
+ end
101
+
102
+ # Basic method to add a dependent variable to the DES.
103
+ # @param name [String] Name to use for accessing the variable in the variables hash and through the generated
104
+ # accessor method. Use caveats for method naming (no dashes or spaces, etc). If only one ordered argument is
105
+ # passed, the symbol will get the same value as the variable name and no accessor method will be left.
106
+ # @param symbol [String] String used in formulas for Keisan. Recommended to be short as in 't' for time.
107
+ # @param title [String] Longer name which could be used for presentation and printing which unlike name has no
108
+ # restrictions.
109
+ # @param unit [String] The units of the variable, for printing and reporting.
110
+ # @param value [Numeric] Allows to set the value of the parameter.
111
+ # @param accessor [Boolean] Controls if an accessor method is created for the variable using its name argument.
112
+ # @return [DifferentialSystemParameter] The parameter created.
113
+ # @example Add a rate coefficient ára,eter-
114
+ # add_parameter 'rate_coefficient', 'r', 'Rate Coefficient', 'h-1', value: 0.52
115
+ def add_parameter(name, symbol = nil, title = nil, unit = nil, value: nil, accessor: true)
116
+ symbol_provided = !symbol.nil?
117
+ symbol = name unless symbol_provided
118
+ par = DifferentialSystemParameter.new name: name, symbol: symbol, title: title,
119
+ unit: unit, value: value
120
+ parameters.store name.downcase, par
121
+ return unless accessor && symbol_provided
122
+
123
+ instance_variable_set("@#{name.downcase}", par)
124
+ define_singleton_method name.downcase do
125
+ instance_variable_get "@#{name.downcase}"
126
+ end
127
+ par
128
+ end
129
+
130
+ # Shortcut method which adds multiple variable in quick form (name identical to symbol, no accessor) along their
131
+ # rate variable.
132
+ # @param vars_hash [Hash{String => String}] The hash with the variable names as keys and rate function as values.
133
+ # @example
134
+ # add_variables({'y' => '3 * t - 2', 'z' => '2 * y - t'})
135
+ def add_variables(vars_hash)
136
+ vars_hash.each { |k, v| add_variable k, rate_function: v }
137
+ end
138
+
139
+ # Similar to add_variables, add multiple parameters in quick style (name identical to symbol, no accessor) along
140
+ # their values.
141
+ # @param pars_hash [Hash{String => Numeric}] The hash with the parameters names and values.
142
+ # @example
143
+ # add_parameters({ a => 3.5, b => 3/5r })
144
+ def add_parameters(pars_hash)
145
+ pars_hash.each { |k, v| add_parameter k, value: v }
146
+ end
147
+
148
+ # Shortcut writer method to edit the step_size attribute of the tracker object.
149
+ # @param step_size [Numeric] the width of the step_size.
150
+ def step_size=(step_size)
151
+ tracker.step_size = step_size
152
+ end
153
+
154
+ # Shortcut writer method to edit the duration attribute of the step_tracker object.
155
+ # @param duration [Numeric] the total duration or timespan of the integration.
156
+ def duration=(duration)
157
+ tracker.duration = duration
158
+ end
159
+
160
+ # Shortcut method to add uniform tolerances to all dependent variable. Not recommended if variables are of
161
+ # different units or priority. See {#add_variable}
162
+ def set_global_tolerance(atol = 0, rtol = 0)
163
+ rate_variables.each { |v| v.atol = atol; v.rtol = rtol }
164
+ end
165
+
166
+ # Way to assign values for both parameters and variables for the start of the run. Shortcut to the method of the
167
+ # syms oject.
168
+ # @param values_hash [Hash{String => Numeric}] Hash with the variable and parameter names and their intended values.
169
+ # @example
170
+ # des.assign_values({ 'time' => 0, 'pressure' => 0.5, 'temperature' => 273.15 })
171
+ def assign_values(values_hash)
172
+ syms.assign_values(values_hash)
173
+ end
174
+
175
+ # Shortcut method to the logger method +add_variables+.
176
+ def add_variables_to_log(vars_decimals)
177
+ logger.add_variables(vars_decimals)
178
+ end
179
+
180
+ # Shortcut method to +Logger#add_all+ log all variables and optionally their rate terms.
181
+ def log_all(decimals, with_terms: false)
182
+ logger.add_all(decimals, with_terms:)
183
+ end
184
+
185
+ # Shortcut method to access the variables of the _syms_ object.
186
+ # @return [Hash{String => DifferentialSystemVariable}] The hash with the variables.
187
+ # @example
188
+ # puts des.variables['time'].value
189
+ def variables = syms.variables
190
+ # Shortcut method to acess the parameters of the _syms_ object.
191
+ # @return [Hash{String => DifferentialSystemParameter}]
192
+ def parameters = syms.parameters
193
+ # Checks if the prepare_integration method has already been called. Because the _current_step_ attribute is only set
194
+ # until the method is called, if it is not nil it means the DES is already set. See {#prepare_integration}
195
+ def ready? = !tracker.current_step.nil?
196
+ # Checks if the adpative method is in use.
197
+ def adaptive? = stepper.butcher_table.adaptive?
198
+ # Shortcut to get the logs
199
+ def logs = logger.logs
200
+
201
+ # The main exposed method to perform the numerical integration through a whole range (duration or timespan).
202
+ # @param starting_values[Hash{String => Numeric}] Provides a last chance to introduce all values to the variables
203
+ # and parameters. See {#assign_values}. The independent variable is set to zero if not provided. Every other
204
+ # variable or parameter with nil value will raise an error.
205
+ # @param step_size[Numeric] Allows to pass the step_size to be used (for adaptive methods, on the first one at
206
+ # least). The step size can be set beforehand.
207
+ # @param duration[Numeric] Allows to pass the duration or timespan of the integration.
208
+ # @param method[Symbol] Specifies a preset method. Currently :rk4, :midpoint and :dopr45 (adpative) are available.
209
+ # It will default to :4rk and :dopr45 for non adaptive and adaptive unless a custom butcher table has been
210
+ # previously set in the _stepper.butcher_table_ object.
211
+ # @param adaptive[Boolean] Chooses if an adpative step size or non adaptive is going to be used.
212
+ # @param display_progress[Boolean] Controls whether a progress bar is displayed in CLI or not.
213
+ def integrate(starting_values: nil, step_size: nil, duration: nil, method: nil, adaptive: false,
214
+ display_progress: true)
215
+ prepare_integration(starting_values:, step_size:, duration:, method:, adaptive:) unless ready?
216
+ progress_bar = Formatador::ProgressBar.new(tracker.total_steps, color: 'light_green') if display_progress
217
+ while syms.ind_var.value < tracker.ending_point
218
+ unitary_step
219
+ progress_bar.increment if display_progress
220
+ end
221
+ self
222
+ end
223
+
224
+ # Performs only a single step of the integration. Might generate several logging points when using dense output.
225
+ # see {#integrate} for parameters.
226
+ def single_step(starting_values: nil, step_size: nil, duration: nil, method: nil, adaptive: false)
227
+ prepare_integration(starting_values:, step_size:, duration:, method:, adaptive:, single_step: true) unless ready?
228
+ unitary_step
229
+ self
230
+ end
231
+
232
+ # Converts to JSON object
233
+ # @return [json] The json object.
234
+ def to_json(*_args)
235
+ { 'variables' => syms.rate_variables.values,
236
+ 'parameters' => syms.parameters.values,
237
+ 'logging' => logger.entries.values,
238
+ 'values' => syms.variables.transform_values(&:value),
239
+ 'simulation_options' => tracker }.to_json
240
+ end
241
+
242
+ # Converts to TOML object. So far there is no satisfactory way to handle null data. Tomlib converts nil to "" so
243
+ # loading it back with _from_toml_ may cause error.
244
+ def to_toml
245
+ Tomlib.dump(JSON.parse(to_json))
246
+ end
247
+
248
+ # Parses a JSON and loads all its data into the DES object. Useful to store integration metadata and load it later.
249
+ # @return [DifferentialEquationSystem] The DES object itself.
250
+ def from_json(data)
251
+ from_serial(JSON.parse(data))
252
+ end
253
+
254
+ # Parses a JSON and loads all its data into the DES object. Useful to store integration metadata and load it later.
255
+ # @return [DifferentialEquationSystem] The DES object itself.
256
+ def from_toml(data)
257
+ from_serial(Tomlib.load(data))
258
+ end
259
+
260
+ # Prints to stdout a summary of the DES.
261
+ def formatador_print
262
+ f = Formatador.new
263
+ f.display_line 'Variables'
264
+ vars = variables.values.reduce([]) do |ar, v|
265
+ ar.push({ symbol: v.symbol, name: v.name, unit: v.unit })
266
+ end
267
+ f.display_compact_table vars, %i[symbol name unit]
268
+ f.display_line 'Parameters'
269
+ params = parameters.values.reduce([]) do |ar, v|
270
+ ar.push({ symbol: v.symbol, name: v.name, unit: v.unit, value: v.value })
271
+ end
272
+ f.display_compact_table params, %i[symbol name value unit]
273
+ f.display_line 'Equations'
274
+ eqs = []
275
+ syms.rate_equations.each do |n, e|
276
+ if e.terms.empty?
277
+ eqs.push({ equation: n, formula: e.rate_function })
278
+ else
279
+ e.terms.each { |sk, sv| eqs.push({ variable: n, term: sk, formula: sv.rate_function }) }
280
+ end
281
+ end
282
+ f.display_compact_table eqs, %i[variable term formula]
283
+ end
284
+
285
+ private
286
+
287
+ # This is the core internal method that represents the traversing of one step.
288
+ def unitary_step
289
+ walk_step
290
+ append_log
291
+ change_variables
292
+ end
293
+
294
+ # Adds the independent variable. Kept private to avoid external libraries being able to add independent variables.
295
+ def add_independent(name, symbol = nil, title = nil)
296
+ symbol = name if symbol.nil?
297
+ variables.store name.downcase,
298
+ instance_variable_set("@#{name.downcase}",
299
+ DifferentialSystemVariable.new(name: name, symbol: symbol, title: title,
300
+ independent: true))
301
+ define_singleton_method name.downcase do
302
+ instance_variable_get "@#{name.downcase}"
303
+ end
304
+ end
305
+
306
+ # Routine shared by #from_json and #from_toml
307
+ def from_serial(poro)
308
+ poro['variables'].each do |v|
309
+ add_variable v['name'], v['symbol'], v['title'], v['unit'], rate_function: v['rate_function']
310
+ var = variables[v['name']]
311
+ if v['rate_function']
312
+ var.rate_equation.rate_function = v['rate_function']
313
+ else
314
+ v['terms'].each { |h| var.rate_equation.add_rate_term(h['name'], h['rate_function']) }
315
+ end
316
+ end
317
+ poro['parameters'].each do |p|
318
+ add_parameter p['name'], p['symbol'], p['title'], p['unit'], value: p['value']
319
+ end
320
+ poro['logging'].each do |l|
321
+ case l['type']
322
+ when 'variable'
323
+ logger.add_variables({ l['variable'] => l['decimals'] })
324
+ when 'term'
325
+ logger.add_rate_term l['variable'], l['term'], l['decimals']
326
+ when 'step'
327
+ logger.add_step_state
328
+ when 'expression'
329
+ logger.add_expression l['name'], l['formula'], l['decimals']
330
+ end
331
+ end
332
+ assign_values poro['values']
333
+ tracker.step_size = poro['simulation_options']['step_size']
334
+ tracker.duration = poro['simulation_options']['duration']
335
+ tracker.logging_interval = poro['simulation_options']['logging_interval']
336
+ self
337
+ end
338
+
339
+ # Major method to set up all instance variable and objects before starting to perform steps. Single step is
340
+ # included to discriminate whether a duration is necessary or not.
341
+ def prepare_integration(starting_values: nil, step_size: nil, duration: nil, method: nil, adaptive: false,
342
+ single_step: false)
343
+ assign_values({ syms.ind_var.name => 0 }) if syms.ind_var.value.nil?
344
+ assign_values starting_values unless starting_values.nil?
345
+ tracker.step_size = step_size unless step_size.nil?
346
+ tracker.duration = duration unless duration.nil?
347
+ method ||= (adaptive ? :dopr45 : :rk4)
348
+ stepper.prepare(method:)
349
+ tracker.prepare(syms.ind_var.value, stepper.butcher_table.adaptive?, single_step: single_step)
350
+ logger.prepare
351
+ end
352
+
353
+ # Method which calls the major core method which performs a single step.
354
+ def walk_step
355
+ stepper.walk_step tracker.step_size
356
+ tracker.walk_forward
357
+ end
358
+
359
+ def change_variables
360
+ variables.each_value { |v| v.value += v.step_change; v.buffer_value = v.value }
361
+ end
362
+
363
+ def append_log
364
+ logger.log_forward
365
+ end
366
+ end
367
+ end