[QUIZ] String Equations (#112)

R

Ruby Quiz

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

This quiz was adapted from an ACM programming challenge at the suggestion of
Gavin Kistner.

Let's define two operators for a simple set of string equations.

First, we will use the plus (+) operator and keep Ruby's meaning of
concatenation for it. Therefore, "james" + "gray" is "jamesgray".

The other operator we will support is equality (==). Two strings will be
considered equal if they are anagrams (they contain the same letters in a
possibly different ordering). In other words, "cinema" == "iceman".

Using these two operators we can build equations:

"i" + "am" + "lord" + "voldemort" == "tom" + "marvolo" + "riddle"

This week's quiz is to write a program that accepts a list of words on STDIN and
outputs any one string equation using those words, assuming there is one. Each
word in the list may be used zero or more times, but only on one side of the
equation. The program should exit(0) on success and exit(1), without printing
any output, if a solution cannot be found.

Let's sidestep case sensitivity issues by lower casing all incoming words. You
can also remove all non-alphanumeric characters.

Posting word lists and/or equations is not spoiler material.
 
D

Daniel Lucraft

cool quiz.

I'm currently having trouble with this evil word list:
bwu
ha
bwuhahahahahaha
 
D

Daniel Lucraft

James Gray
one. Each
word in the list may be used zero or more times, but only on one side of
the
equation.

Shall we also add the requirement that at least one word is used at
least once? :)

Daniel Lucraft
 
J

James Edward Gray II

James Gray

Shall we also add the requirement that at least one word is used at
least once? :)

Yes, see my post about this a little earlier.

James Edward Gray II
 
S

Sander Land

cool quiz.

I'm currently having trouble with this evil word list:
bwu
ha
bwuhahahahahaha

Here are the tests I'm using:
http://pastie.caboo.se/37546

Started
....
Finished in 0.735 seconds.
4 tests, 14 assertions, 0 failures, 0 errors

Tests are boolean only because my solution doesn't do anything else...
 
K

Kalman Noel

Ruby Quiz (James E. Gray):
This week's quiz is to write a program that accepts a list of words on STDIN and
outputs any one string equation using those words, assuming there is one. Each
word in the list may be used zero or more times, but only on one side of the
equation. The program should exit(0) on success and exit(1), without printing
any output, if a solution cannot be found.

Shouldn't there be a requirement that an equations is invalid if there is
another, shorter equation that uses the same words? Otherwise:

$ ruby quiz112.rb foo oof
"foo" = "oof"
"foo" + "foo" = "oof" + "oof"
"foo" + "foo" + "foo" = "oof" + "oof" + "oof"
...

Kalman
 
D

Daniel Lucraft

Kalman said:
Shouldn't there be a requirement that an equations is invalid if there
is
another, shorter equation that uses the same words? Otherwise:

$ ruby quiz112.rb foo oof
"foo" = "oof"
"foo" + "foo" = "oof" + "oof"
"foo" + "foo" + "foo" = "oof" + "oof" + "oof"
...

Kalman

That sounds right. How about if you have an equation a == b, you should
not be able to find equations c == d, and e == f such that c + e makes a
and d + f makes b?

Daniel
 
P

Paulo Köch

That sounds right. How about if you have an equation a =3D=3D b, you =20=
should
not be able to find equations c =3D=3D d, and e =3D=3D f such that c + = e =20
makes a
and d + f makes b?

Wouldn't a prolog script be better at describing all the rules than =20
trying to figuring them out?

If someone developed a prolog script that would behave as the final =20
ruby script should, we would then have the specs without a shadow of =20
doubt.

Just don't look at me to write it =3DP

Paulo Jorge Duarte K=F6ch
(e-mail address removed)
 
P

Paolo Negri

here's my solution for this quiz
It's quite slow, so you may want to tweak LENGTH_LIMIT constant to
have the script running in a reasonable time.
If you want to use this script to find equations for all the terms in
wikipedia you may want to look at some other solution.
It should behave as required from the original quiz.
I don't think it respects all the rules added after the original quiz
requirement :)

With this word list

bici
bwu
ha
bwuhahahahahaha
boci
cibo
bibo
bocibici
bibobici
ci
bo

produces 166 equations

Any comment is really welcome. Thanks for the fun.

#!/usr/bin/env ruby
require 'set'
words = $stdin
#a version of hash that supports + - / %
class AlgHash < Hash
def initialize(default = 0)
super(default)
end
def keys
super.to_set
end
alias len length
%w(+ - % /).each do |sign|
eval <<-END_EVAL
def #{sign}(other)
res = AlgHash.new
total_keys = self.keys + other.keys
total_keys.each {|k| res[k] = self[k] #{sign} other[k]}
res
end
END_EVAL
end
def base?(other)
other = other.signature unless other.respond_to? :keys
return false unless self.keys == other.keys
res = other % self
if res.values.uniq == [0]
if (multiplier = (other / self).values.uniq).size == 1
multiplier.first
end
end
end
end
#some utility modules we want for our special version of string
#and for Arrays
#we want to have some methods used in Set available for arrays and string
module SpecComp

def multiple?(other)
return false unless (self.len % other.len) == 0
if (self.signature % other.signature).values.uniq == [0] &&
(multiplier = (self.signature / other.signature).values.uniq).size == 1
multiplier.first
else
false
end
end

def base?(other)
other.multiple?(self)
end

def minus(other)
(self.signature - other.signature).delete_if {|k, v| v == 0}
end


%w(subset? superset? intersection).each do |comp_method|
eval <<-END_EVAL
def #{comp_method}(other)
return self.origin.send("#{comp_method}", other.origin)
end
END_EVAL
end

end
#a rich indexed version of string
#every string is lowercase and non alphanumeric chars are stripped
#every string has a signature which is hash with letters as keys and number
#of occurrences of the letter as values
#some arithmetics is possible on the signature of the strings
#the string reply to some Set class method that will be useful when we'll
#compare string groups
class HashedString
attr_accessor :signature, :eek:rigin, :len
include SpecComp
def initialize(string)
@signature = AlgHash.new
sane_string = string.downcase.unpack('c*')
sane_string.delete_if {|c| c < 49 || c > 122}
sane_string.each {|c| @signature[c] = @signature[c] + 1}
@len = sane_string.length
@sym = sane_string.pack('c*').to_sym
@origin = [@sym].to_set
end

def ==(other)
self.signature == other.signature && self.origin != other.origin
end

def +(other)
[self] + other
end

def *(integer)
ret = []
integer.times {ret += self}
ret
end

def to_s
return "\"#{@sym.to_s}\""
end

def to_ary
[self]
end

def to_sym
@sym
end
end
#Array have signature too
class Array
include SpecComp
def to_s
(self.map {|w| w.to_s}).join(' + ')
end
def signature
@signature ||= self.inject(AlgHash.new) {|sum, element| sum +
element.signature}
end
def origin
@origin ||= (self.map {|element| element.to_sym}).to_set
end
def len
@len ||= self.inject(0) {|len, element| len + element.len}
end
end
#the anagram finder
#LENGTH_LIMIT is necessary if you don't want your PC busy for years
class EquationFinder
LENGTH_LIMIT = 1000
attr_reader :success
def initialize(words_list)
@words_list = words_list.to_a.map! {|w| HashedString.new(w)}
@search_hash = Hash.new() {|h, k| h[k] = []}
@words_list.each_with_index do |word, index|
@search_hash[word.signature.keys.to_a.sort] << word
to_add = []
@search_hash.each_value do |other_words|
other_words.each do |other_word|
if !word.subset?(other_word) && (other_word.origin.size <
LENGTH_LIMIT)
sum = word + other_word
to_add << sum
end
end
end
to_add.each {|v| @search_hash[v.signature.keys.to_a.sort] << v}
end
end
def write_equation(left_string, right_string)
@success = 0 unless @success
puts left_string.to_s + " == " + right_string.to_s
end
def find
@search_hash.each_value do |homogeneus_words_list|
homogeneus_words_list.size.times do
word = homogeneus_words_list.pop
find_equation_in_array(word, homogeneus_words_list)
end
end
end
def find_equation_in_array(word, array)
array.each { |other_word| equation(word, other_word) }
end
def equation(left, right)
if right.intersection(left).empty?
if left == right
write_equation(left, right)
elsif multiplier = left.multiple?(right)
write_equation(left, right * multiplier)
elsif multiplier = left.base?(right)
write_equation(left * multiplier, right)
else
try_other_formulas(left, right)
end
end
end
def try_other_formulas(left, right)
short, long = [left, right].sort! {|a, b| a.len <=> b.len}
short = [short] if short.instance_of? HashedString
#begin
return false if (short.collect {|o| o.len}).min > (long.len/2)
#rescue
# p short
# p short.origin
# raise
#end
difference = (long.minus short)
short.each do |short_origin|
if multiplier = (short_origin.signature.base? difference)
write_equation((short + short_origin * multiplier), long) if
multiplier > 0
end
end
end
end
finder = EquationFinder.new($stdin)
finder.find
exit(finder.success || 1)
 
S

Sander Land

Here is my solution.
It generates 26 equations (1 per character) in matrix form.
If the nullity of this matrix is zero then the only solution is the
zero solution (no words on either side), which is invalid.
If it is > 0 then there are infinitely many solutions, any vector of
integers in the null space corresponds to one of them.

The way to generate such a solution is:
(1) Find a vector in the null space using Rationals
(2) Multiply it with large enough number (least common multiple of the
denominators) such that all the Rationals turn into integers.
(3) All the negative numbers are counts for words on the left side,
positive for words on the right side.

Step (1) would take more time than I have right now, so I tried
searching for a library. But anything that calculates a basis for the
null space has no support for Rationals and using floats makes step
(2) pretty much impossible. So my code just returns if an equation
exists instead of generating one.

One last remark about my code: it uses Matrix#rank, which seems to be
bugged. To run it you may need to patch your matrix.rb file (see
http://www.ruby-forum.com/topic/96387)

code:

require 'matrix'

class String
def to_freq
('a'..'z').map{|c| Rational(count(c)) } # use Rational for
stability in calculating rank
end
end

def has_string_eqn(words)
table = words.uniq.map{|w| w.downcase.gsub(/[^a-z]/,'').to_freq }.transpose
nullity = words.length - Matrix[*table].rank
return nullity > 0
end
 
J

James Edward Gray II

Ruby Quiz (James E. Gray):

Shouldn't there be a requirement that an equations is invalid if
there is
another, shorter equation that uses the same words? Otherwise:

$ ruby quiz112.rb foo oof
"foo" = "oof"
"foo" + "foo" = "oof" + "oof"
"foo" + "foo" + "foo" = "oof" + "oof" + "oof"
...

If you want to make shorter equations a goal for your solution, go
for it. This is not a requirement of the quiz though.

James Edward Gray II
 
D

Daniel Lucraft

Sander said:
Step (1) would take more time than I have right now, so I tried

I guess I have too much time on my hands :) This is a solver for
homogenous systems of linear equations: http://pastie.caboo.se/37827

It reduces the matrix to row echelon form, and then reads off the
solutions from the non-unitary columns, multiplying them up to make them
integral. It's probably not that robust in the case of degenerate or
edge case matrices, but it works for solving the word equations.
Obviously there are infinite solutions, so it returns the largest set of
linearly independent solutions. It was fun to make, I had to dig out the
linear algebra textbook I've had since my undergrad days.

And this is the solution that uses it. It does exactly the same thing as
Sander's in making the matrix, and then just uses the linear solver and
translates the solutions into word equations.

Fun quiz!

Daniel

# WordEquations
#
# $ cat test.txt
# i
# am
# lord
# voldemort
# tom
# marvolo
# riddle
# $ cat test.txt | ruby quiz112.rb
# i + am + lord + voldemort == tom + marvolo + riddle
#
# Prints equation closest to 80 chars in length.
#
# Also:
# $ cat test.txt | ruby quiz112.rb shortest
# $ cat test.txt | ruby quiz112.rb longest
# $ cat test.txt | ruby quiz112.rb all
#
# Generates word equations by solving the equivalent
# homogenous linear equations. E.g.
#
# a = hohoho
# b = h
# c = oo
# d = ho
# ->
# h's: 3a + b + + d = 0
# o's: 3a + 2c + d = 0
# ->
# solutions:
# [-2, 6, 3, 0] & [-1, 0, 0, 3]
# ->
# hohoho+hohoho == h+h+h+h+h+h+oo+oo+oo
# hohoho == ho+ho+ho

require 'homogenouslinearsolver'

class WordEquations
def self.word_problem(words)
words = words.map {|word| word.downcase.match(/\w+/); $&}.uniq
words = words.select{|w| w}

rows = ('a'..'z').map do |l|
words.map {|word| Rational(word.count(l), 1) }
end
solutions = HomogenousLinearSolver.solve(Matrix[*rows])

# if solution value is negative, add the word to the left side,
# otherwise add to the right.
solutions.collect do |sol|
left_words = []
right_words = []
sol.each_with_index do |val, i|
if val < 0
left_words += [words]*(val*-1)
elsif val > 0
right_words += [words]*val
end
end
left_words.join(" + ") +
" == " + right_words.join(" + ")
end
end
end

if ARGV[0] == "test"
require 'test/unit'

class String
def sorted
(unpack('c*').sort).pack('c*')
end
end

class TestWordEquations < Test::Unit::TestCase
def test_hohoho
words = %w{hohoho h oo}
assert_equal ["hohoho + hohoho == h + h + h + h + h + h + oo + oo
+ oo"],
WordEquations.word_problem(words)
end

def test_get_words_harry_potter
words = %w{i am lord voldemort tom marvolo riddle}
assert_equal ["i + am + lord + voldemort == tom + marvolo +
riddle"],
WordEquations.word_problem(words)
end

def test_get_words_me
words = %w{daniel lucraft fluent radical}
assert_equal ["daniel + lucraft == fluent + radical"],
WordEquations.word_problem(words)
end

def test_get_words_evil!
words = %w{bwu ha bwuhahahahaha}
assert_equal ["bwu + ha + ha + ha + ha + ha == bwuhahahahaha"],
WordEquations.word_problem(words)
end

def test_get_words_1
words = %w{dormitory dirty room}
assert_equal ["dormitory == dirty + room"],
WordEquations.word_problem(words)
end

def test_get_words_2
words = %w{statue of liberty built to stay free}
assert_equal ["statue + of + liberty == built + to + stay +
free"],
WordEquations.word_problem(words)
end

def test_get_words_3
words = %w{hohoho h oo ho}
assert_equal ["hohoho + hohoho == h + h + h + h + h + h + oo + oo
+ oo",
"hohoho == ho + ho + ho"],
WordEquations.word_problem(words)
end

def test_no_solutions
words = %w{foo bar}
assert_equal [], WordEquations.word_problem(words)
end

def test_long
words = %w[My experience in Amsterdam is that cyclists ride where
the hell they like and aim in a state of rage at all pedestrians while
ringing their bell loudly, the concept of avoiding people being foreign
to them. My dream holiday would be a ticket to Amsterdam, immunity from
prosecution and a baseball bat- Terry Pratchett]
results = WordEquations.word_problem(words).sort_by{|eq|
-eq.length}
result = results[0]
assert_equal "my + my + my + my + my + my + my + my + my + my + my
+ "+
"my + my + my + my + my + my + my + my + my + my + my + my + my
+ "+
"my + my + my + my + my + my + my + my + my + my + in + in + in
+ "+
"in + in + in + in + in + is + is + is + is + is + is + is + is
+ "+
"is + is + is + is + is + that + that + that + that + ride +
ride + "+
"ride + ride + ride + ride + ride + ride + ride + the + the +
the + "+
"the + the + the + the + the + the + the + the + the + the + the
+ "+
"the + the + the + the + the + the + the + the + the + the + the
+ "+
"the + the + the + the + the + the + the + the + the + the + a +
a + "+
"a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a +
a + "+
"a + a + a + a + a + a + a + a + a + a + a + a + a + a + a + a +
a + "+
"a + a + loudly + loudly + loudly + loudly + concept + concept +
"+
"concept + concept == amsterdam + amsterdam + amsterdam + "+
"amsterdam + amsterdam + cyclists + cyclists + hell + hell +
hell + "+
"they + they + they + they + they + they + they + they + they +
"+
"they + they + they + they + they + they + they + they + they +
"+
"they + they + they + they + they + they + they + they + they +
"+
"they + they + they + they + they + they + they + they + they +
"+
"and + and + and + and + and + and + and + and + aim + aim + aim
+ "+
"aim + aim + aim + aim + aim + aim + aim + aim + aim + aim + aim
+ "+
"aim + aim + aim + aim + aim + aim + aim + aim + aim + aim + "+
"prosecution + prosecution + prosecution + prosecution",
result
sides = result.split("==")
sides.map! {|side| side.delete("+").delete(" ")}
left = sides[0]
right = sides[1]
assert left.sorted == right.sorted
end
end
else
results = WordEquations.word_problem(STDIN.read.split).sort_by{|eq|
-eq.length}
if results == []
exit(1)
else
if ARGV[0] == "longest"
puts results.first
elsif ARGV[0] == "shortest"
puts results.last
elsif ARGV[0] == "all"
results.each {|r| puts r; puts}
else
puts results.sort_by{|eq| (80-eq.length).abs}.first
end
exit(0)
end
end
 
H

Hendrik Lipka

If you want to make shorter equations a goal for your solution, go
for it. This is not a requirement of the quiz though.

If you don't have this requirement, you have either none or an infinite
number of solution - if a solution a==b exists, the ist also the solution
a+a==b+b (and so on). So, only the minimal equations should count.

hli
 
E

Eric I.

My solution to the quiz below belongs to this same family of
solutions. I, like Daniel, actually solved the set of equations via
matrix transformations, although my code is self-contained and quick
'n dirty, not nicely refactored like his.

When multiple families of word equation solutions exist, my program
chooses one of them randomly to display.

Eric

----------------
Are you interested in on-site Ruby training that's been highly
reviewed by former students? http://LearnRuby.com

================

require 'mathn'

CompactOutput = false

# calculate the least common multiple of one or more numbers
def lcm(first, *rest)
rest.inject(first) { |l, n| l.lcm(n) }
end

# Returns nil if there is no solution or an array containing two
# elements, one for the left side of the equation and one for the
# right side. Each of those elements is itself an array containing
# pairs, where each pair is an array in which the first element is the
# number of times that word appears and the second element is the
# word.
def solve_to_array(words)
# clean up word list by eliminating non-letters, converting to lower
# case, and removing duplicate words
words.map! { |word| word.downcase.gsub(/[^a-z]/, '') }.uniq!

# calculate the letters used in the set of words
letters = Hash.new
words.each do |word|
word.split('').each { |letter| letters[letter] = true }
end

# create a matrix to represent a set of linear equations.
column_count = words.size
row_count = letters.size
equations = []
letters.keys.each do |letter|
letter_counts = []
words.each { |word| letter_counts << word.count(letter) }
equations << letter_counts
end

# transform matrix into row echelon form
equations.size.times do |row|
# re-order the rows, so the row with a value in then next column
# to process is above those that contain zeroes
equations.sort! do |row1, row2|
column = 0
column += 1 until column == column_count ||
row2[column].abs != row1[column].abs
if column == column_count : 0
else row2[column].abs <=> row1[column].abs
end
end

# figure out which column to work on
column = (0...column_count).detect { |i| equations[row] != 0 }
break unless column

# transform rows below the current row so that there is a zero in
# the column being worked on
((row + 1)...equations.size).each do |row2|
factor = -equations[row2][column] / equations[row][column]
(column...column_count).each do |c|
equations[row2][c] += factor * equations[row][c]
end
end
end

# only one of the free variables chosen randomly will get a 1, the
# rest 0
rank = equations.select { |row| row.any? { |v| v != 0 }}.size
free = equations[0].size - rank
free_values = Array.new(free, 0)
free_values[rand(free)] = 2 * rand(2) - 1

values = Array.new(equations[0].size) # holds the word_counts

# use backward elimination to find values for the variables; process
# each row in reverse order
equations.reverse_each do |row|
# determine number of free variables for the given row
free_variables = (0...column_count).inject(0) do |sum, index|
row[index] != 0 && values[index].nil? ? sum + 1 : sum
end

# on this row, 1 free variable will be calculated, the others will
# get the predetermined free values; the one being calculated is
# marked with nil
free_values.insert(rand(free_variables), nil) if free_variables >
0

# assign values to the variables
sum = 0
calc_index = nil
row.each_index do |index|
if row[index] != 0
if values[index].nil?
values[index] = free_values.shift

# determine if this is a calculated or given free value
if values[index] : sum += values[index] * row[index]
else calc_index = index
end
else
sum += values[index] * row[index]
end
end
end
# calculate the remaining value on the row
values[calc_index] = -sum / row[calc_index] if calc_index
end

if values.all? { |v| v } && values.any? { |v| v != 0 }
# in case we ended up with any non-integer values, multiply all
# values by their collective least common multiple of the
# denominators
multiplier =
lcm(*values.map { |v| v.kind_of?(Rational) ? v.denominator :
1 })
values.map! { |v| v * multiplier }

# deivide the terms into each side of the equation depending on
# whether the value is positive or negative
left, right = [], []
values.each_index do |i|
if values > 0 : left << [values, words]
elsif values < 0 : right << [-values, words]
end
end

[left, right] # return found equation
else
nil # return no found equation
end
end


# Returns a string containing a solution if one exists; otherwise
# returns nil. The returned string can be in either compact or
# non-compact form depending on the CompactOutput boolean constant.
def solve_to_string(words)
result = solve_to_array(words)
if result
if CompactOutput
result.map do |side|
side.map { |term| "#{term[0]}*\"#{term[1]}\"" }.join(' + ')
end.join(" == ")
else
result.map do |side|
side.map { |term| (["\"#{term[1]}\""] * term[0]).join(' +
') }.
join(' + ')
end.join(" == ")
end
else
nil
end
end


if __FILE__ == $0 # if run from the command line...
# collect words from STDIN
words = []
while line = gets
words << line.chomp
end

result = solve_to_string(words)

if result : puts result
else exit 1
end
end
 
E

Eric I.

I'll feed the datapoint pool as well using my solution. Here is the
output of multiple runs using the Terry Pratchett quote that appeared
earlier in the thread. To make it slightly easier to interpret, I'm
using a more compact form (that deviates slightly from the quiz
requirements) to output the word equations.

1*"my" + 2*"the" == 1*"they" + 1*"them"

2*"my" + 1*"amsterdam" + 3*"ride" + 2*"where" + 1*"the" + 1*"hell" +
2*"and" + 2*"aim" == 2*"in" + 1*"is" + 2*"they" + 2*"while" +
6*"dream"

2*"amsterdam" + 1*"is" + 3*"hell" + 5*"they" + 1*"aim" + 6*"bat" ==
5*"my" + 3*"that" + 2*"ride" + 5*"the" + 2*"at" + 3*"baseball"

2*"my" + 7*"amsterdam" + 5*"is" + 27*"the" + 1*"hell" + 4*"and" +
8*"aim" + 4*"pratchett" == 8*"that" + 2*"cyclists" + 7*"ride" + 18*"a"
+ 4*"pedestrians" + 24*"them"

5*"cyclists" + 16*"the" + 3*"like" + 7*"and" + 6*"aim" + 7*"to" +
7*"pratchett" == 3*"amsterdam" + 7*"is" + 14*"that" + 4*"ride" +
4*"hell" + 5*"they" + 7*"concept" + 3*"ticket"

6*"in" + 11*"amsterdam" + 13*"is" + 18*"they" + 2*"and" + 6*"while" +
8*"pratchett" == 14*"my" + 16*"that" + 4*"cyclists" + 5*"ride" +
6*"where" + 9*"the" + 1*"hell" + 8*"aim" + 8*"pedestrians"

Eric
 
J

James Edward Gray II

If you don't have this requirement, you have either none or an
infinite
number of solution - if a solution a==b exists, the ist also the
solution
a+a==b+b (and so on).

That's why the quiz asks for any of the possible solutions.
So, only the minimal equations should count.

An right answer is fine, so feel free to use minimal equations.

James Edward Gray II
 
K

Kalman Noel

James Edward Gray II:
That's why the quiz asks for any of the possible solutions.

Ah, this is where I didn't read properly. Also this may be the reason why I
didn't actually try and start coding.

Kalman
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,982
Messages
2,570,185
Members
46,736
Latest member
AdolphBig6

Latest Threads

Top