Does prolog fit my use case?

is Prolog (swiProlog) a suitable tool for finding combinations between various facts in the knowledge base that are compatible with each other based on defined rules.
Is this good for doing this type of work for large knowledge bases of approximately 3000 facts ?

I ask because I am thinking of using Prolog for a project where I can get a combination of parts to build a computer system based on rules I defined which will act upon the attributes of the various facts.

2 Likes

Yes --your problem statement is almost the definition of what Prolog is good at – although you might need some help in representing the facts for efficient and easy-to-understand access (and feel free to ask here).

BTW, 3000 facts is not “large”. :wink:
SWI-Prolog can index millions of facts for efficient access.

3 Likes

Than you for your reply. I will start working on how I will structure my facts.

1 Like

One hint: write them down the way you are most comfortable with regarding producing and reading them. Worry about how you will use them later. You can ask Prolog to do macro-expansion on it such that they appear in the database in a form that can be processed efficiently. With only a couple of thousands facts this step might not be needed anyway. This also applies to how you formulate your rules.

3 Likes

Jan, Thanks for the tip :slightly_smiling_face:

1 Like

I have tried JPL (Java Prolog library) with a java spring application ,which is pretty neat.

I am thinking of two options to have the Prolog part of my application be available for other applications to consume:

  1. have Prolog part of the application embedded in JAVA using JPL
  2. create a web service in prolog using the http server libraries and the JSON libraries where I can accept JSON requests and return the answers via JSON.

What are the pros and cons of using either method ? Also in you experience which one you think would be best ?

It all depends. Using JAVA and JPL, embedding Prolog typically makes your Polog relatively hard to develop and debug as you have none or only limited access to the goodies such as the Prolog toplevel, dynamic reloading, debuggers, etc. With some persistency you can get some of that running, but it isn’t all that natural. Also debugging issues in the Java/Prolog interaction or crashes can be pretty hard. The ood points are that the latency is really small and you do not need a socket and thus need not to deal with authentication, etc. On some platforms JPL can be rather nasty to get configured correctly.

Using a Prolog server gets you mostly all the negations of the above: better development, higher latency and you may need to protect the server. You can generalise a little using the Pengine infrastructure. You can specialize using something directly on top of sockets so you can avoid the HTTP protocol overhead, use a dedicated data serialization to simplify I/O and, on Unix, you can use Unix domain sockets (file system entries) to simplify authentication.

Typically, I’d go for a network solution.

1 Like

I decided to have prologue running in its own process and it worked out pretty well when i comes to debugging.

When queries over the large amount of devices I have it takes a very long time . see below.

My facts about devices are of the form.

device(DeviceId,Price,DeviceType,Model,Brand,DeviceAttributeList,CategoryList).

I have around 2500 devices in total in my knowledge base.

Here is an example.

device(‘58cec6679af27f6bc3c5e6e4’, 629.99, desktop, ‘TGMBG860X0025GN’, ‘CybertronPC’, [processor_brand=[amd], number_of_ps2_ports=[‘2’], hard_drive_capacity=[‘1000’], graphics_processing_unit_gpu=[‘amd radeon r7 360’], system_memory_ram_speed=[‘1600’], processor_model=[‘amd athlon ii x4’], cache_memory=[‘4’], wireless_networking_b=[none], number_of_hdmi_output_ports=[‘1’], video_memory_type=[dedicated], graphics_type=[discrete], processor_speed=[‘3.7’], number_of_usb_ports=[‘6’], operating_system=[‘windows 10’], processor_number_of_cores=[‘4’], brand=[cybertronpc], optical_drive_type=[‘dvd+r dl’], port_types=[usb], enclosure_type=[‘mid tower’], system_memory_ram=[‘8’], media_card_reader_b=[no], number_of_dvi_ports=[‘1’], graphics_card=[‘amd radeon r7 360’], ethernet=[integrated], video_memory=[‘2048’], number_of_ethernet_ports=[‘1’], system_memory_ram_expandable_ports=[‘16’], keyboard_included_b=[yes], operating_system_architecture=[‘64-bit’], mouse_included_b=[yes]], [‘Computers & Tablets’, ‘Desktop & All-in-One Computers’, ‘All Desktops’]).

The following query was able to be executed in approximately 20 seconds.

–query1–

statistics(runtime,[Start|]),device(Id1,Price1,desktop,Model1,Brand1,,),device(Id2,Price2,keyboard,Model2,Brand2,,),device(Id3,Price3,mouse,Model3,Brand3,,),device(Id4,Price4,monitor,Model4,Brand4,,), TotalPrice is Price1+Price2+Price3+Price4, TotalPrice >=200,TotalPrice=<300,statistics(runtime,[Stop|]),Runtime is Stop - Start.

When I decided to add the predicate member(built_in_webcam_b=[‘yes’],Attr1) to make sure device with Id1 has in it’s DeviceAttributeList the member built_in_webcam_b=[‘yes’] . This query seems like it will take a long time to execute as I had to stop it after having it run for 5 minutes.

–query 2–

statistics(runtime,[Start|]),device(Id1,Price1,desktop,Model1,Brand1,Attr1,),member(built_in_webcam_b=[‘yes’],Attr1),device(Id2,Price2,keyboard,Model2,Brand2,,),device(Id3,Price3,mouse,Model3,Brand3,,),device(Id4,Price4,monitor,Model4,Brand4,,), TotalPrice is Price1+Price2+Price3+Price4, TotalPrice >=200,TotalPrice=<300,statistics(runtime,[Stop|_]),Runtime is Stop - Start.

I am not sure if it is best to implement an attribute list or should they be implement as predicate facts. for example like

attribute(DeviceId,Attribute,Value).

I would love for you input as to how a model the facts and what tips you may offer on how I can make things more efficient and increase query performance

I found the profile(Goal) predicate and decided to use it on various queries. What are some ways that I can improve performance of these queries.
I ran the queries on a system with
Intel Core i5 4590 (4cores) 3.5 GHZ
24G Ram

See profiling of the queries below.

 30 ?- profile((device(Id1,Price1,desktop,Model1,Brand1,Attr1,_),member(built_in_webcam_b=['yes'],Attr1),member(processor_number_of_cores=[Cores|_],Attr1),atom_number(Cores,Cores2),Cores2>=2,device(Id2,Price2,keyboard,Model2,Brand2,_,_),device(Id3,Price3,mouse,Model3,Brand3,Attr3,_),device(Id4,Price4,monitor,Model4,Brand4,_,_), TotalPrice is Price1+Price2+Price3+Price4,TotalPrice >=100,TotalPrice=<400)).
=====================================================================
Total time: 224.375 seconds
=====================================================================
Predicate                       Box Entries =    Calls+Redos     Time
=====================================================================
device/7                        345,163,990 =  828,283+344,335,707 43.0%
is/2                            344,335,024 =344,335,024+0        29.2%
=</2                            344,335,024 =344,335,024+0        12.4%
>=/2                            344,335,061 =344,335,061+0         8.9%
,/2                                       1 =        1+0         6.5%
atom_number/2                            37 =       37+0         0.0%
lists:member_/3                         832 =      758+74        0.0%
member/2                                758 =      758+0         0.0%
false.


29 ?- profile((device(Id1,Price1,desktop,Model1,Brand1,Attr1,_),member(built_in_webcam_b=['yes'],Attr1),member(processor_number_of_cores=['4'],Attr1),device(Id2,Price2,keyboard,Model2,Brand2,_,_),device(Id3,Price3,mouse,Model3,Brand3,Attr3,_),device(Id4,Price4,monitor,Model4,Brand4,_,_), TotalPrice is Price1+Price2+Price3+Price4,TotalPrice >=100,TotalPrice=<400)).
=====================================================================
Total time: 178.547 seconds
=====================================================================
Predicate                       Box Entries =    Calls+Redos     Time
=====================================================================
device/7                        270,534,094 =  649,195+269,884,899 44.8%
is/2                            269,884,208 =269,884,208+0        27.7%
=</2                            269,884,208 =269,884,208+0        11.7%
>=/2                            269,884,208 =269,884,208+0         8.6%
,/2                                       1 =        1+0         7.2%
lists:member_/3                         824 =      758+66        0.0%
member/2                                758 =      758+0         0.0%
false.


29 ?- profile((device(Id1,Price1,desktop,Model1,Brand1,Attr1,_),member(built_in_webcam_b=['yes'],Attr1),member(processor_number_of_cores=['4'],Attr1),device(Id2,Price2,keyboard,Model2,Brand2,_,_),device(Id3,Price3,mouse,Model3,Brand3,Attr3,_),device(Id4,Price4,monitor,Model4,Brand4,_,_), TotalPrice is Price1+Price2+Price3+Price4,TotalPrice >=100,TotalPrice=<400)).
=====================================================================
Total time: 178.547 seconds
=====================================================================
Predicate                       Box Entries =    Calls+Redos     Time
=====================================================================
device/7                        270,534,094 =  649,195+269,884,899 44.8%
is/2                            269,884,208 =269,884,208+0        27.7%
=</2                            269,884,208 =269,884,208+0        11.7%
>=/2                            269,884,208 =269,884,208+0         8.6%
,/2                                       1 =        1+0         7.2%
lists:member_/3                         824 =      758+66        0.0%
member/2                                758 =      758+0         0.0%
false.



26 ?- profile((device(Id1,Price1,desktop,Model1,Brand1,Attr1,_),member(built_in_webcam_b=['yes'],Attr1),device(Id2,Price2,keyboard,Model2,Brand2,_,_),device(Id3,Price3,mouse,Model3,Brand3,Attr3,_),device(Id4,Price4,monitor,Model4,Brand4,_,_), TotalPrice is Price1+Price2+Price3+Price4,TotalPrice >=100,TotalPrice=<400)).
=====================================================================
Total time: 224.156 seconds
=====================================================================
Predicate                       Box Entries =    Calls+Redos     Time
=====================================================================
device/7                        345,163,990 =  828,283+344,335,707 44.8%
is/2                            344,335,024 =344,335,024+0        27.9%
=</2                            344,335,024 =344,335,024+0        11.8%
>=/2                            344,335,024 =344,335,024+0         9.0%
,/2                                       1 =        1+0         6.5%
lists:member_/3                         758 =      721+37        0.0%
member/2                                721 =      721+0         0.0%
false.


25 ?- profile((device(Id1,Price1,desktop,Model1,Brand1,Attr1,_),member(built_in_webcam_b=['yes'],Attr1),device(Id2,Price2,keyboard,Model2,Brand2,_,_),device(Id3,Price3,mouse,Model3,Brand3,Attr3,_),device(Id4,Price4,monitor,Model4,Brand4,_,_), TotalPrice is Price1+Price2+Price3+Price4,TotalPrice >=500,TotalPrice=<600)).
=====================================================================
Total time: 24.203 seconds
=====================================================================
Predicate                       Box Entries =    Calls+Redos     Time
=====================================================================
device/7                         37,314,987 =   89,548+37,225,439 43.5%
is/2                             37,225,415 =37,225,415+0        28.1%
=</2                             37,225,415 =37,225,415+0        12.6%
>=/2                             37,225,415 =37,225,415+0        10.1%
,/2                                       1 =        1+0         5.8%
lists:member_/3                          34 =       30+4         0.0%
member/2                                 30 =       30+0         0.0%
Id1 = '58cec6679af27f6bc3c5e8c0',
Price1 = 447.99,
Model1 = 'BBY-CY44XFX',
Brand1 = 'Dell',
Attr1 = [processor_brand=[amd], number_of_ps2_ports=['0'], hard_drive_capacity=['500'], graphics_processing_unit_gpu=[amd], system_memory_ram_speed=['1600'], processor_model=['amd e2-series'], cache_memory=['2'], wireless_networking_b=['wireless-ac', 'wireless-ac (433 mbps)'], number_of_hdmi_output_ports=['1'], video_memory_type=[shared], display_type=['led-lcd'], graphics_type=[integrated], processor_speed=['1.8'], number_of_usb_ports=['4'], operating_system=['windows 10'], screen_size=['23.8'], processor_number_of_cores=['2'], built_in_webcam_b=[yes], brand=[dell], speakers_included_b=[yes], optical_drive_type=['dvd-r', 'dvd+r', 'dvd-rw', 'dvd+rw', 'cd-r', 'cd-rw'], port_types=[ethernet, usb, hdmi], system_memory_ram=['4'], screen_resolution=['1920 x 1080 (full hd)'], media_card_reader_b=[yes], graphics_card=['amd radeon r2'], bluray_player_b=[no], ethernet=['10/100/1000'], number_of_vga_ports=['0'], number_of_ethernet_ports=['1'], system_memory_ram_expandable_ports=['8'], has_bluetooth_enabled=[yes], operating_system_architecture=['64-bit'], multi_touch_screen_b=[no], audio_technology_b=['waves maxxaudio']],
Id2 = '58cec6679af27f6bc3c5e6af',
Price2 = 21.99,
Model2 = 'NS-PNK6811',
Brand2 = 'Insignia�',
Id3 = '58cec6679af27f6bc3c5e61a',
Price3 = 17.99,
Model3 = 'K72369US',
Brand3 = 'Kensington',
Attr3 = [ergonomic_design_b=[yes], interfaces=[usb], brand=[kensington]],
Id4 = '58cec6679af27f6bc3c5e677',
Price4 = 73.99,
Model4 = 'VS197DP',
Brand4 = 'Asus',
TotalPrice = 561.96.



24 ?- profile((device(Id1,Price1,desktop,Model1,Brand1,Attr1,_),device_attribute(Id1,built_in_webcam_b,yes),device(Id2,Price2,keyboard,Model2,Brand2,_,_),device(Id3,Price3,mouse,Model3,Brand3,_,_),device(Id4,Price4,monitor,Model4,Brand4,_,_), TotalPrice is Price1+Price2+Price3+Price4, TotalPrice >=200,TotalPrice=<500)).
=====================================================================
Total time: 135.563 seconds
=====================================================================
Predicate                       Box Entries =    Calls+Redos     Time
=====================================================================
device/7                        205,237,029 =  492,507+204,744,522 45.5%
is/2                            204,744,379 =204,744,379+0        27.7%
=</2                            204,744,379 =204,744,379+0        11.3%
>=/2                            204,744,379 =204,744,379+0         8.2%
,/2                                       1 =        1+0         7.2%
device_attribute/3                      167 =      167+0         0.0%
Id1 = '58cec6679af27f6bc3c5eebf',
Price1 = 410.99,
Model1 = '22-B010',
Brand1 = 'HP',
Attr1 = [processor_brand=[amd], number_of_ps2_ports=['0'], hard_drive_capacity=['1000'], graphics_processing_unit_gpu=[amd], processor_model=['amd a6-series'], cache_memory=['2'], wireless_networking_b=['wireless-n'], number_of_hdmi_output_ports=['1'], video_memory_type=['total available'], display_type=['widescreen led'], graphics_type=[discrete], processor_speed=['2'], number_of_usb_ports=['4'], number_of_displayport_outputs=['0'], operating_system=['windows 10'], screen_size=['21.5'], processor_number_of_cores=['4'], built_in_webcam_b=[yes], brand=[hp], optical_drive_type=['dvd+rw dl'], video_resolution=['1920 x 1080'], port_types=['usb 2.0', 'usb 3.0', ethernet, hdmi, headphone, microphone], system_memory_ram=['4'], screen_resolution=['1920 x 1080 (full hd)'], media_card_reader_b=[yes], number_of_dvi_ports=['0'], graphics_card=['amd radeon r4'], ethernet=['ethernet; fast ethernet; gigabit ethernet'], number_of_vga_ports=['0'], number_of_ethernet_ports=['1'], has_bluetooth_enabled=[yes]],
Id2 = '58cec6679af27f6bc3c5e6af',
Price2 = 21.99,
Model2 = 'NS-PNK6811',
Brand2 = 'Insignia�',
Id3 = '58cec6679af27f6bc3c5e7f2',
Price3 = 15.99,
Model3 = 'GM2400',
Brand3 = 'AZIO',
Id4 = '58cec6679af27f6bc3c5e993',
Price4 = 49.99,
Model4 = 'UM.IS0AA.C02.RB1',
Brand4 = 'Acer',
TotalPrice = 498.96000000000004.

Not sure if this will help. You ask about whether you should implement attributes as list or as a fact. One of the great things about Prolog is that you can have both quite easily using term expansion. If you put the following code before you consult your database of devices it will automatically create predicates for you. (Like a database index).

term_expansion( device(DeviceId,Price,DeviceType,Model,Brand,DeviceAttributeList,CategoryList) ,
     :- (
        % clean up 
        retractall(price(_,_)),
        retractall(type(_,_)),
        retractall(model(_,_)),
        retractall(brand(_,_)),
        retractall(attribute(_,_,_)),
        retractall(category(_,_)),
        retractall(deviceById(_,_)),

        assert(price(DeviceId, Price)),
        assert(type(DeviceId, DeviceType)),
        assert(model(DeviceId, Model)),
        assert(brand(DeviceId, Brand)),
        forall(member(Attribute=Values, DeviceAttributeList), (forall(member(V,Values), assert(attribute(DeviceId,Attribute,V))))),
        forall(member(Value, CategoryList), assert(category(DeviceId,Value))),
        assert(deviceById(DeviceId,device(DeviceId,Price,DeviceType,Model,Brand,DeviceAttributeList,CategoryList)))
    )).

Listing the database with your example fact. I see:

model('58cec6679af27f6bc3c5e6e4', 'TGMBG860X0025GN').

deviceById('58cec6679af27f6bc3c5e6e4', device('58cec6679af27f6bc3c5e6e4', 629.99, desktop, 'TGMBG860X0025GN', 'CybertronPC', [processor_brand=[amd], number_of_ps2_ports=['2'], hard_drive_capacity=['1000'], graphics_processing_unit_gpu=['amd radeon r7 360'], system_memory_ram_speed=['1600'], processor_model=['amd athlon ii x4'], cache_memory=['4'], wireless_networking_b=[none], number_of_hdmi_output_ports=['1'], video_memory_type=[dedicated], graphics_type=[discrete], processor_speed=['3.7'], number_of_usb_ports=['6'], operating_system=['windows 10'], processor_number_of_cores=['4'], brand=[cybertronpc], optical_drive_type=['dvd+r dl'], port_types=[usb], enclosure_type=['mid tower'], system_memory_ram=['8'], media_card_reader_b=[no], number_of_dvi_ports=['1'], graphics_card=['amd radeon r7 360'], ethernet=[integrated], video_memory=['2048'], number_of_ethernet_ports=['1'], system_memory_ram_expandable_ports=['16'], keyboard_included_b=[yes], operating_system_architecture=['64-bit'], mouse_included_b=[yes]], ['Computers & Tablets', 'Desktop & All-in-One Computers', 'All Desktops'])).

type('58cec6679af27f6bc3c5e6e4', desktop).

category('58cec6679af27f6bc3c5e6e4', 'Computers & Tablets').
category('58cec6679af27f6bc3c5e6e4', 'Desktop & All-in-One Computers').
category('58cec6679af27f6bc3c5e6e4', 'All Desktops').

brand('58cec6679af27f6bc3c5e6e4', 'CybertronPC').

attribute('58cec6679af27f6bc3c5e6e4', processor_brand, amd).
attribute('58cec6679af27f6bc3c5e6e4', number_of_ps2_ports, '2').
attribute('58cec6679af27f6bc3c5e6e4', hard_drive_capacity, '1000').
attribute('58cec6679af27f6bc3c5e6e4', graphics_processing_unit_gpu, 'amd radeon r7 360').
attribute('58cec6679af27f6bc3c5e6e4', system_memory_ram_speed, '1600').
attribute('58cec6679af27f6bc3c5e6e4', processor_model, 'amd athlon ii x4').
attribute('58cec6679af27f6bc3c5e6e4', cache_memory, '4').
attribute('58cec6679af27f6bc3c5e6e4', wireless_networking_b, none).
attribute('58cec6679af27f6bc3c5e6e4', number_of_hdmi_output_ports, '1').
attribute('58cec6679af27f6bc3c5e6e4', video_memory_type, dedicated).
attribute('58cec6679af27f6bc3c5e6e4', graphics_type, discrete).
attribute('58cec6679af27f6bc3c5e6e4', processor_speed, '3.7').
attribute('58cec6679af27f6bc3c5e6e4', number_of_usb_ports, '6').
attribute('58cec6679af27f6bc3c5e6e4', operating_system, 'windows 10').
attribute('58cec6679af27f6bc3c5e6e4', processor_number_of_cores, '4').
attribute('58cec6679af27f6bc3c5e6e4', brand, cybertronpc).
attribute('58cec6679af27f6bc3c5e6e4', optical_drive_type, 'dvd+r dl').
attribute('58cec6679af27f6bc3c5e6e4', port_types, usb).
attribute('58cec6679af27f6bc3c5e6e4', enclosure_type, 'mid tower').
attribute('58cec6679af27f6bc3c5e6e4', system_memory_ram, '8').
attribute('58cec6679af27f6bc3c5e6e4', media_card_reader_b, no).
attribute('58cec6679af27f6bc3c5e6e4', number_of_dvi_ports, '1').
attribute('58cec6679af27f6bc3c5e6e4', graphics_card, 'amd radeon r7 360').
attribute('58cec6679af27f6bc3c5e6e4', ethernet, integrated).
attribute('58cec6679af27f6bc3c5e6e4', video_memory, '2048').
attribute('58cec6679af27f6bc3c5e6e4', number_of_ethernet_ports, '1').
attribute('58cec6679af27f6bc3c5e6e4', system_memory_ram_expandable_ports, '16').
attribute('58cec6679af27f6bc3c5e6e4', keyboard_included_b, yes).
attribute('58cec6679af27f6bc3c5e6e4', operating_system_architecture, '64-bit').
attribute('58cec6679af27f6bc3c5e6e4', mouse_included_b, yes).

price('58cec6679af27f6bc3c5e6e4', 629.99).

which you can query against.

This is basically a good advice. There is a but though: avoid assert/retract and other global state from term_expansion/2. Just return a list of clauses and the system will take care of the cleanup and associate all clauses with the file and line from where the original term was read.

That said, it will at best cut the runtime (almost) in half according to the profile output. Without any layout, both query and data are really hard to read. I think the thing finds a desktop, keyboard, mouse and monitor with a total combined price between 200 and 300, no? Implemented natively like this, the enumerates all possible combinations, considering is/2, these are about 344 million.

You have two options. In native Prolog you need to design your own query plan. What could that be? Well, the mouse and keyboard do little for the price. You can find the highest and lowest prices and combine these. Now you can combine desktops + monitors that combined with the most expensive mouse/keyboard exceed the min and combined with the cheapest does not exceed the max. Some more subtle approaches are possible (e.g. sort the data).

Alternatively you can express this as a constraint problem. I’m not very experienced in that and don’t know the most elegant modeling.

P…s. Simply ?- time(Goal). is an easier way to time on a goal.

Thanks Jan, I would have preferred to just return a list in the second term of term_expansion, however I wanted to expand the embedded lists. In general, how would I go about expanding

fruits([apple,banana,cherry,date]).

into

fruit(apple).
fruit(banana).
fruit(cherry).
fruit(date).

without using the :- mode of term_expansion, and explicitly using assert/retract?

Not sure I understand your question, but are you looking for something like:

:- multifile(user:term_expansion/2).
user:term_expansion(fruits(Fruits), [(:- dynamic(fruit/1)| Clauses]) :-
    findall(fruit(Fruit), member(Fruit, Fruits), Clauses).
1 Like

Thanks Ian and jan.
Jan, When you were referring to sort the data, do you suggest I sort the facts in the file or were you referring to another way of doing it ?

That is a bit of an open question. It depends on the algorithm you end up with. Sorting the facts using term_expansion/2 can be done, but is a bit more tricky. I’d first establish how to approach this and possibly use runtime sorting, e.g., using order_by/2. That is really easy.

Thank you Jan. I review my decide on a solution.
You mention it is best to return a list of the new terms instead of using assert and retractall
using the term expansion below, I get warning indicating the

Clauses of device_brand/2 are not together in the source-file
Clauses of device_model/2 are not together in the source-file
Clauses of device_type/2 are not together in the source-file

term_expansion(device(Id,Price,DeviceType,Model,Brand,,), [price(Id,Price),device_type(Id,DeviceType),device_model(Id,Model),device_brand(Id,Brand)]).

In addition how can term expansion for new devices that will be submitted remotely to a swiprolog http server ?

To get rid of the warnings, try something like:

:- multifile(user:term_expansion/2).
user:term_expansion(
    begin_of_file,
    [begin_of_file, (:- discontiguous(device_brand/2), ...]
).

Because your query is depending on the sum of price you can trim the search space tremendously by inserting conditions in the middle like this:

   device(Id1,Price1,desktop,_,_,_,_),
   Price1 =< 300,
   device(Id2,Price2,keyboard,_,_,_,_),
   Price1 + Price2 =< 300,
   device(Id3,Price3,mouse,_,_,_,_),
   Price1 + Price2 + Price3 =< 300,
   device(Id4,Price4,monitor,_,_,_,_),
   TotalPrice is Price1+Price2+Price3+Price4,
   TotalPrice >= 200, TotalPrice =< 300,

Also, if you sort the items by price it will speed up things even more.

For best performance you want to arrange the test for the most expensive items first, so you want to put the monitor before the keyboard and mouse (the idea is to fail as early as possible, so we don’t waste time further down the tree):

   device(Id1,Price1,desktop,_,_,_,_),
   Price1 =< 300,
   device(Id2,Price2,monitor,_,_,_,_),
   Price1 + Price2 =< 300,
   device(Id3,Price3,keyboard,_,_,_,_),
   Price1 + Price2 + Price3 =< 300,
   device(Id4,Price4,mouse,_,_,_,_),
   TotalPrice is Price1+Price2+Price3+Price4,
   TotalPrice >= 200, TotalPrice =< 300

In my little test with random prices I get a tremendous speed up by trimming the search space with intermediate conditions:

5 ?- time(query(trimmed_search)).
desktop:  devid1 $233 model1 hp
keyboard: devid14 $5 model7 ibm
mouse:    devid8 $24 model4 ibm
monitor:  devid11 $22 model6 hp
% 5,030 inferences, 0.002 CPU in 0.002 seconds (100% CPU, 2163996 Lips)
true .

6 ?- time(query(plain)).
desktop:  devid1 $233 model1 hp
keyboard: devid14 $5 model7 ibm
mouse:    devid8 $24 model4 ibm
monitor:  devid11 $22 model6 hp
% 225,030,026 inferences, 22.867 CPU in 22.896 seconds (100% CPU, 9840674 Lips)
true .

That’s almost 12, 000 times faster!

2 Likes

Solution using CLP(FD)

You can also solve the problem in a more generic way using constraint logic programming ( CLP(FD) ), we even get a little faster query.

   :- use_module(library(clpfd)).
   % [...]
   query :-
      Price1 #> 0,Price2 #> 0,Price3 #> 0,Price4 #> 0,
      TotalPrice #= Price1+Price2+Price3+Price4,
      TotalPrice #>= 200, TotalPrice #=< 300,
      device(Id1,Price1,desktop,_,_,_,_),
      device(Id2,Price2,keyboard,_,_,_,_),
      device(Id3,Price3,mouse,_,_,_,_),
      device(Id4,Price4,monitor,_,_,_,_).

Results:

12 ?- time(query(plain)).
desktop:  devid120001 $199 model60001 hp
keyboard: devid120061 $91 model60031 hp
mouse:    devid120168 $4 model60084 ibm
monitor:  devid120083 $5 model60042 hp
% 1,050,685,131 inferences, 126.783 CPU in 127.403 seconds (100% CPU, 8287246 Lips)
true .

13 ?- time(query(clpfd)).
desktop:  devid120001 $199 model60001 hp
keyboard: devid120061 $91 model60031 hp
mouse:    devid120168 $4 model60084 ibm
monitor:  devid120083 $5 model60042 hp
% 4,924 inferences, 0.001 CPU in 0.001 seconds (98% CPU, 4347788 Lips)
true .

14 ?- 

In my test with random prices, the plain query takes 127 seconds and the CLP(FD) takes 1 millisecond!

Notice the following is needed:

  1. The constraints need to be added in the beginning of the query, before calling the device/[...] predicates.
  2. A constraint indicating that prices are positive numbers is required, otherwise you will not see any speed difference (because it will not restrict the domain for Prices).

NB: I write this solution here because it is a generic method that could be applied to other problems.

EDIT for floating point prices: I noticed you are using floating point numbers for prices (with two digits for the fractional part). CLP(FD) only works with integers. To solve this, simply multiply all prices by 100 to obtain an integer to be used in the device/... predicate; then, after you obtain the result, divide by 100 for display.

3 Likes