Skip to content

Commit 9258caa

Browse files
committed
move fit into separate module
1 parent 6ca2353 commit 9258caa

File tree

2 files changed

+168
-162
lines changed

2 files changed

+168
-162
lines changed

lib/gnuplotrb/fit.rb

Lines changed: 166 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,178 +1,182 @@
11
module GnuplotRB
22
##
3-
# ====== Overview
4-
# Fit given data with function. Covered in
5-
# {fit notebook}[http://nbviewer.ipython.org/github/dilcom/gnuplotrb/blob/master/notebooks/fitting_data.ipynb].
6-
# ====== Arguments
7-
# * *data* - method accepts the same sources as Dataset.new
8-
# and Dataset object
9-
# * *:function* - function to fit data with. Default 'a2*x*x+a1*x+a0'
10-
# * *:initials* - initial values for coefficients used in fitting.
11-
# Default: {a2: 1, a1: 1, a0: 1}
12-
# * *:term_options* - terminal options that should be setted to terminal before fit.
13-
# You can see them in Plot's documentation (or even better in gnuplot doc).
14-
# Most useful here are ranges (xrange, yrange etc) and fit option which tunes fit parameters
15-
# (see {gnuplot doc}[http://www.gnuplot.info/docs_5.0/gnuplot.pdf] p. 122).
16-
# * *options* - options passed to Gnuplot's fit such as *using*. They are covered in
17-
# {gnuplot doc}[http://www.gnuplot.info/docs_5.0/gnuplot.pdf](pp. 69-74)
18-
# ====== Return value
19-
# Fit returns hash of 4 elements:
20-
# * *:formula_ds* - dataset with best fit curve as data
21-
# * *:coefficients* - hash of calculated coefficients. So if you gave {via: [:a, :b, :c]} or
22-
# {initials: {a: 1, b: 1, c: 1} } it will return hash with keys :a, :b, :c and its values
23-
# * *:deltas* - Gnuplot calculates possible deltas for coefficients during fitting and
24-
# *deltas* hash contains this deltas
25-
# * *:data* - pointer to Datablock with given data
26-
# ====== Examples
27-
# fit(some_data, function: 'exp(a/x)', initials: {a: 10}, term_option: { xrange: 1..100 })
28-
# fit(some_dataset, using: '1:2:3')
29-
def fit(data, function: 'a2*x*x+a1*x+a0', initials: { a2: 1, a1: 1, a0: 1 }, term_options: {}, **options)
30-
dataset = data.is_a?(Dataset) ? Dataset.new(data.data) : Dataset.new(data)
31-
opts_str = OptionHandling.ruby_class_to_gnuplot(options)
32-
output = gnuplot_fit(function, dataset, opts_str, initials, term_options)
33-
res = parse_output(initials.keys, function, output)
34-
{
35-
formula_ds: Dataset.new(res[2], title: 'Fit formula'),
36-
coefficients: res[0],
37-
deltas: res[1],
38-
data: dataset
39-
}
40-
end
41-
42-
##
43-
# ====== Overview
44-
# Shortcut for fit with polynomial. Degree here is max power of *x* in polynomial.
45-
# ====== Arguments
46-
# * *data* - method accepts the same sources as Dataset.new and Dataset object
47-
# * *:degree* - degree of polynomial
48-
# * *options* - all of this options will be passed to *#fit* so you
49-
# can set here any *term_options*. If you pass here *:initials* hash, it
50-
# will be merged into default initals hash (all values are 1).
51-
# ====== Return value
52-
# See the same section for #fit.
53-
# ====== Examples
54-
# fit_poly(some_data, degree: 5, initials: { a4: 10, a2: -1 }, term_option: { xrange: 1..100 })
55-
# #=> The same as:
56-
# #=> fit(
57-
# #=> some_data,
58-
# #=> function: 'a5*x**5 + a4*x**4 + ... + a0*x**0',
59-
# #=> initals: {a5: 1, a4: 10, a3: 1, a2: -1, a1: 1, a0: 1},
60-
# #=> term_option: { xrange: 1..100 }
61-
# #=> )
62-
def fit_poly(data, degree: 2, **options)
63-
sum_count = degree + 1
64-
initials = {}
65-
sum_count.times { |i| initials["a#{i}".to_sym] = 1 }
66-
options[:initials] = initials.merge(options[:initials] || {})
67-
function = sum_count.times.map { |i| "a#{i}*x**#{i}" }.join(' + ')
68-
fit(data, **options, function: function)
69-
end
3+
# Contains methods relating to Gnuplot's fit function.
4+
module Fit
5+
##
6+
# ====== Overview
7+
# Fit given data with function. Covered in
8+
# {fit notebook}[http://nbviewer.ipython.org/github/dilcom/gnuplotrb/blob/master/notebooks/fitting_data.ipynb].
9+
# ====== Arguments
10+
# * *data* - method accepts the same sources as Dataset.new
11+
# and Dataset object
12+
# * *:function* - function to fit data with. Default 'a2*x*x+a1*x+a0'
13+
# * *:initials* - initial values for coefficients used in fitting.
14+
# Default: {a2: 1, a1: 1, a0: 1}
15+
# * *:term_options* - terminal options that should be setted to terminal before fit.
16+
# You can see them in Plot's documentation (or even better in gnuplot doc).
17+
# Most useful here are ranges (xrange, yrange etc) and fit option which tunes fit parameters
18+
# (see {gnuplot doc}[http://www.gnuplot.info/docs_5.0/gnuplot.pdf] p. 122).
19+
# * *options* - options passed to Gnuplot's fit such as *using*. They are covered in
20+
# {gnuplot doc}[http://www.gnuplot.info/docs_5.0/gnuplot.pdf](pp. 69-74)
21+
# ====== Return value
22+
# Fit returns hash of 4 elements:
23+
# * *:formula_ds* - dataset with best fit curve as data
24+
# * *:coefficients* - hash of calculated coefficients. So if you gave {via: [:a, :b, :c]} or
25+
# {initials: {a: 1, b: 1, c: 1} } it will return hash with keys :a, :b, :c and its values
26+
# * *:deltas* - Gnuplot calculates possible deltas for coefficients during fitting and
27+
# *deltas* hash contains this deltas
28+
# * *:data* - pointer to Datablock with given data
29+
# ====== Examples
30+
# fit(some_data, function: 'exp(a/x)', initials: {a: 10}, term_option: { xrange: 1..100 })
31+
# fit(some_dataset, using: '1:2:3')
32+
def fit(data, function: 'a2*x*x+a1*x+a0', initials: { a2: 1, a1: 1, a0: 1 }, term_options: {}, **options)
33+
dataset = data.is_a?(Dataset) ? Dataset.new(data.data) : Dataset.new(data)
34+
opts_str = OptionHandling.ruby_class_to_gnuplot(options)
35+
output = gnuplot_fit(function, dataset, opts_str, initials, term_options)
36+
res = parse_output(initials.keys, function, output)
37+
{
38+
formula_ds: Dataset.new(res[2], title: 'Fit formula'),
39+
coefficients: res[0],
40+
deltas: res[1],
41+
data: dataset
42+
}
43+
end
7044

71-
##
72-
# :method: fit_<function>
73-
# :call-seq:
74-
# fit_exp(data, **options) -> Hash
75-
# fit_log(data, **options) -> Hash
76-
# fit_sin(data, **options) -> Hash
77-
#
78-
# ====== Overview
79-
# Shortcuts for fitting with several math functions (exp, log, sin).
80-
# ====== Arguments
81-
# * *data* - method accepts the same sources as Dataset.new and Dataset object
82-
# * *options* - all of this options will be passed to *#fit* so you
83-
# can set here any *term_options*. If you pass here *:initials* hash, it
84-
# will be merged into default initals hash { yoffset: 0.1, xoffset: 0.1, yscale: 1, xscale: 1 }
85-
# ====== Return value
86-
# See the same section for #fit.
87-
# ====== Examples
88-
# fit_exp(some_data, initials: { yoffset: -11 }, term_option: { xrange: 1..100 })
89-
# #=> The same as:
90-
# #=> fit(
91-
# #=> some_data,
92-
# #=> function: 'yscale * (yoffset + exp((x - xoffset) / xscale))',
93-
# #=> initals: { yoffset: -11, xoffset: 0.1, yscale: 1, xscale: 1 },
94-
# #=> term_option: { xrange: 1..100 }
95-
# #=> )
96-
# fit_log(...)
97-
# fit_sin(...)
98-
%w(exp log sin).map do |fname|
99-
define_method("fit_#{fname}".to_sym) do |data, **options|
100-
options[:initials] = {
101-
yoffset: 0.1,
102-
xoffset: 0.1,
103-
yscale: 1,
104-
xscale: 1
105-
}.merge(options[:initials] || {})
106-
function = "yscale * (yoffset + #{fname} ((x - xoffset) / xscale))"
45+
##
46+
# ====== Overview
47+
# Shortcut for fit with polynomial. Degree here is max power of *x* in polynomial.
48+
# ====== Arguments
49+
# * *data* - method accepts the same sources as Dataset.new and Dataset object
50+
# * *:degree* - degree of polynomial
51+
# * *options* - all of this options will be passed to *#fit* so you
52+
# can set here any *term_options*. If you pass here *:initials* hash, it
53+
# will be merged into default initals hash (all values are 1).
54+
# ====== Return value
55+
# See the same section for #fit.
56+
# ====== Examples
57+
# fit_poly(some_data, degree: 5, initials: { a4: 10, a2: -1 }, term_option: { xrange: 1..100 })
58+
# #=> The same as:
59+
# #=> fit(
60+
# #=> some_data,
61+
# #=> function: 'a5*x**5 + a4*x**4 + ... + a0*x**0',
62+
# #=> initals: {a5: 1, a4: 10, a3: 1, a2: -1, a1: 1, a0: 1},
63+
# #=> term_option: { xrange: 1..100 }
64+
# #=> )
65+
def fit_poly(data, degree: 2, **options)
66+
sum_count = degree + 1
67+
initials = {}
68+
sum_count.times { |i| initials["a#{i}".to_sym] = 1 }
69+
options[:initials] = initials.merge(options[:initials] || {})
70+
function = sum_count.times.map { |i| "a#{i}*x**#{i}" }.join(' + ')
10771
fit(data, **options, function: function)
10872
end
109-
end
110-
111-
private
11273

113-
##
114-
# It takes some time to produce output so here we need
115-
# to wait for it.
116-
def wait_for_output(term, variables)
117-
# now we should catch 'error' from terminal: it will contain approximation data
118-
# but we can get a real error instead of output, so lets wait for limited time
119-
start = Time.now
120-
output = ''
121-
until output_ready?(output, variables)
122-
begin
123-
term.check_errors
124-
rescue GnuplotRB::GnuplotError => e
125-
output += e.message
74+
##
75+
# :method: fit_<function>
76+
# :call-seq:
77+
# fit_exp(data, **options) -> Hash
78+
# fit_log(data, **options) -> Hash
79+
# fit_sin(data, **options) -> Hash
80+
#
81+
# ====== Overview
82+
# Shortcuts for fitting with several math functions (exp, log, sin).
83+
# ====== Arguments
84+
# * *data* - method accepts the same sources as Dataset.new and Dataset object
85+
# * *options* - all of this options will be passed to *#fit* so you
86+
# can set here any *term_options*. If you pass here *:initials* hash, it
87+
# will be merged into default initals hash { yoffset: 0.1, xoffset: 0.1, yscale: 1, xscale: 1 }
88+
# ====== Return value
89+
# See the same section for #fit.
90+
# ====== Examples
91+
# fit_exp(some_data, initials: { yoffset: -11 }, term_option: { xrange: 1..100 })
92+
# #=> The same as:
93+
# #=> fit(
94+
# #=> some_data,
95+
# #=> function: 'yscale * (yoffset + exp((x - xoffset) / xscale))',
96+
# #=> initals: { yoffset: -11, xoffset: 0.1, yscale: 1, xscale: 1 },
97+
# #=> term_option: { xrange: 1..100 }
98+
# #=> )
99+
# fit_log(...)
100+
# fit_sin(...)
101+
%w(exp log sin).map do |fname|
102+
define_method("fit_#{fname}".to_sym) do |data, **options|
103+
options[:initials] = {
104+
yoffset: 0.1,
105+
xoffset: 0.1,
106+
yscale: 1,
107+
xscale: 1
108+
}.merge(options[:initials] || {})
109+
function = "yscale * (yoffset + #{fname} ((x - xoffset) / xscale))"
110+
fit(data, **options, function: function)
126111
end
127-
if Time.now - start > Settings.max_fit_delay
128-
fail GnuplotError, "Seems like there is an error in gnuplotrb: #{output}"
112+
end
113+
114+
private
115+
116+
##
117+
# It takes some time to produce output so here we need
118+
# to wait for it.
119+
def wait_for_output(term, variables)
120+
# now we should catch 'error' from terminal: it will contain approximation data
121+
# but we can get a real error instead of output, so lets wait for limited time
122+
start = Time.now
123+
output = ''
124+
until output_ready?(output, variables)
125+
begin
126+
term.check_errors
127+
rescue GnuplotRB::GnuplotError => e
128+
output += e.message
129+
end
130+
if Time.now - start > Settings.max_fit_delay
131+
fail GnuplotError, "Seems like there is an error in gnuplotrb: #{output}"
132+
end
129133
end
134+
output
130135
end
131-
output
132-
end
133136

134-
##
135-
# Check if current output contains all the
136-
# variables given to fit.
137-
def output_ready?(output, variables)
138-
output =~ /Final set .*#{variables.join('.*')}/
139-
end
137+
##
138+
# Check if current output contains all the
139+
# variables given to fit.
140+
def output_ready?(output, variables)
141+
output =~ /Final set .*#{variables.join('.*')}/
142+
end
140143

141-
##
142-
# Parse Gnuplot's output to get coefficients and their deltas
143-
# from it. Also replaces coefficients in given function with
144-
# exact values.
145-
def parse_output(variables, function, output)
146-
plottable_function = " #{function.clone} "
147-
coefficients = {}
148-
deltas = {}
149-
variables.each do |var|
150-
value, error = output.scan(%r{#{var} *= ([^ ]+) *\+/\- ([^ ]+)})[0]
151-
plottable_function.gsub!(/#{var}([^0-9a-zA-Z])/) { value + Regexp.last_match(1) }
152-
coefficients[var] = value.to_f
153-
deltas[var] = error.to_f
144+
##
145+
# Parse Gnuplot's output to get coefficients and their deltas
146+
# from it. Also replaces coefficients in given function with
147+
# exact values.
148+
def parse_output(variables, function, output)
149+
plottable_function = " #{function.clone} "
150+
coefficients = {}
151+
deltas = {}
152+
variables.each do |var|
153+
value, error = output.scan(%r{#{var} *= ([^ ]+) *\+/\- ([^ ]+)})[0]
154+
plottable_function.gsub!(/#{var}([^0-9a-zA-Z])/) { value + Regexp.last_match(1) }
155+
coefficients[var] = value.to_f
156+
deltas[var] = error.to_f
157+
end
158+
[coefficients, deltas, plottable_function]
154159
end
155-
[coefficients, deltas, plottable_function]
156-
end
157160

158-
##
159-
# Make fit command and send it to gnuplot
160-
def gnuplot_fit(function, data, options, initials, term_options)
161-
variables = initials.keys
162-
term = Terminal.new
163-
term.set(term_options)
164-
initials.each { |var_name, value| term.stream_puts "#{var_name} = #{value}" }
165-
command = "fit #{function} #{data.to_s(term)} #{options} via #{variables.join(',')}"
166-
term.stream_puts(command)
167-
output = wait_for_output(term, variables)
168-
begin
169-
term.close
170-
rescue GnuplotError
171-
# Nothing interesting here.
172-
# If we had an error, we never reach this line.
173-
# Error here may be only additional information
174-
# such as correlation matrix.
161+
##
162+
# Make fit command and send it to gnuplot
163+
def gnuplot_fit(function, data, options, initials, term_options)
164+
variables = initials.keys
165+
term = Terminal.new
166+
term.set(term_options)
167+
initials.each { |var_name, value| term.stream_puts "#{var_name} = #{value}" }
168+
command = "fit #{function} #{data.to_s(term)} #{options} via #{variables.join(',')}"
169+
term.stream_puts(command)
170+
output = wait_for_output(term, variables)
171+
begin
172+
term.close
173+
rescue GnuplotError
174+
# Nothing interesting here.
175+
# If we had an error, we never reach this line.
176+
# Error here may be only additional information
177+
# such as correlation matrix.
178+
end
179+
output
175180
end
176-
output
177181
end
178182
end

spec/spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
include ChunkyPNG::Color
1414
include GnuplotRB
15+
include GnuplotRB::Fit
16+
1517
$RSPEC_TEST = true
1618

1719
def same_images?(*imgs)

0 commit comments

Comments
 (0)