Рекурсивная программа для вычисления аппроксимации Тейлора косинуса не работает в Прологе

Я все еще новичок в Прологе, и я не уверен, почему этот код не работает. Я считаю, что это, скорее всего, проблема с базовым случаем или в последних 3 строках рекурсивного случая. Все остальное работает просто отлично.

Эта программа определяет косинус, вычисленный с помощью аппроксимации ряда,

введите здесь описание изображения

для этого необходимо вычислить факториал 2K, а также -1 ^ K, а затем использовать эти 2 вычисления в окончательном уравнении (это делается в % рекурсивном случае).

% Factorial from class
fact(0, 1).
fact(N, F) :- 
    N > 0,
    N1 is N-1,
    fact(N1, F1),
    F is F1 * N.

% Calculate -1 ^ K
signCnt(0,1).
signCnt(K,S) :- 
    K > 0,
    K1 is K - 1,
    signCnt(K1,S1),
    S is S1 * -1.

% Base case
cosN(N,_,_,0).

% Recursive case
cosN(K,N,X,Y) :- K < N,
    signCnt(K,S),
    K2 is 2 * K,
    fact(K2,F),
    Yk is (S * X**K2)/F,
    K1 is K + 1,
    cosN(K1,N,X,Y1),
    Y is Y1 + Yk.

cosN(N,X,Y) :- 
    N>0,
    cosN(0,N,X,Y).

Входные данные должны быть в форме

?- cosN(25,pi,Y).

с ожидаемым выходом

Y = -1.0 ;
false.

однако рекурсия не проходит должным образом, и в итоге результат выглядит следующим образом: пример вывода

где 5 и pi могут быть любыми, пока pi остается в форме pi (то есть pi/2, pi/3), также не должно быть никаких дополнительных строк, так как нам дали ограничение на количество строк. Строки должны быть отредактированы/заменены. Все, что укажет мне в правильном направлении, тоже будет очень признательно.

(Спасибо Гаю Кодеру за помощь в форматировании)


Под редакцией Гая Кодера

Некоторые тестовые примеры с использованием SWI-Prolog

:- begin_tests(cosine_approximation).

factorial_test_case_generator(0,1).
factorial_test_case_generator(1,1).
factorial_test_case_generator(2,2).
factorial_test_case_generator(3,6).
factorial_test_case_generator(4,24).
factorial_test_case_generator(5,120).
factorial_test_case_generator(6,720).
factorial_test_case_generator(7,5040).
factorial_test_case_generator(8,40320).
factorial_test_case_generator(20,2432902008176640000).

test('factorial',[nondet,forall(factorial_test_case_generator(N,Factorial))]) :-
    fact(N,Factorial).

signCnt_test_case_generator(0,1).
signCnt_test_case_generator(1,-1).
signCnt_test_case_generator(2,1).
signCnt_test_case_generator(3,-1).
signCnt_test_case_generator(4,1).
signCnt_test_case_generator(5,-1).

test('signCnt',[nondet,forall(signCnt_test_case_generator(N,Sign))]) :-
    signCnt(N,Sign).

:- end_tests(cosine_approximation).

Пример запуска:

?- make.
% c:/users/eric/documents/projects/prolog/so_question_161 compiled 0.00 sec, 5 clauses
% PL-Unit: cosine_approximation .......... done
% All 10 tests passed
true.

person ItsDraig    schedule 13.02.2020    source источник
comment
Можете ли вы уточнить, что вы ожидаете от этого кода и что он делает вместо этого?   -  person Dan    schedule 13.02.2020
comment
Готово, извините за это.   -  person ItsDraig    schedule 13.02.2020
comment
Кажется, вы пытаетесь получить логический язык программирования для математических вычислений. Он может это сделать, но если вы учитесь, вам лучше решать проблемы, которые включают в себя большую унификацию и возврат.   -  person Enigmativity    schedule 13.02.2020
comment
Я полагаю, вы, возможно, выполнили некоторую отладку резинового утёнка.   -  person Guy Coder    schedule 14.02.2020
comment
Я сделал! Просто нужно было понять, почему базовый случай был там, каким будет последний шаг рекурсии, а затем понял свою глупую ошибку.   -  person ItsDraig    schedule 14.02.2020


Ответы (2)


Базовый регистр был неправильным, должен был быть cosN (N, N, _, 0). поскольку K и N должны быть равны N, когда программа завершает рекурсивный процесс.

Тестовые примеры:

:- begin_tests(cosine_approximation).

factorial_test_case_generator(0,1).
factorial_test_case_generator(1,1).
factorial_test_case_generator(2,2).
factorial_test_case_generator(3,6).
factorial_test_case_generator(4,24).
factorial_test_case_generator(5,120).
factorial_test_case_generator(6,720).
factorial_test_case_generator(7,5040).
factorial_test_case_generator(8,40320).
factorial_test_case_generator(20,2432902008176640000).

test('factorial',[nondet,forall(factorial_test_case_generator(N,Factorial))]) :-
    fact(N,Factorial).

signCnt_test_case_generator(0,1).
signCnt_test_case_generator(1,-1).
signCnt_test_case_generator(2,1).
signCnt_test_case_generator(3,-1).
signCnt_test_case_generator(4,1).
signCnt_test_case_generator(5,-1).

test('signCnt',[nondet,forall(signCnt_test_case_generator(N,Sign))]) :-
    signCnt(N,Sign).

cosN_test_case_generator(3,pi/2,0.01996895776487828).
cosN_test_case_generator(5,pi,-0.9760222126236076).
cosN_test_case_generator(25,pi,-1.0).
cosN_test_case_generator(10,pi/2,-3.3306690738754696e-15).

test('cosN',[nondet,forall(cosN_test_case_generator(N,X,Y))]) :-
    cosN(N,X,Y).

:- end_tests(cosine_approximation).

Пример запуска:

?- make.
% /Users/oliverclarke/prolog/lab5-quiz compiled 0.00 sec, 3 clauses
% PL-Unit: cosine_approximation .................... done
% All 20 tests passed
true.
person ItsDraig    schedule 13.02.2020
comment
Если вы не добавите тестовые примеры, я не дам вам голос. - person Guy Coder; 14.02.2020
comment
также спасибо за помощь с тестовыми примерами, они очень полезны! Я никогда не использовал их раньше. - person ItsDraig; 14.02.2020
comment
Ха-ха, я постараюсь иметь это в виду. Спасибо за наводку, чувак, удачи. - person ItsDraig; 14.02.2020
comment
В нем говорится, что вы можете утвердить свой ответ через 2 дня. - person ItsDraig; 14.02.2020

Просто дополнение

Мне пришлось подумать о том, действительно ли программа суммирует маленькие числа с плавающей запятой в последовательно большие числа с плавающей запятой, а не маленькие числа с плавающей запятой в большие числа с плавающей запятой (что может сделать результат более неточным, чем нужно), но это так.

Хотя неэлегантно полностью пересчитывать факториал для каждого элемента ряда Тейлора и не использовать -1 * (k mod 2) для получения (-1)^k напрямую, вместо этого выполняя рекурсию.

Вот схема вызова для ориентации:

Диаграмма вызовов серии Тейлор

Приложение 2: Код для более эффективных вычислений

Поэтому я воспользовался этим временем, чтобы выполнить упражнение по написанию cos приближения, которое просто рекурсивно обращается к самому себе и несет всю вспомогательную информацию для вычисления условий и и суммы.

% ===
% Entry point!
% Evaluate the Taylor series for cos(z) at "z" (not too far from 0, probably
% less than 1). The terms (sum elements) for index values 0..K are computed
5 and added. (K >= 0)
% ===

taylor_cos(Res,Z,Kmax,Verbose) :- 
   Zf is Z*1.0, % make a float
   float(Zf),
   integer(Kmax),Kmax >= 0,
   Zsq is Zf*Zf,
   at_element_k(Res,0,Kmax,Zsq,_,_,Verbose).

% The value computed is always the first one

even(K) :- integer(K), (K mod 2) =:= 0. % eval left & compare numerically
odd(K)  :- integer(K), (K mod 2) =:= 1. % eval left & compare numerically

% Compute (-1)^k, k an integer >= 0.
% Computed value is on first place in predicate argument list.

minus_one_tothe_k( 1,K) :- even(K),!. % ! to make this deterministic
minus_one_tothe_k(-1,K) :- odd(K).    % actually no need to test odd(K)

% Compute (2*k)!, k an integer >= 0, if (2*(k-1))! is known.
% Computed value is on first place in predicate argument list.
% The base case is conceptually jarring as the "prior value" can be anything.
% This is not unlike a function becoming evaluatable because of lazy evaluation.

two_times_k_factorial(1  ,0,_)        :- !.
two_times_k_factorial(Res,K,ResPrior) :- K>0, Res is ResPrior*K*(4*K-2).

% Compute (z^(2*k)), k an integer >= 0, if (z^(2*(k-1))) is known.
% z² is passed too so that we do not need to recompute it again and again.
% Computed value is on first place in predicate argument list.

z_tothe_2k(1,   0, _   ,_)        :- !.
z_tothe_2k(Res, K, Zsq ,ResPrior) :- K>0, Res is ResPrior * Zsq.

% Compute the Taylor series by summing the elements(k) with k in [0..Kmax)
% (so Kmax >= 1).
% When calling this initially, the values for TTKFprior and ZTT2Kprior
% are of no importance. 
% The procedures calls itself recursively to compute element(i), element(i+1)
% etc. based on prior intermediate values. The base case is attained when
% K > Kmax. The sum accumulates in SumFromKmaxBackwards when the recursion
% comes back up the stack.

at_element_k(0.0,K,Kmax,_,_,_,Verbose) :-
   K > Kmax,!,
   ((Verbose = verbose) -> 
   format("past the end as K=~d > Kmax=~d, returning back up the stack\n",[K,Kmax]) ; true).

at_element_k(SumFromKmaxBackwards,K,Kmax,Zsq,TTKFprior,ZTT2Kprior,Verbose) :- 
   minus_one_tothe_k(M1TTK,K),                 % M1TTK = (-1)^K
   two_times_k_factorial(TTKF,K,TTKFprior),    % TTKF  = f(K,TTKFprior)
   z_tothe_2k(ZTT2K,K,Zsq,ZTT2Kprior),         % ZTT2K = f(K,z²,ZTT2Kprior)
   ElementK is M1TTK * ZTT2K / TTKF,           % element_k = M1TTK * (ZTT2K / TTKF)
   ((Verbose = verbose) -> format("element(~d) = ~e\n",[K,ElementK]) ; true),
   KP1 is K+1,
   at_element_k(SumFromKmaxBackwardsPrior,KP1,Kmax,Zsq,TTKF,ZTT2K,Verbose),
   SumFromKmaxBackwards is SumFromKmaxBackwardsPrior + ElementK,
   ((Verbose = verbose) -> format("taylor-series-sum(~d ... ~d) = ~e (added ~e to prior value ~e)\n",
                                  [K,Kmax,SumFromKmaxBackwards, ElementK, SumFromKmaxBackwardsPrior]) ; true).

Запустите это! Переменной Verbose присвоено значение verbose, чтобы генерировать больше распечаток во время вычисления ряда Тейлора. Вычисляем 11 членов ряда (индексы 0...10).

?- taylor_cos(Res,0.01,10,verbose).
element(0) = 1.000000e+00
element(1) = -5.000000e-05
element(2) = 4.166667e-10
element(3) = -1.388889e-15
element(4) = 2.480159e-21
element(5) = -2.755732e-27
element(6) = 2.087676e-33
element(7) = -1.147075e-39
element(8) = 4.779477e-46
element(9) = -1.561921e-52
element(10) = 4.110318e-59
past the end as K=11 > Kmax=10, returning back up the stack
taylor-series-sum(10 ... 10) = 4.110318e-59 (added 4.110318e-59 to prior value 0.000000e+00)
taylor-series-sum(9 ... 10) = -1.561920e-52 (added -1.561921e-52 to prior value 4.110318e-59)
taylor-series-sum(8 ... 10) = 4.779476e-46 (added 4.779477e-46 to prior value -1.561920e-52)
taylor-series-sum(7 ... 10) = -1.147074e-39 (added -1.147075e-39 to prior value 4.779476e-46)
taylor-series-sum(6 ... 10) = 2.087675e-33 (added 2.087676e-33 to prior value -1.147074e-39)
taylor-series-sum(5 ... 10) = -2.755730e-27 (added -2.755732e-27 to prior value 2.087675e-33)
taylor-series-sum(4 ... 10) = 2.480156e-21 (added 2.480159e-21 to prior value -2.755730e-27)
taylor-series-sum(3 ... 10) = -1.388886e-15 (added -1.388889e-15 to prior value 2.480156e-21)
taylor-series-sum(2 ... 10) = 4.166653e-10 (added 4.166667e-10 to prior value -1.388886e-15)
taylor-series-sum(1 ... 10) = -4.999958e-05 (added -5.000000e-05 to prior value 4.166653e-10)
taylor-series-sum(0 ... 10) = 9.999500e-01 (added 1.000000e+00 to prior value -4.999958e-05)
Res = 0.9999500004166653.

80 колонок в Stackoverflow немного действует мне на нервы. В настоящее время у нас на экранах миллион пикселей ширины, и они не используются и оставлены белыми, потому что «Визуальный дизайн»!! В любом случае...

Теперь добавим код для создания Count тестовых поплавков, равномерно распределенных между From и To. generator/4 генерирует последовательные значения при возврате. cos_compare/3 сравнивает то, что вычисляет наша cos-аппроксимирующая функция, и то, что вычисляет система (которая появляется где-то глубоко в библиотеке).

generator(X,From,To,1) :- 
   From =< To,
   From_f is From*1.0,
   To_f   is To*1.0,
   X      is (From_f + To_f) / 2.0.

generator(X,From,To,Count) :- 
   integer(Count), 
   Count > 1,
   From =< To,
   From_f  is From*1.0,
   To_f    is To*1.0,
   Delta_f is (To_f - From_f)/(Count * 1.0),
   CountM1 is Count-1, 
   between(0,CountM1,I), 
   X is From_f + Delta_f*I.

cos_compare(Z,Kmax,Verbose) :-
   taylor_cos(Res,Z,Kmax,Verbose),
   Cos is cos(Z),
   Delta is abs(Res-Cos),
   format("For z = ~e, k_max = ~d, difference to real cos = ~e\n", [Z, Kmax, Delta]).

Затем давайте фактически сравним 100 значений между числами с плавающей запятой -4.0 и числом с плавающей запятой +4.0 и, где мы вычисляем 11 членов (индексы 0..11) ряда Тейлора для каждого значения:

run(Verbose) :- forall(generator(Z,-4.0,+4.0,100), cos_compare(Z,10,Verbose)).

?- run(quiet).  
For z = -4.000000e+00, k_max = 10, difference to real cos = 1.520867e-08
For z = -3.920000e+00, k_max = 10, difference to real cos = 9.762336e-09
For z = -3.840000e+00, k_max = 10, difference to real cos = 6.209067e-09
For z = -3.760000e+00, k_max = 10, difference to real cos = 3.911487e-09
For z = -3.680000e+00, k_max = 10, difference to real cos = 2.439615e-09
......
For z = 3.680000e+00, k_max = 10, difference to real cos = 2.439615e-09
For z = 3.760000e+00, k_max = 10, difference to real cos = 3.911487e-09
For z = 3.840000e+00, k_max = 10, difference to real cos = 6.209067e-09
For z = 3.920000e+00, k_max = 10, difference to real cos = 9.762336e-09
true.

Выглядит не так уж плохо.

Приложение 3: Использование «слов» SWI-Prolog для связи между предикатами

Я обнаружил, что при написании Perl-функций часто бывает выгоднее сократить передачу аргументов на основе позиции и вместо этого передать один набор пар имя-значение, также известных как «хэши». Это добавляет большую гибкость (именованные параметры, простота добавления параметров, простота отладки, простота передачи параметров в подфункции и т. д.).

Попробуем и здесь.

Это ограничено SWI-Prolog, потому что "dicts" являются функция SWI-Prolog< /а>. Подобный код делает механизм индексации Пролога бесполезным, так как теперь каждый предикат имеет точно такой же аргумент, Dict, поэтому во время выполнения он должен быть относительно медленным.

Только модифицированные предикаты

taylor_cos(Res,Z,Kmax,Verbose) :-
   Zf is Z*1.0, % make a float
   float(Zf),
   integer(Kmax),Kmax >= 0,
   Zsq is Zf*Zf,
   at_element_k(taylor{  sum     : Res  % the result
                        ,k       : 0
                        ,kmax    : Kmax
                        ,zsq     : Zsq
                        ,ttkf_prior  : _
                        ,ztt2k_prior : _
                        ,verbose : Verbose }).


% ---
% Base case, when k > kmax
% ---

% We map the passed "Dict" to a sub-Dict to grab values.
% As this is "unification", not only "pattern matching" the value for
% sum "0.0" is shared into "Dict".

at_element_k(Dict) :-
   taylor{  sum     : 0.0
           ,k       : K
           ,kmax    : Kmax
           ,verbose : Verbose } :< Dict,

   K > Kmax,  % guard
   !,         % commit
   ((Verbose = verbose) ->
      format("past the end as K=~d > Kmax=~d, returning back up the stack\n",[K,Kmax])
      ; true).

% ---
% Default case, when k <= kmax
% ---

% We map the passed "Dict" to a sub-Dict to grab values.
% We use ":<" instead of "=" so that, if the passed Dict has more values
% than expected (which can happen during program extension and fiddling),
% "partial unification" can still proceed, "=" would fail. However, no
% values can be missing!
% This gives us also the funny possibility of completely ignoring Kmax in
% the "input Dict", it doesn't appear anywhere and is still passed down
% through the recursive call. Well, it *does* appear because we print it
% out.

at_element_k(Dict) :-
   taylor{  sum         : SumFromKmaxBackwards  % the output value, to be captured by the caller
           ,k           : K                     % index of the current term/element in the Taylor sum
           ,kmax        : Kmax                  % max index for which a term/element will be computed
           ,zsq         : Zsq                   % z², a constant
           ,ttkf_prior  : TTKFprior             % prior "two times k factorial" i.e. (2*(k-1))!
           ,ztt2k_prior : ZTT2Kprior            % prior "z to the 2*k" i.e. z^(2*(k-1))
           ,verbose     : Verbose } :< Dict,    % emit messages about progress if Verbose = verbose

   minus_one_tothe_k(M1TTK,K),                       % compute (-1)^K
   two_times_k_factorial(TTKF,K,TTKFprior),          % compute (2*k)! based on prior value
   z_tothe_2k(ZTT2K,K,Zsq,ZTT2Kprior),               % compute z^(2*k) based on prior value
   ElementK is M1TTK * ZTT2K / TTKF,                 % compute value for Taylor sum term/element at k

   % (isn't there a better way to print conditionally?)

   ((Verbose = verbose) ->
      format("element(~d) = ~e\n",[K,ElementK])
      ; true),

   % create a NextDict from Dict for recursive call

   KP1 is K+1,
   put_dict( _{ sum        : SumFromKmaxBackwardsPrior
               ,k          : KP1
               ,ttkf_prior : TTKF
               ,ztt2k_prior: ZTT2K }, Dict, NextDict),

   % recursive call 
   % (foundational thought: the procedure is really a **channel-doing-computations between the series of dicts**)

   at_element_k(NextDict),

   % on return, complete summing the Taylor series backwards from highest index to the current index k

   SumFromKmaxBackwards is SumFromKmaxBackwardsPrior + ElementK,

   % (more conditional printing)

   ((Verbose = verbose) ->
      format("taylor-series-sum(~d ... ~d) = ~e (added ~e to prior value ~e)\n",
            [K,Kmax,SumFromKmaxBackwards,ElementK,SumFromKmaxBackwardsPrior])
      ; true).

Это более читабельно? Я так полагаю.

person David Tonhofer    schedule 13.02.2020
comment
Непосредственно (-1)^K можно вычислить с помощью (is)/2! (Нечетный порядок слов, чтобы обойти предупреждение) - person false; 14.02.2020
comment
Обновление синтаксиса? О, пожалуйста, у нас было такое разделение несколько лет назад и это все еще очень больно. Каждый. - person false; 14.02.2020
comment
@false Ну, есть Perl 5 и Perl 6, которые теперь называются Raku и есть в этом тоже есть разногласия (и я еще даже не смотрел на Perl 6). Перерыв в совместимости был санкционирован с самого начала проекта и сразу позволил внести некоторые изменения, предложенные Ларри Уоллом в его первоначальном выступлении. Исторические бородавки, такие как путаница, связанная с использованием сигилов для контейнеров; неоднозначность между функциями выбора; синтаксическое влияние файловых дескрипторов bareword;... - person David Tonhofer; 14.02.2020
comment
Только что проверил версию, к которой относятся мои две книги по Perl: 4.0. - person false; 14.02.2020