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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +26 -0
- data/README.md +909 -0
- data/lib/bibun/differential_butcher_table.rb +72 -0
- data/lib/bibun/differential_equation_logger.rb +157 -0
- data/lib/bibun/differential_equation_system.rb +367 -0
- data/lib/bibun/differential_stepper.rb +237 -0
- data/lib/bibun/differential_system_parameter.rb +21 -0
- data/lib/bibun/differential_system_symbol.rb +35 -0
- data/lib/bibun/differential_system_variable.rb +202 -0
- data/lib/bibun/log_entry.rb +131 -0
- data/lib/bibun/step_tracker.rb +129 -0
- data/lib/bibun/symbol_group.rb +100 -0
- data/lib/bibun/version.rb +6 -0
- data/lib/bibun.rb +22 -0
- data/lib/data/butcher_tables.toml +44 -0
- data.tar.gz.sig +0 -0
- metadata +255 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'differential_butcher_table'
|
|
4
|
+
|
|
5
|
+
module Bibun
|
|
6
|
+
class DifferentialEquationSystem
|
|
7
|
+
# This class handles the inner process of numerical calculations to perform each integration step and set
|
|
8
|
+
# the changes to add to the variables to udpate them to the next step.
|
|
9
|
+
class DifferentialStepper
|
|
10
|
+
# @return [Bibun::DifferentialButcherTable] The butcher table with the info to perform the Runge-Kutta
|
|
11
|
+
# integration.
|
|
12
|
+
attr_accessor :butcher_table
|
|
13
|
+
# @return [Bibun::DifferentialEquationSystem] Reference to the outer DES object.
|
|
14
|
+
attr_accessor :system
|
|
15
|
+
# @return [Bibun::DifferentialEquationSystem::SymbolGroup] Reference to the SymbolGroup object of the outer DES
|
|
16
|
+
# object.
|
|
17
|
+
attr_accessor :syms
|
|
18
|
+
# @return [Bibun::DifferentialEquationSystem::StepTracker] Reference to the step tracker of the outer DES object.
|
|
19
|
+
attr_accessor :tracker
|
|
20
|
+
# @return [Array<Keisan::Calculator>] Array with the calculators for each formula of the interpolation (this is
|
|
21
|
+
# necessary to be able to cache the AST for each formula and improve performance).
|
|
22
|
+
attr_accessor :dense_calculators
|
|
23
|
+
# @return [Hash] A nested hash used to pass the values to log to the logger object.
|
|
24
|
+
attr_accessor :terms_log
|
|
25
|
+
|
|
26
|
+
# Basic initialization. +StepTracker+ and +GroupSymbol+ objects are assinged for easier access.
|
|
27
|
+
def initialize(parent_system, tracker, syms)
|
|
28
|
+
@system = parent_system
|
|
29
|
+
@tracker = tracker
|
|
30
|
+
@syms = syms
|
|
31
|
+
@butcher_table = DifferentialButcherTable.new
|
|
32
|
+
@adaptive = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Preparation prior the taking the first step.
|
|
36
|
+
def prepare(method: nil)
|
|
37
|
+
set_buffers
|
|
38
|
+
butcher_table.load_preset(method:) if butcher_table.empty?
|
|
39
|
+
return unless butcher_table.adaptive?
|
|
40
|
+
|
|
41
|
+
raise 'No tolerances provided for adaptive integration' unless syms.tolerances_provided?
|
|
42
|
+
|
|
43
|
+
set_interpolation if dense_output?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Quick way to check if dense output has to be given.
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def dense_output? = butcher_table.adaptive? && !tracker.logging_interval.nil?
|
|
49
|
+
|
|
50
|
+
# Step for explicit Runge-Kutta methods
|
|
51
|
+
def walk_step(step_size)
|
|
52
|
+
if butcher_table.adaptive?
|
|
53
|
+
adaptive_loop(step_size)
|
|
54
|
+
else
|
|
55
|
+
straightforward_step(step_size)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Performs a single step, resulting in new values assigned to +DifferentialSystemVariable.step_size+
|
|
60
|
+
def straightforward_step(step_size, alternate_weights: false)
|
|
61
|
+
estimate_k_steps(step_size)
|
|
62
|
+
combine_with_weighted(step_size, butcher_table.weights)
|
|
63
|
+
combine_with_weighted(step_size, butcher_table.alternate_weights, alternate: true) if alternate_weights
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Loop for adaptive methods, which results in new values of +DifferentialSystemVariable.step_size+ and a
|
|
67
|
+
# new +StepTracker.step_size+
|
|
68
|
+
def adaptive_loop(step_size)
|
|
69
|
+
runner_step_size = step_size
|
|
70
|
+
acceptable_error = false
|
|
71
|
+
i = 0
|
|
72
|
+
until acceptable_error
|
|
73
|
+
i += 1
|
|
74
|
+
# First run
|
|
75
|
+
straightforward_step(runner_step_size, alternate_weights: true)
|
|
76
|
+
result = evaluate_error
|
|
77
|
+
runner_step_size *= result[:factor].clamp(1/5r, 10) # Suggestion by W.H. Press
|
|
78
|
+
if result[:err] <= 1
|
|
79
|
+
acceptable_error = true
|
|
80
|
+
else
|
|
81
|
+
# Perform again with lower step size, do not correct step_size again.
|
|
82
|
+
# Undo the change done from the previous, wrong step size.
|
|
83
|
+
syms.undo_term_changes
|
|
84
|
+
straightforward_step(runner_step_size, alternate_weights: true)
|
|
85
|
+
if evaluate_error[:err] <= 1
|
|
86
|
+
acceptable_error = true
|
|
87
|
+
else
|
|
88
|
+
# Adjust again step size for next iteration of the loop.
|
|
89
|
+
syms.undo_term_changes
|
|
90
|
+
runner_step_size *= result[:factor].clamp(1/5r, 10)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
raise 'Too many adapations' if i >= 5
|
|
94
|
+
end
|
|
95
|
+
tracker.next_step_size = runner_step_size
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Provides to the logger the variable value that correspond to the current logging point.
|
|
99
|
+
# The leaps array and index are both provided to be able to assess the relative position of the index.
|
|
100
|
+
# @return [Numeric] The value to log for the variable.
|
|
101
|
+
def variable_log_value(variable, leaps, index)
|
|
102
|
+
if dense_output?
|
|
103
|
+
dense_interpolation(variable, leaps[index])
|
|
104
|
+
else
|
|
105
|
+
variable.value + variable.step_change
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Provides to the logger the rate additive term value that correspond to the current logging point.
|
|
110
|
+
# The leaps array and index are both provided to be able to assess the relative position of the index.
|
|
111
|
+
# @return [Numeric] The value to log for the rate additive term.
|
|
112
|
+
def term_log_value(term, marks, index)
|
|
113
|
+
if dense_output?
|
|
114
|
+
term_dense_accumulation(term, marks, index)
|
|
115
|
+
else
|
|
116
|
+
term.report_and_reset
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# This routine records the amount of change that keeps unregistered at the end of each walking step and adds it
|
|
121
|
+
# to the first logging step of the next walking step. Also ensures only the change caused by each individual
|
|
122
|
+
# logging step is provided for those beyond the first.
|
|
123
|
+
# @return [Numeric] The net amount of change due to the rate additive term for dense interpolation.
|
|
124
|
+
def term_dense_accumulation(term, marks, index)
|
|
125
|
+
value = 0
|
|
126
|
+
interpol = dense_interpolation(term, marks[index])
|
|
127
|
+
value += interpol
|
|
128
|
+
if index == 0
|
|
129
|
+
value += term.lagging_change
|
|
130
|
+
term.lagging_change = 0
|
|
131
|
+
else
|
|
132
|
+
value -= term.last_interpol
|
|
133
|
+
end
|
|
134
|
+
term.last_interpol = interpol
|
|
135
|
+
value
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Set the variable arrays for the k values (k_steps)
|
|
141
|
+
def set_buffers
|
|
142
|
+
stages = butcher_table.stages
|
|
143
|
+
syms.rate_variables.each_value do |v|
|
|
144
|
+
v.k_steps = [*0..stages]
|
|
145
|
+
end
|
|
146
|
+
syms.terms_array.each { |s| s.k_steps = [*0..stages] }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Calculates the k values of the Runge-Kutta methods.
|
|
150
|
+
def estimate_k_steps(step_size)
|
|
151
|
+
stages = (0..butcher_table.stages)
|
|
152
|
+
stages.each do |s|
|
|
153
|
+
calculate_stage(step_size, s, last: s == stages.max)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Each individual k value calculation. NOTE: here the calculation k = h * F(t + ch, y + Σ(a_is)) instead of
|
|
158
|
+
# k = F(t + ch, y + Σ(a_is)) to resemble books on computer numerical methdos like Numerical Recipes.
|
|
159
|
+
# Effect: this updates the +k_steps+ values of +DifferentialSystemVariable+ and +DifferentialTerm+ and the
|
|
160
|
+
# buffer value for the next stage.
|
|
161
|
+
def calculate_stage(step_size, stage, last: false)
|
|
162
|
+
syms.ind_var.buffer_value = syms.ind_var.value + butcher_table.nodes[stage] * step_size
|
|
163
|
+
syms.rate_variables.each_value do |v|
|
|
164
|
+
v.k_steps[stage] = step_size * v.rate_equation.derivative_value(syms.syms_buffers_hash, stage)
|
|
165
|
+
v.terms.each_value { |tv| tv.k_steps[stage] *= step_size }
|
|
166
|
+
end
|
|
167
|
+
return if last
|
|
168
|
+
|
|
169
|
+
syms.rate_variables.each_value do |v|
|
|
170
|
+
v.buffer_value = v.value + (0..stage).map { |i|
|
|
171
|
+
butcher_table.matrix_coefficients[stage][i] * v.k_steps[i]
|
|
172
|
+
}.sum
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Performs the sum pondered with the Butcher table weights for the ks.
|
|
177
|
+
# Effect: updates +DifferentialSystemVariable.StepChange+ or +DifferentialSystemVariable.alternate_step_change+,
|
|
178
|
+
# depending on the +alternate+ parameter.
|
|
179
|
+
def combine_with_weighted(step_size, weights, alternate: false)
|
|
180
|
+
writer = alternate ? :alternate_step_change= : :step_change=
|
|
181
|
+
syms.rate_variables.each_value do |v|
|
|
182
|
+
v.send writer, v.k_steps[0..(weights.size - 1)].zip(weights)
|
|
183
|
+
.reduce(0) { |sum, e| sum + e[0] * e[1] }
|
|
184
|
+
end
|
|
185
|
+
# Calculation of separate term contributions is unnecessary for lesser order estimations
|
|
186
|
+
unless alternate
|
|
187
|
+
syms.terms_array.each do |t|
|
|
188
|
+
term_contribution = t.k_steps[0..weights.size].zip(weights).reduce(0) { |sum, e| sum + e[0] * e[1] }
|
|
189
|
+
t.register_step_change(term_contribution)
|
|
190
|
+
t.variable.term_changes[t.name] = term_contribution
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
syms.ind_var.send writer, step_size
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# For adaptive methods, evaluates the error from the difference in estimation between the two weight sets
|
|
197
|
+
# of varying order considering the tolerances provided.
|
|
198
|
+
# @return [Hash{Symbol => Numeric}] A hash with the err index to judge whether to increase or reduce the step
|
|
199
|
+
# size and the factor by which the current step has to be multiplied.
|
|
200
|
+
def evaluate_error
|
|
201
|
+
vars = syms.vars_with_tols
|
|
202
|
+
# err calculation according to W. H. Press
|
|
203
|
+
relative_sq_sum = vars.values.reduce(0) do |acum, v|
|
|
204
|
+
acum + (v.delta / v.scale.to_f)**2
|
|
205
|
+
end
|
|
206
|
+
err = Math.sqrt(relative_sq_sum / vars.size)
|
|
207
|
+
# 0.9 is a recommended factor by Butcher
|
|
208
|
+
factor = 0.9 * (1 / err)**(1 / butcher_table.order.to_f)
|
|
209
|
+
{ err: err, factor: factor }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Part of the preparation which loads calculators for the interpolation functions.
|
|
213
|
+
def set_interpolation
|
|
214
|
+
@dense_calculators = []
|
|
215
|
+
butcher_table.interpolation_formulas.length.times { @dense_calculators.push Keisan::Calculator.new(cache: true) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Calculates the interpolated value of the variables or terms given the "slot", that is, the ordinal of the log
|
|
219
|
+
# for the given step.
|
|
220
|
+
# @param sym [DifferentialSystemVariable] The variable whose value is going to be calculated.
|
|
221
|
+
# @param point [Numeric] The independent variable value of the row to be recorded within the step.
|
|
222
|
+
def dense_interpolation(sym, point)
|
|
223
|
+
return tracker.current_logging_point if sym.instance_of?(DifferentialSystemVariable) && sym.independent?
|
|
224
|
+
|
|
225
|
+
data = sym.interpolator_hash.merge(butcher_table.interpolation_constants)
|
|
226
|
+
dense_calculators[0..-2].each_with_index do |c, j|
|
|
227
|
+
r_coef = c.evaluate(butcher_table.interpolation_formulas["r#{j + 1}"], data)
|
|
228
|
+
data.store "r#{j + 1}".to_sym, r_coef
|
|
229
|
+
end
|
|
230
|
+
data.store :theta, tracker.theta(point)
|
|
231
|
+
coef_keys = %i[r1 r2 r3 r4 r5 theta]
|
|
232
|
+
poly_coefs = data.slice(*coef_keys)
|
|
233
|
+
dense_calculators.last.evaluate(butcher_table.interpolation_formulas['final'], poly_coefs)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'differential_system_symbol'
|
|
4
|
+
|
|
5
|
+
module Bibun
|
|
6
|
+
# Parameters lack buffer values and logs, but can take a reference interval to retrieve at random
|
|
7
|
+
class DifferentialSystemParameter < Bibun::DifferentialSystemSymbol
|
|
8
|
+
attr_accessor :reference_data
|
|
9
|
+
|
|
10
|
+
# In progress. A ReferenceData class is in mind to be able to pass reference ranges.
|
|
11
|
+
def sample
|
|
12
|
+
@value = @reference_data.random_value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# json serializer
|
|
16
|
+
def to_json(*_args)
|
|
17
|
+
{ 'name' => name, 'symbol' => symbol, 'title' => title, 'unit' => unit, 'value' => value }.to_json
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bibun
|
|
4
|
+
# This class is not used directly, but implements behaviour common to DifferentialSystemVariable and
|
|
5
|
+
# DifferentialSystemParameter. Here 'symbol' refers to either a parameter or a variable.
|
|
6
|
+
class DifferentialSystemSymbol
|
|
7
|
+
# @return [String] The name of the symbol which is referenced in hashes and accessor methods.
|
|
8
|
+
attr_accessor :name
|
|
9
|
+
# @return [String] The string, usually short, which is used in string formulas for _Keisan::Calculator_
|
|
10
|
+
attr_accessor :symbol
|
|
11
|
+
# @return [String] A string to designate the variable for printing, so it can have spaces, dashes, etc.
|
|
12
|
+
attr_accessor :title
|
|
13
|
+
# @return [String] A short string to document the units of the symbol
|
|
14
|
+
attr_accessor :unit
|
|
15
|
+
# @return [Numeric] The numeric value which the symbol holds at one given moment.
|
|
16
|
+
attr_accessor :value
|
|
17
|
+
|
|
18
|
+
# Regular initialization.
|
|
19
|
+
def initialize(name:, symbol:, title: nil, value: nil, unit: nil)
|
|
20
|
+
@name = name
|
|
21
|
+
@symbol = symbol
|
|
22
|
+
@title = title
|
|
23
|
+
@value = value
|
|
24
|
+
@unit = unit
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Method to express the current value of the symbol with units (e.g. "30 m")
|
|
28
|
+
# @param significant_figures[Integer] The decimal places to round the value of the symbol.
|
|
29
|
+
# @return [String] The value with units.
|
|
30
|
+
def with_units(significant_figures = nil)
|
|
31
|
+
quantity = significant_figures.nil? ? value : value.round(significant_figures).to_s
|
|
32
|
+
"#{quantity} #{@unit}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bibun
|
|
4
|
+
# Class to implement variables (in this library, variables change their value as the independent variable change,
|
|
5
|
+
# parameters keep their value fixed across the simulation)
|
|
6
|
+
class DifferentialSystemVariable < Bibun::DifferentialSystemSymbol
|
|
7
|
+
|
|
8
|
+
# Reference to the +SubDifferentialEquation+ object that contains the function for its derivative.
|
|
9
|
+
attr_accessor :rate_equation
|
|
10
|
+
# Adds a custom type which can be useful for grouping variables and subclassing.
|
|
11
|
+
attr_accessor :type
|
|
12
|
+
# Reference to the quantity change that is caused by a integration step and which is eventually added to the value.
|
|
13
|
+
attr_accessor :step_change
|
|
14
|
+
# For adaptive embedded methods, it stores the quantity change calculated with the alternative set of weights of
|
|
15
|
+
# a different order in order to calculate the error of the intergration step.
|
|
16
|
+
attr_accessor :alternate_step_change
|
|
17
|
+
# This is hash with the quantity changes caused by each of the rate additive terms, when those are defined.
|
|
18
|
+
# Their sum equals the step change.
|
|
19
|
+
attr_accessor :term_changes
|
|
20
|
+
# Absolute tolerance to consider for adpative methods.
|
|
21
|
+
attr_accessor :atol
|
|
22
|
+
# Relative tolerance to consider for adaptive methods.
|
|
23
|
+
attr_accessor :rtol
|
|
24
|
+
# Array which holds the k quantities (k1, k2, k3, etc in Runge Kutta methods) corresponding to each stage of the
|
|
25
|
+
# Runge Kutta method. Since arrays start in zero, add 1 to be equal to the book formulas.
|
|
26
|
+
attr_accessor :k_steps
|
|
27
|
+
# Value for the variable to be taken for the evaluation of the next k and which is calcualted using the matrix
|
|
28
|
+
# coefficients a11, a21, a22, a31, a32, a33... of the Butcher Table and which does not require further storing.
|
|
29
|
+
# Also used as temporary cache for expression calculations when logging.
|
|
30
|
+
attr_accessor :buffer_value
|
|
31
|
+
# Boolean to distinguish the independent variable from the dependent variables.
|
|
32
|
+
attr_reader :independent
|
|
33
|
+
alias independent? independent
|
|
34
|
+
|
|
35
|
+
# Regular initialization.
|
|
36
|
+
def initialize(name:, symbol:, title: nil, value: nil, unit: nil, type: nil, rate_function: nil,
|
|
37
|
+
atol: nil, rtol: nil, independent: false)
|
|
38
|
+
super(name: name, symbol: symbol, title: title, value: value, unit: unit)
|
|
39
|
+
@type = type
|
|
40
|
+
@term_changes = {}
|
|
41
|
+
@rate_equation = SubDifferentialEquation.new(self) unless type == :independent_variable
|
|
42
|
+
@atol = atol || 0
|
|
43
|
+
@rtol = rtol || 0
|
|
44
|
+
@independent = independent
|
|
45
|
+
rate_equation.rate_function = rate_function unless rate_function.nil?
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Shortcut attribute writer for the rate function of the _SubDifferentialEquation_ object
|
|
49
|
+
# @param [String] formula The mathematical expression of the formula in native Ruby.
|
|
50
|
+
# @example
|
|
51
|
+
# variables['temperature'].rate_function = '3 * t^(1.5)'
|
|
52
|
+
def rate_function=(formula)
|
|
53
|
+
rate_equation.rate_function = formula if rate_equation.terms.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Shortcut method to adds a rate term to the _SubDifferentialEquation_ object.
|
|
57
|
+
# see {SubDifferentialEquation#add_rate_term}
|
|
58
|
+
def add_rate_term(name, formula)
|
|
59
|
+
rate_equation.add_rate_term name, formula
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Routine to convert to json.
|
|
63
|
+
def to_json(*_args)
|
|
64
|
+
basic_hash = { 'name' => name, 'symbol' => symbol, 'title' => title, 'unit' => unit }
|
|
65
|
+
rates_hash = if rate_equation.terms.empty?
|
|
66
|
+
{ 'rate_function' => rate_equation.rate_function }
|
|
67
|
+
else
|
|
68
|
+
{ 'terms' => rate_equation.terms.values }
|
|
69
|
+
end
|
|
70
|
+
basic_hash.merge(rates_hash).to_json
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Formula to evaluate error for adaptive methods. From Numerical Recipes by W. H. Press.
|
|
74
|
+
# Notice how both tolerances influence the result so that is always higher than either one of them.
|
|
75
|
+
# @return [Float] the combined value of the scale.
|
|
76
|
+
def scale = atol + rtol * [value + step_change, value + alternate_step_change].max
|
|
77
|
+
|
|
78
|
+
# Difference between the estimations of embedded methods (the actual value cancels out).
|
|
79
|
+
# @return [Float] the difference between both estimations.
|
|
80
|
+
def delta = step_change - alternate_step_change
|
|
81
|
+
|
|
82
|
+
# Quick method to check if the variable has additive rate terms defined.
|
|
83
|
+
# @return [Boolean]
|
|
84
|
+
def terms? = !rate_equation.terms.empty?
|
|
85
|
+
|
|
86
|
+
# Returns the hash of rate additive terms. Shortcut to access from the variable itself.
|
|
87
|
+
# @return [Hash{String => DifferentialTerm}] The hash with the additive terms for the variable.
|
|
88
|
+
def terms = rate_equation.terms
|
|
89
|
+
|
|
90
|
+
# Hash with the values necessary for polynomial interpolation for dense outputs. This is fed as the second
|
|
91
|
+
# argument for Keisan::Calculator#evaluate.
|
|
92
|
+
# @return [Hash{Symbol => Float}] the hash with the key value pairs.
|
|
93
|
+
def interpolator_hash
|
|
94
|
+
vals = { y0: value, y1: value + step_change }
|
|
95
|
+
k_hash = k_steps.map.with_index { |k, i| ["k#{i + 1}".to_sym, k] }.to_h
|
|
96
|
+
vals.merge(k_hash)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Class that handle the differential equation which governs the variable's rate of change. Shortcut methods on the
|
|
100
|
+
# variable make its access non essential, but are provided anyway.
|
|
101
|
+
class SubDifferentialEquation
|
|
102
|
+
attr_accessor :name, :rate_function, :terms, :current_value, :rate_variable
|
|
103
|
+
|
|
104
|
+
def initialize(parent_variable, rate_formula = nil, with_terms: false)
|
|
105
|
+
@rate_variable = parent_variable
|
|
106
|
+
@name = parent_variable.name
|
|
107
|
+
@terms = {}
|
|
108
|
+
@rate_function = rate_formula
|
|
109
|
+
@calculator = Keisan::Calculator.new(cache: true)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Method to add a rate term. Automatically turns the existing rate function into another term to avoid mixing
|
|
113
|
+
# the two.
|
|
114
|
+
# @param [String] name The name of the rate term to be used for referencing
|
|
115
|
+
# @param [String] formula The mathematical expression for the term in native Ruby.
|
|
116
|
+
def add_rate_term(name, formula)
|
|
117
|
+
unless rate_function.nil?
|
|
118
|
+
@terms.store 'base_rate', DifferentialTerm.new('base_rate', rate_function, self)
|
|
119
|
+
@rate_function = nil
|
|
120
|
+
end
|
|
121
|
+
@terms&.store name, DifferentialTerm.new(name, formula, self)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# This part evaluates the derivative for the values provided.
|
|
125
|
+
# @param [Hash{String => Numeric}] values_hash The hash with the values of the variables and parameters
|
|
126
|
+
# @param [Integer] stage The stage of the Runge-Kutta method. Used to store the value in the proper array slot.
|
|
127
|
+
def derivative_value(values_hash, stage)
|
|
128
|
+
if @terms.empty?
|
|
129
|
+
@calculator.evaluate(@rate_function, values_hash)
|
|
130
|
+
else
|
|
131
|
+
@terms.values.reduce(0) do |sum, term|
|
|
132
|
+
contr = term.contribution(values_hash)
|
|
133
|
+
term.k_steps[stage] = contr
|
|
134
|
+
sum + contr
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# It is common to have several distinct additive contributions to the derivatives of variables.
|
|
140
|
+
# This class enables to create different contributions (rate additive terms) that can be summed.
|
|
141
|
+
class DifferentialTerm
|
|
142
|
+
attr_accessor :name, :rate_function, :k_steps, :variable, :complementary, :last_mark, :last_interpol
|
|
143
|
+
# This is the change in the variable through the integration steps that is caused by the particular
|
|
144
|
+
# rate additive term. The sum of all rate additive terms is the variable step_change.
|
|
145
|
+
# @return [Float]
|
|
146
|
+
attr_accessor :step_change
|
|
147
|
+
# This attribute is necessary when a logging_interval is specified in order to track the total change
|
|
148
|
+
# between two logging points across steps.
|
|
149
|
+
# @return [Float]
|
|
150
|
+
attr_accessor :cumulative_change
|
|
151
|
+
# For dense output, tracks change not logged yet.
|
|
152
|
+
attr_accessor :lagging_change
|
|
153
|
+
|
|
154
|
+
def initialize(name, formula, equation)
|
|
155
|
+
@name = name
|
|
156
|
+
@rate_function = formula
|
|
157
|
+
@calculator = Keisan::Calculator.new(cache: true)
|
|
158
|
+
@variable = equation.rate_variable
|
|
159
|
+
@equation = equation
|
|
160
|
+
@cumulative_change, @lagging_change = 0, 0
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# This part evaluates the contribution for the values provided. See {SubDifferentialEquation#derivative_value}.
|
|
164
|
+
# Called by the parent equation on each rate additive term to sum the contributions later.
|
|
165
|
+
def contribution(values_hash)
|
|
166
|
+
@calculator.evaluate(@rate_function, values_hash)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Serializer to json for the overall DES json object.
|
|
170
|
+
def to_json(*_args)
|
|
171
|
+
{ 'name' => name, 'rate_function' => rate_function }.to_json
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Provides value for dense output polynomial interpolation. For additive terms, only the step contribution is
|
|
175
|
+
# computed so starting value is zero by default.
|
|
176
|
+
# @return [Hash{Symbol => Numeric}] The hash with key value pairs.
|
|
177
|
+
def interpolator_hash
|
|
178
|
+
vals = { y0: 0, y1: step_change }
|
|
179
|
+
k_hash = k_steps.map.with_index { |k, i| ["k#{i + 1}".to_sym, k] }.to_h
|
|
180
|
+
vals.merge(k_hash)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# When writing the step change, adds to the cumulative change in order to keep total across the steps for
|
|
184
|
+
# logging.
|
|
185
|
+
def register_step_change(amount)
|
|
186
|
+
@step_change = amount
|
|
187
|
+
@cumulative_change += amount
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Gives the total cumulative change and resets it to zero for future logging
|
|
191
|
+
def report_and_reset
|
|
192
|
+
c = @cumulative_change
|
|
193
|
+
@cumulative_change = 0
|
|
194
|
+
c
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Gives the concatenation variable_name:term_name from the object itself.
|
|
198
|
+
def id = "#{variable.name}:#{name}"
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bibun
|
|
4
|
+
class DifferentialEquationSystem
|
|
5
|
+
class DifferentialEquationLogger
|
|
6
|
+
# Class to handle each column or quantity that is set to be logged (variables, terms, custom expressions...).
|
|
7
|
+
# Generic class. Only specific subclasses are meant to be instanced.
|
|
8
|
+
# Contains attributes for all subclasses to implement polymorphism
|
|
9
|
+
class LogEntry
|
|
10
|
+
# The array with the values at each row.
|
|
11
|
+
attr_accessor :values, :stepper, :logger, :tracker, :buffer_hash, :decimals, :type
|
|
12
|
+
|
|
13
|
+
# Standard initializer. Subclasses receive the object which are meant to track and log.
|
|
14
|
+
def initialize(logger, decimals)
|
|
15
|
+
@values = []
|
|
16
|
+
@stepper = logger.system.stepper
|
|
17
|
+
@logger = logger
|
|
18
|
+
@tracker = logger.tracker
|
|
19
|
+
@decimals = decimals
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Method to add one row value to the values array.
|
|
23
|
+
def register(marks = nil, index = nil)
|
|
24
|
+
values << loggable(marks, index).round(decimals)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Shortcut method to access the values without having to type values each time.
|
|
28
|
+
def [](index)
|
|
29
|
+
values[index]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Stub method for inheriting.
|
|
33
|
+
def loggable(marks, index); end
|
|
34
|
+
|
|
35
|
+
# Basic hash to be used for serializing to JSON.
|
|
36
|
+
def basic_hash(*_args)
|
|
37
|
+
{ 'type' => @type, 'decimals' => @decimals }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Subclass for logging values of variables
|
|
42
|
+
class VariableLogEntry < LogEntry
|
|
43
|
+
attr_reader :variable
|
|
44
|
+
|
|
45
|
+
def initialize(logger, decimals, var_name)
|
|
46
|
+
super(logger, decimals)
|
|
47
|
+
@variable = var_name
|
|
48
|
+
@type = 'variable'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Asks the stepper for the value to log given the marks array and the index.
|
|
52
|
+
def loggable(marks,index) = stepper.variable_log_value(var_obj, marks, index)
|
|
53
|
+
|
|
54
|
+
# Necessary for logging the starting values
|
|
55
|
+
def register_value
|
|
56
|
+
values << logger.syms.variables[variable].value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Quick way to get the variable object rather than its name.
|
|
60
|
+
def var_obj = logger.syms.variables[variable]
|
|
61
|
+
|
|
62
|
+
# Serializer for the outer class.
|
|
63
|
+
def to_json(*_args)
|
|
64
|
+
basic_hash.merge({ 'variable' => variable }).to_json
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Subclass to log rate additive terms
|
|
69
|
+
class TermLogEntry < LogEntry
|
|
70
|
+
attr_reader :variable, :term
|
|
71
|
+
|
|
72
|
+
def initialize(logger, decimals, var_name, term_name)
|
|
73
|
+
super(logger, decimals)
|
|
74
|
+
@variable = var_name
|
|
75
|
+
@term = term_name
|
|
76
|
+
@type = 'term'
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Asks the stepper for the value to log providing the marks array and the index.
|
|
80
|
+
def loggable(marks, index)
|
|
81
|
+
stepper.term_log_value(term_obj, marks, index)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Quick way to get the rate additive term object.
|
|
85
|
+
def term_obj = logger.syms.variables[variable].terms[term]
|
|
86
|
+
|
|
87
|
+
# Serializer to json
|
|
88
|
+
def to_json(*_args)
|
|
89
|
+
basic_hash.merge({ 'variable' => variable, 'term' => term }).to_json
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Subclass for logging the current step
|
|
94
|
+
class StepLogEntry < LogEntry
|
|
95
|
+
def initialize(logger, decimals)
|
|
96
|
+
super(logger, decimals)
|
|
97
|
+
@type = 'step'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Gives the current logging step for logging.
|
|
101
|
+
def loggable(*args) = tracker.current_logging_step
|
|
102
|
+
# Serializer to JSON
|
|
103
|
+
def to_json(*_args) = basic_hash.to_json
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Subclass for logging custom math expressions.
|
|
107
|
+
class ExpressionLogEntry < LogEntry
|
|
108
|
+
# @return [String] The mathematical expression for Keisan in plain Ruby syntax.
|
|
109
|
+
attr_accessor :expression
|
|
110
|
+
# Unlike other entries, expressions require to know their name since it is custom made.
|
|
111
|
+
attr_accessor :name
|
|
112
|
+
|
|
113
|
+
def initialize(logger, decimals, name, expression)
|
|
114
|
+
super(logger, decimals)
|
|
115
|
+
@expression = expression
|
|
116
|
+
@calculator = Keisan::Calculator.new(cache: true)
|
|
117
|
+
@type = 'expression'
|
|
118
|
+
@name = name
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Gives the calculated value for the expression for logging.
|
|
122
|
+
def loggable(*args) = @calculator.evaluate(expression, buffer_hash)
|
|
123
|
+
|
|
124
|
+
# Serializer to JSON.
|
|
125
|
+
def to_json(*_args)
|
|
126
|
+
basic_hash.merge('name' => name, 'formula' => expression).to_json
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|