├── LICENSE ├── README.md ├── docs ├── CLP_BNR_Guide │ ├── CLPBNR_AlternativeSearch.myw │ ├── CLPBNR_Appendices.myw │ ├── CLPBNR_DevTools.myw │ ├── CLPBNR_Module.myw │ ├── CLPBNR_PrgCons.myw │ ├── CLPBNR_Reference.myw │ ├── CLPBNR_SynSem.myw │ ├── CLPBNR_UC_globalopt.myw │ ├── CLPBNR_UC_integrate.myw │ ├── CLPBNR_UC_linearsys.myw │ ├── CLPBNR_UC_metaContractors.myw │ ├── CLPBNR_UC_optionTrading.myw │ ├── CLPBNR_UC_polynomialRoots.myw │ ├── CLPBNR_UC_salesman.myw │ ├── CLPBNR_UC_scheduling.myw │ ├── CLPBNR_UC_timingAnalysis.myw │ ├── CLPBNR_UsingConstraints.myw │ ├── CLP_BNR_Guide.html │ ├── CLP_BNR_Guide.myw │ ├── MyWordDocStyle.mmk │ ├── images │ │ ├── Burma14_LA.png │ │ ├── Eclipse_tutorial044flat.png │ │ ├── EgMinConfig.png │ │ ├── EgWaveforms.png │ │ ├── P_K_KT_1.png │ │ ├── P_K_KT_L_LA_1.png │ │ ├── P_K_KT_L_LA_R1.png │ │ ├── P_K_KT_L_LA_R2.png │ │ ├── Salesman_LA.png │ │ ├── SimpleCircuit.png │ │ ├── TrafficExample5.png │ │ ├── Ulysses16_LA.png │ │ ├── circle-parabola.png │ │ ├── dependency.png │ │ ├── linearConvergance.png │ │ ├── simple_quad.png │ │ └── timing80286_2716.png │ ├── lib │ │ ├── commonmark.min.js │ │ ├── grit.js │ │ ├── markit.js │ │ ├── x-markup.js │ │ └── x-markup.mmk │ ├── pkgs │ │ ├── asciimath.mmk │ │ ├── demo.mmk │ │ ├── highlight.mmk │ │ ├── highlight │ │ │ ├── LICENSE │ │ │ ├── highlight.pack.js │ │ │ └── styles │ │ │ │ └── xcode.css │ │ ├── toc.mmk │ │ └── tsv.mmk │ └── supplemental │ │ ├── 2142-datasheet.pdf │ │ ├── 2716-datasheet.pdf │ │ ├── 8086-datasheet.pdf │ │ ├── 8282-datasheet.pdf │ │ ├── 8286-datasheet.pdf │ │ ├── camels.pl │ │ ├── colouredBins.pl │ │ ├── integrateV.pl │ │ ├── linear.pl │ │ ├── scheduling.pl │ │ ├── travellingSalesman.pl │ │ └── ts_data_USA.pl ├── asciiMath.html └── asciiMath.myw ├── pack.pl ├── prolog ├── clpBNR.pl ├── clpBNR │ ├── ia_primitives.pl │ ├── ia_simplify.pl │ └── ia_utilities.pl ├── clpBNR_search.pl └── clpBNR_toolkit.pl └── swish ├── clpBNR_quickstart.swinb └── index.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2025 Rick Workman (ridgeworks) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CLP(BNR) 2 | 3 | `clpBNR` is an implementation of CLP(BNR) structured as a package for SWI-Prolog. 4 | CLP(BNR) is an instance of CLP(*R*), i.e., CLP over the domain of real numbers. It differs from some other CLP(*R*)'s in that: 5 | * CLP(BNR) is complete in that any real number can be finitely represented even though the set of reals is infinite. It does this by sacrificing precision: a real number *R* is represented by an interval (*L,U*) where *L=3}. 51 | M = 6, 52 | N = 2. 53 | 54 | ?- [X,Y]::real, {1==X + 2*Y, Y - 3*X==0}. 55 | X:: 0.1428571428571428..., 56 | Y:: 0.428571428571428... . 57 | 58 | In the first example, both `M` and `N` are narrowed by applying the constraint `{M == 3*N}`. Applying the additional constraint `{M>3}` further narrows the intervals to single values so the original variables are unified with the point (integer) values. 59 | 60 | In the last example, the constraint causes the bounds to contract almost to a single point value, but not quite. The underlying reason for this is that standard floating point arithmetic is mathematically unsound due to floating point rounding errors and cannot represent all the real numbers in any case, due to the finite bit length of the floating point representation. Therefore all the interval arithmetic primitives in CLP(BNR) round any computed result outwards (lower bound towards -infinity, upper bound towards infinity) to ensure that any answer is included in the resulting interval, and so is mathematically sound. (This is just a brief, informal description; see the literature on interval arithmetic for a more complete justification and analysis.) This example also demonstrates the more compact "ellipsis postfix" form used to output `real` intervals whose bounds have narrowed so that at least the first 3 digits match. This is not actually a goal but does present the domain in a more readable form. (A strict "goal" format including constraints is supported by enabling a CLP(BNR) environment flag.) 61 | 62 | Sound constraints over real intervals (since interval ranges are closed) include `==`, `=<`, and `>=`, while `<>`, `<` and `>` are provided for `integer`'s. A fairly complete set of standard arithmetic operators (`+`, `-`, `*`, `/`, `**`), boolean operators (`and`, `or`, `xor`, `nand`, `nor`, `~`) and common functions (`exp`, `log`, `sqrt`, `abs`, `min`, `max` and standard trig and inverse trig functions) provided as interval relations. Note that these are relations so `{X==exp(Y)}` is the same as `{log(X)==Y}`. A few more examples: 63 | 64 | ?- {X==cos(X)}. 65 | X:: 0.73908513321516... . 66 | 67 | ?- {X>=0,Y>=0, tan(X)==Y, X**2 + Y**2 == 5}. 68 | X:: 1.096668128705471..., 69 | Y:: 1.94867108960995... . 70 | 71 | ?- {Z==exp(5/2)-1, Y==(cos(Z)/Z)**(1/3), X==1+log((Y+3/Z)/Z)}. 72 | Z:: 11.18249396070347..., 73 | Y:: 0.25518872031002..., 74 | X:: -2.0616342622472... . 75 | 76 | These examples did not require an explicit `::` declaration since the constraints were sufficient to narrow the values to acceptable answers. Internally, any undeclared interval is assigned infinite bounds which often inhibits narrowing possibilities, so it's good practice to always declare intervals with reasonable (or default) bounds values. 77 | 78 | It is often the case that the fixed point iteration that executes as a consequence of applying constraints is unable to generate meaningful solutions without applying additional constraints. This may be due to multiple distinct solutions within the interval range, or because the interval iteration is insufficiently "clever". For example: 79 | 80 | ?- {2==X*X}. 81 | X::real(-1.4142135623730951,1.4142135623730951). 82 | but 83 | 84 | ?- {2==X*X, X>=0}. 85 | X:: 1.414213562373095... . 86 | 87 | In more complicated examples, it may not be obvious what the additional constraints might be. In these cases a higher level search technique can be used, e.g., enumerating integer values or applying "branch and bound" algorithms over real intervals. A general predicate called `solve/1` is provided for this purpose. Additional application specific techniques can also be implemented. Some examples: 88 | 89 | ?- {2==X*X},solve(X). 90 | X:: -1.414213562373095... ; 91 | X:: 1.414213562373095... . 92 | 93 | ?- X::real, {0 == 35*X**256 - 14*X**17 + X}, solve(X). 94 | X:: -0.847943660827315... ; 95 | X:: 0.0... ; 96 | X:: 0.847943660827315... ; 97 | X:: 0.995842494200498... . 98 | 99 | ?- {Y**3+X**3==2*X*Y, X**2+Y**2==1},solve([X,Y]). 100 | Y:: 0.39105200..., 101 | X:: -0.92036858... ; 102 | Y:: -0.920368584..., 103 | X:: 0.39105200... ; 104 | Y:: 0.8931356..., 105 | X:: 0.4497874... ; 106 | Y:: 0.449787..., 107 | X:: 0.8931356... ; 108 | false. 109 | 110 | Note that all of these examples produce multiple solutions that are produced by backtracking in the top-level listener (just like any other top level query). `solve/1` is one of several predicates in CLP(BNR) which "search" for solutions. 111 | 112 | Here is the current set of operators and functions supported in this version: 113 | 114 | == is <> =< >= < > %% comparison (`is` synonym for `==`) 115 | + - * / %% basic arithmetic 116 | ** ^ %% includes real exponent, odd/even integer 117 | abs %% absolute value 118 | sqrt %% square root (needed?) 119 | min max %% min/max (arity 2) 120 | <= %% included (one way narrowing) 121 | and or nand nor xor -> , %% boolean (`,` synonym for `and`) 122 | - ~ %% unary negate and not 123 | exp log %% exp/ln 124 | sin asin cos acos tan atan %% trig functions 125 | integer %% must be an integer value 126 | sig %% signum of real, (-1,0,+1) 127 | 128 | Further explanation and examples, including a complete reference section, can be found in the [Guide to CLP(BNR)][clpBNR_UG]. Examples include problems in finite domains, and finding roots, global optima, and boundary value solutions to differential equations. Additional background material is available at [BNR Prolog Papers][bnrpp]. 129 | 130 | [ia1]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.44.6767 131 | [ia2]: http://fab.cba.mit.edu/classes/S62.12/docs/Hickey_interval.pdf 132 | [clip]: https://scholar.lib.vt.edu/ejournals/JFLP/jflp-mirror/articles/2001/S01-02/JFLP-A01-07.pdf 133 | [cldl]: http://interval.sourceforge.net/interval/index.html 134 | [swip]: http://www.swi-prolog.org 135 | [swish]: https://swish.swi-prolog.org/ 136 | [bnrpp]: https://ridgeworks.github.io/BNRProlog-Papers 137 | [clpBNR_UG]: https://ridgeworks.github.io/clpBNR/CLP_BNR_Guide/CLP_BNR_Guide.html 138 | [BNRParchive]: https://github.com/ridgeworks/BNRProlog-Source-Archive 139 | [Eclipse_org]: https://www.eclipseclp.org/ 140 | 141 | ## Getting Started 142 | 143 | CLP(BNR) is available online on SWISH - no downloading required; see the [CLP(BNR) QuickStart Guide](https://swish.swi-prolog.org/p/CLP(BNR)%20QuickStart%20Guide.swinb). 144 | 145 | If SWI-Prolog has not been installed, see [downloads](http://www.swi-prolog.org/Download.html). A current development release or stable release 9.1.22 or greater is required for `clpBNR` 0.11.5 or later. Earlier versions of `clpBNR` can run on older versions SWI-Prolog - see README's for those releases for details. (Past releases can be found in the repo "Releases" e.g., .) 146 | 147 | If you do not want to download this entire repo, a package can be installed using the URL `https://github.com/ridgeworks/clpBNR.git`. Once installed, it can be loaded with `use_module/1`. For example: 148 | 149 | ?- pack_install(clpBNR,[url('https://github.com/ridgeworks/clpBNR.git')]). 150 | % Cloning into '/Users/rworkman/.local/share/swi-prolog/pack/clpBNR'... 151 | Verify package status (anonymously) 152 | at "https://www.swi-prolog.org/pack/query" Y/n? 153 | % Contacting server at https://www.swi-prolog.org/pack/query ... ok 154 | % "clpBNR.git" was downloaded 2 times 155 | Package: clpBNR 156 | Title: CLP over Reals using Interval Arithmetic - includes Rational, Integer and Boolean domains as subsets. 157 | Installed version: 0.12.2 158 | Author: Rick Workman 159 | Home page: https://github.com/ridgeworks/clpBNR 160 | Download URL: https://github.com/ridgeworks/clpBNR.git 161 | Activate pack "clpBNR" Y/n? 162 | true. 163 | 164 | ?- use_module(library(clpBNR)). 165 | % *** clpBNR v0.12.2 ***. 166 | % Arithmetic global flags set to prefer rationals and IEEE continuation values. 167 | 168 | Or if the repository has been down downloaded, just consult `clpBNR.pl` (in `prolog/` directory) which will automatically include helper files in directory `clpBNR`. 169 | 170 | The `clpBNR` module declaration is: 171 | 172 | :- module(clpBNR, % SWI module declaration 173 | [ 174 | op(700, xfx, ::), 175 | (::)/2, % declare interval 176 | {}/1, % define constraint 177 | interval/1, % filter for clpBNR constrained var 178 | interval_degree/2, % number of constraints on clpBNR constrained var 179 | interval_goals/2, % list of goals to build clpBNR constrained var 180 | list/1, % O(1) list filter (also for compatibility) 181 | domain/2, range/2, % get type and bounds (domain) 182 | delta/2, % width (span) of an interval or numeric (also arithmetic function) 183 | midpoint/2, % midpoint of an interval (or numeric) (also arithmetic function) 184 | median/2, % median of an interval (or numeric) (also arithmetic function) 185 | lower_bound/1, % narrow interval to point equal to lower bound 186 | upper_bound/1, % narrow interval to point equal to upper bound 187 | 188 | % additional constraint operators 189 | op(200, fy, ~), % boolean 'not' 190 | op(500, yfx, and), % boolean 'and' 191 | op(500, yfx, or), % boolean 'or' 192 | op(500, yfx, nand), % boolean 'nand' 193 | op(500, yfx, nor), % boolean 'nor' 194 | op(700, xfx, <>), % integer not equal 195 | op(700, xfx, <=), % included (one way narrowing) 196 | 197 | % utilities 198 | print_interval/1, print_interval/2, % pretty print interval with optional stream 199 | small/1, small/2, % defines small interval width based on precision value 200 | solve/1, solve/2, % solve (list of) intervals using split to find point solutions 201 | splitsolve/1, splitsolve/2, % solve (list of) intervals using split 202 | absolve/1, absolve/2, % absolve (list of) intervals, narrows by nibbling bounds 203 | enumerate/1, % "enumerate" integers 204 | global_minimum/2, % find interval containing global minimum(s) for an expression 205 | global_minimum/3, % global_minimum/2 with definable precision 206 | global_maximum/2, % find interval containing global maximum(s) for an expression 207 | global_maximum/3, % global_maximum/2 with definable precision 208 | global_minimize/2, % global_minimum/2 plus narrow vars to found minimizers 209 | global_minimize/3, % global_minimum/3 plus narrow vars to found minimizers 210 | global_maximize/2, % global_maximum/2 plus narrow vars to found maximizers 211 | global_maximize/3, % global_maximum/3 plus narrow vars to found maximizers 212 | nb_setbounds/2, % non-backtracking set bounds (use with branch and bound) 213 | partial_derivative/3, % differentiate Exp wrt. X and simplify 214 | clpStatistics/0, % reset 215 | clpStatistic/1, % get selected 216 | clpStatistics/1, % get all defined in a list 217 | watch/2, % enable monitoring of changes for interval or (nested) list of intervals 218 | trace_clpBNR/1 % enable/disable tracing of clpBNR ops 219 | ]). 220 | 221 | Two additional modules are also included with this pack. `library(clpBNR_toolkit)` is a collection of useful utilities for global optimization problems or the use "meta-contractors" to improve performance. Also included is module `library(clpBNR_search)` which implements a compatible subset of the [ECLiPSe][Eclipse_org] search library supporting `clpBNR` finite and continuous domains (`integer`'s and `real`'s). 222 | 223 | Reference documentation for both libraries are included in the User Guide ([Guide to CLP(BNR)][clpBNR_UG]). 224 | 225 | ## SWI-Prolog Environment Flags 226 | 227 | This package sets the SWI-Prolog arithmetic global environment flags as follows: 228 | 229 | set_prolog_flag(prefer_rationals, true), % enable rational arithmetic 230 | set_prolog_flag(max_rational_size, 16), % rational size in bytes before .. 231 | set_prolog_flag(max_rational_size_action, float), % conversion to float 232 | 233 | set_prolog_flag(float_overflow,infinity), % enable IEEE continuation values 234 | set_prolog_flag(float_zero_div,infinity), 235 | set_prolog_flag(float_undefined,nan), 236 | 237 | The setting of these flags occurs when the first `clpBNR` attributed variable in a thread is created. This package will not work as intended if these flag values are not respected. 238 | 239 | Example output in the documentation is premised on flag `write_attributes` is set to `portray`. This (thread-local) flag will be set accordingly when the arithmetic flags are set but nothing prevents it from being subsequently overwritten (as is the case with any global flag). 240 | -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_AlternativeSearch.myw: -------------------------------------------------------------------------------- 1 | #### Alternative Search Strategies 2 | 3 | Finding solutions in CLP(BNR) (as in most CLP implementation) involves two "processes". The first is propagation which ensures that all constraints involving the domain variables (intervals in this case) are consistent. In addition the domains can be narrowed to remove values form the domain which are invalid. Propagation is largely invisible to the user; it just happens as a result of building the model of the problem by specifying the constraints in the language that the CLP implementation understands. 4 | 5 | The second process is the search for solutions since propagation is too weak to produce a solution by itself. In other cases, multiple solutions exist so additional work is required to separate them in a non-deterministic fashion. So an additional process of searching for solutions by problem subdivision is required. The idea is that narrowing of domains can be accomplished through subdivision such that some of the "sub-problems" can be proven invalid through propagation, and thus can be eliminated from the solution space. 6 | 7 | In a nutshell, searching subdivides the problem into a tree with solutions at the leaf nodes while propagation prunes the tree. The result is a tree where the leaf nodes represent solutions which can be enumerated as the tree is built or aggregated using well known techniques. 8 | 9 | Subdividing finite domains (`integer`s) results in an "enumeration" of the domain elements, i.e., choose on element and, on backtracking, remove the element from the domain and choose a different element until the domain is empty. 10 | 11 | Subdividing continuous domains (`real`s) is done by splitting the domain recursively and selecting one of them. If a sub-domain is inconsistent it is removed and then the other sub-domain is checked. Splitting stops when a sub-domain is too narrow according to the criteria defined by [**small/1**] (default controlled via flag `clpBNR_default_precision`). This ensures termination within an acceptable time. Regardless of the termination criteria, nothing guarantees the remaining sub-domain(s) contains a solution. Only the reverse applies, i.e., failure of constraint consistency through propagation proves there are no solutions in that sub-domain. 12 | 13 | Within this general framework, there is considerable flexibility on how the search tree is constructed. Given a list of domain variables (intervals in CLP(BNR)) the first type of choice is which variable from the list should be selected, commonly called the **Select** option. A second option, commonly called the **Choice** option defines how to subdivide the selected interval. The third option is the search *Method* which defines how the search tree should be constructed, e.g., whether it is a complete tree or a partial tree limited by depth or number of leaves/nodes. 14 | 15 | `clpBNR` supports three "built-in" predicates for searching and all build the complete search tree. `solve` and `splitsolve`'s primary focus is `real` intervals (continuous domain). `solve` selects intervals from their order in the input list and splits the interval at points which are not solutions, biased toward the midpoint of the interval. `splitsolve` selects the widest interval on the list always splits at the midpoint. For finite (`integer`) domains, the split point will always be an integer, and if the domain is small enough, enumeration (see next) is used instead of splitting. 16 | 17 | The `enumerate/1` predicate can only be used with integer domains. Selection is defined by the (flattened) list order and choice is from the smallest value in the domain to the largest. 18 | 19 | (For a more "in-depth" discussion of finite domain searching see the [ECLiPSe] tutorial [Tree Search Methods].) 20 | 21 | While the `clpBNR` built-in set of search predicates may be sufficient for many applications, many finite domain CLP's support many more search options. In particular, [ECLiPSe] includes several [search libraries] including `[fd_search]`. Module `clpBNR_search` is an adaptation of that library omitting some features, such as search tree rendering and SBDS library support, but extending it to support `real`s where applicable. This includes a search taxonomy based on search **Method** (including support for complete and partial searching), **Select** criteria, and **Choice** interval subdivision. **Method** and **Select** are independent of the interval type (`real` or `integer`); **Choice** semantics will depend on the type. 22 | 23 | Details of the `clpBNR_search` API can be found in the reference section (see [`clpBNR_search` reference]). 24 | 25 | <#TableOfContents> 26 | 27 | & 28 | [ECLiPSe] <- link https://eclipseclp.org/ 29 | [Tree Search Methods] <- link https://www.eclipseclp.org/doc/tutorial/tutorial086.html 30 | [search libraries] <- link https://eclipseclp.org/doc/libman/libman015.html 31 | [fd_search] <- link https://eclipseclp.org/doc/bips/lib/fd_search/search-6.html 32 | [`clpBNR_search` reference] <- link #toc3_clpBNR_search__Reference 33 | -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_Appendices.myw: -------------------------------------------------------------------------------- 1 | ### Appendices 2 | 3 | #### Appendix 1 - `travellingSalesman.pl` Source for all algorithms 4 | aside> ( [/*travellingSalesman.pl*/raw text] ) 5 | @include supplemental/travellingSalesman.pl 6 | 7 | #### Appendix 2 - Bin Packing Examples 8 | ##### `camels.pl` ( [/*camels.pl*/raw text] ) 9 | @include supplemental/camels.pl 10 | 11 | ##### `colouredBins.pl` ( [/*colouredBins.pl*/raw text] ) 12 | @include supplemental/colouredBins.pl 13 | <#TableOfContents> 14 | 15 | #### Appendix 3 - Scheduling Code 16 | ##### `scheduling.pl` ( [/*scheduling.pl*/raw text] ) 17 | @include supplemental/scheduling.pl 18 | 19 | & 20 | [/*travellingSalesman.pl*/raw text] <- link supplemental/travellingSalesman.pl 21 | [/*camels.pl*/raw text] <- link supplemental/camels.pl 22 | [/*colouredBins.pl*/raw text] <- link supplemental/colouredBins.pl 23 | [/*scheduling.pl*/raw text] <- link supplemental/scheduling.pl 24 | -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_DevTools.myw: -------------------------------------------------------------------------------- 1 | ### Development Tools for CLP(BNR) 2 | 3 | The version of CLP(BNR) as a SWI-Prolog package is entirely implemented in Prolog, so all the standard Prolog development tools (debugger, profiler, etc.) can be used with CLP(BNR) applications. However, the details of the execution of interval narrowing can be overwhelming - many millions of narrowing operations may be required in what may seem to be simple programs. To help with this problem `clpBNR` has been added as a [`debug` topic]. Enabling this topic will generate a trace of constraint additions and narrowing operation failures on the console: 4 | eg 5 | ?- debug(clpBNR). 6 | true. 7 | 8 | ?- X::real(0,10),{X=<5},{X>19}. 9 | % Add {_9178{real(0,10)}=<5} 10 | % Add {_9178{real(0,5)}>19} 11 | % ** fail ** 19<_9178{real(0,5)}. 12 | false. 13 | If this is insufficient, tracing of individual narrowing operations can be done using [**trace_clpBNR/1**]. However it's easy to become overwhelmed by the sheer volume of the trace information, so it's best to limit usage to smaller problems. An example: 14 | ?- trace_clpBNR(true). 15 | true. 16 | 17 | [debug] ?- [M,N]::integer(0,8), {M==3*N, M>3}. 18 | % _304974{integer(0,8)}==3*_305052{integer(0,8r3)}. 19 | % 3<_304974{integer(3.0000000000000004,8)}. 20 | % integral(_305052{integer(0,2)}). 21 | % integral(_304974{integer(4,8)}). 22 | % _304974{integer(4,6)}==3*_305052{integer(4r3,2)}. 23 | % 3<_304974{integer(4,6)}. 24 | % integral(2). 25 | % 6==3*2. 26 | M = 6, 27 | N = 2. 28 | For some problems, it's sufficient to just monitor the value of a interval (or a few intervals). When this is true, the sheer volume of tracing can be reduced using the [**watch/2**] predicate: 29 | eg 30 | ?- debug(clpBNR). 31 | true. 32 | 33 | ?- [M,N]::integer(0,8), watch([M,N],log), {M==3*N, M>3}. 34 | % Add {_57378{integer(0,8)}==3*_57456{integer(0,8)}} 35 | % Unify _58316{real(-1.0Inf,1.0Inf)} with _57378{integer(0,8)} 36 | % Add {_57378{integer(0,8)}>3} 37 | % Set value of _57456{integer(0,8)} to (0,8r3) 38 | % Set value of _57378{integer(0,8)} to (3.0000000000000004,8) 39 | % Set value of _57456{integer(0,8r3)} to (0,2) 40 | % Set value of _57378{integer(3.0000000000000004,8)} to (4,8) 41 | % Set value of _57378{integer(4,8)} to (4,6) 42 | % Set value of _57456{integer(0,2)} to (4r3,2) 43 | % Set value of _57456{integer(4r3,2)} to (2,2) 44 | % Unify _?{integer(4r3,2)} with 2 45 | % Set value of _57378{integer(4,6)} to (6,6) 46 | % Unify _?{integer(4,6)} with 6 47 | M = 6, 48 | N = 2. 49 | 50 | Setting the CLP(BNR) output mode to "verbose" (Prolog environment flag `clpBNR_verbose = true`) can also be helpful in some situations when just the residual constraints on intervals provide useful information. 51 | 52 | Finally, use [**clpStatistics**] to get a general idea of performance and narrowing operation counts. 53 | 54 | <#TableOfContents> 55 | 56 | & 57 | [`debug` topic] <- link https://www.swi-prolog.org/pldoc/man?section=debug 58 | [**watch/2**] <- link #toc4**watch**(*+Intervals,_++Atom*) 59 | [**trace_clpBNR/1**] <- link #toc4**trace_clpBNR**(*?Boolean*) 60 | [**clpStatistics**] <- link #toc4**clpStatistic**(*+Statistic*),_**clpStatistics**,_**clpStatistics**(*--Statistics*) -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_Module.myw: -------------------------------------------------------------------------------- 1 | ### Module clpBNR 2 | 3 | This version of CLP(BNR) is structured as a SWI-Prolog module with the following interface definition: 4 | .pl 5 | :- module(clpBNR, % SWI module declaration 6 | [ 7 | op(700, xfx, ::), 8 | (::)/2, % declare interval 9 | {}/1, % define constraint 10 | add_constraint/1, % more primitive define single constraint, bypass simplify 11 | interval/1, % filter for clpBNR constrained var 12 | interval_degree/2, % number of constraints on clpBNR constrained var 13 | interval_goals/2, % list of goals to build clpBNR constrained var 14 | list/1, % O(1) list filter (also for compatibility) 15 | domain/2, range/2, % get type and bounds (domain) 16 | delta/2, % width (span) of an interval or numeric (also arithmetic function) 17 | midpoint/2, % midpoint of an interval (or numeric) (also arithmetic function) 18 | median/2, % median of an interval (or numeric) (also arithmetic function) 19 | lower_bound/1, % narrow interval to point equal to lower bound 20 | upper_bound/1, % narrow interval to point equal to upper bound 21 | 22 | % additional constraint operators 23 | op(200, fy, ~), % boolean 'not' 24 | op(500, yfx, and), % boolean 'and' 25 | op(500, yfx, or), % boolean 'or' 26 | op(500, yfx, nand), % boolean 'nand' 27 | op(500, yfx, nor), % boolean 'nor' 28 | op(500, yfx, xor), % boolean 'xor' 29 | op(700, xfx, <>), % integer not equal 30 | op(700, xfx, <=), % included (one way narrowing) 31 | 32 | % utilities 33 | print_interval/1, print_interval/2, % pretty print interval with optional stream 34 | small/1, small/2, % defines small interval width based on precision value 35 | solve/1, solve/2, % solve (list of) intervals using split to find point solutions 36 | splitsolve/1, splitsolve/2, % solve (list of) intervals using split 37 | absolve/1, absolve/2, % absolve (list of) intervals, narrows by nibbling bounds 38 | enumerate/1, % "enumerate" integers 39 | global_minimum/2, % find interval containing global minimum(s) for an expression 40 | global_minimum/3, % global_minimum/2 with definable precision 41 | global_maximum/2, % find interval containing global maximum(s) for an expression 42 | global_maximum/3, % global_maximum/2 with definable precision 43 | global_minimize/2, % global_minimum/2 plus narrow vars to found minimizers 44 | global_minimize/3, % global_minimum/3 plus narrow vars to found minimizers 45 | global_maximize/2, % global_maximum/2 plus narrow vars to found maximizers 46 | global_maximize/3, % global_maximum/3 plus narrow vars to found maximizers 47 | nb_setbounds/2, % non-backtracking set bounds (use with branch and bound) 48 | partial_derivative/3, % differentiate Exp wrt. X and simplify 49 | clpStatistics/0, % reset 50 | clpStatistic/1, % get selected 51 | clpStatistics/1, % get all defined in a list 52 | watch/2, % enable monitoring of changes for interval or (nested) list of intervals 53 | trace_clpBNR/1 % enable/disable tracing of clpBNR ops 54 | ]). 55 | 56 | Documenting these predicates and how to use them is the focus of the rest of this document. 57 | 58 | See the [Getting Started] section of the `README` file for instructions on downloading and installing the `clpBNR` package for SWI-Prolog. 59 | 60 | <#TableOfContents> 61 | 62 | & 63 | [Getting Started] <- link https://github.com/ridgeworks/clpBNR_pl#getting-started 64 | -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_PrgCons.myw: -------------------------------------------------------------------------------- 1 | ### Logic Programming With Constraints 2 | 3 | #### Using Constraints to Restore Logical Behaviour to Arithmetic 4 | 5 | The conventional standard arithmetic which has been added to Prolog uses the primitives `is` and the various relational arithmetic operators of the forms: 6 | eg 7 | Var is Expression 8 | Expression Op Expression 9 | where `Expression` denotes a valid and fully instantiated arithmetic expression. Therefore any logic variables in expression must be instantiated before these goals are evaluated; otherwise failure or exceptions are raised. Since most applications need to do some arithmetic, this is one of the main reasons why they do not enjoy the same properties of the small teaching examples. 10 | 11 | Constraints, supported in CLP systems (like CLP(FD), CLP(Z), CLP(R), and CLP(BNR)), avoid early failure (or errors) by delaying arithmetic evaluation until the conditions are sufficient to do so. When this occurs, the logic variables in the "question" will be bound to specific numeric values in the "answer". But in some cases, this may never happen, so the answer just indicates the set of possible values the variable might have. In the continuous domain of reals supported by CLP(BNR) this is just the closed interval defined by an upper and lower bound. And where floating point values are mandated, a variable can never be bound to a precise value due to the inaccuracy in the underlying floating point representations and operations. While cumbersome this is necessary for reasons of correctness. (One would hope that a single floating point value would at least be in an acceptable range of possible answers, but for most computer arithmetic implementations even that isn't a certainty.) 12 | 13 | It is tempting to recommend that all arithmetic in Prolog programs should be done with constraints and restore the logical properties to those applications that use it, but that incurs a cost that may be unacceptable in many cases. For those applications where logical behaviour, or code simplicity is more important, this section illustrates how constraints may be used in Prolog applications. 14 | 15 | #### Example 1, List Length 16 | 17 | To get an idea of the what must be done to restore logical behaviour of predicates built with "illogical" arithmetic, here's the `listing` of the SWI Prolog system predicate `length/2`: 18 | eg 19 | ?- listing(length). 20 | system:length(List, Length) :- 21 | var(Length), 22 | !, 23 | '$skip_list'(Length0, List, Tail), 24 | ( Tail==[] 25 | -> Length=Length0 26 | ; var(Tail) 27 | -> Tail\==Length, 28 | '$length3'(Tail, Length, Length0) 29 | ; throw(error(type_error(list, List), context(length/2, _))) 30 | ). 31 | system:length(List, Length) :- 32 | integer(Length), 33 | Length>=0, 34 | !, 35 | '$skip_list'(Length0, List, Tail), 36 | ( Tail==[] 37 | -> Length=Length0 38 | ; var(Tail) 39 | -> Extra is Length-Length0, 40 | '$length'(Tail, Extra) 41 | ; throw(error(type_error(list, List), context(length/2, _))) 42 | ). 43 | system:length(_, Length) :- 44 | integer(Length), 45 | !, 46 | throw(error(domain_error(not_less_than_zero, Length), 47 | context(length/2, _))). 48 | system:length(_, Length) :- 49 | throw(error(type_error(integer, Length), context(length/2, _))). 50 | 51 | true. 52 | To be fair, part of the complexity is due to performance optimization (use of foreign language primitives to avoid recursion in Prolog) and part is due to the tendency of standard Prolog to generate errors under "exceptional" conditions, rather than treat them as a "failure of logic". But the key observation is that it has to be separated into two main cases: `Length` as a variable or `Length` as an integer. The various ways it can be used: 53 | eg 54 | ?- length( [1,2,3,4], N). 55 | N = 4. 56 | 57 | ?- length( X, 4). 58 | X = [_, _, _, _]. 59 | 60 | ?- length( [X|Xs], 4). 61 | Xs = [_, _, _]. 62 | 63 | ?- length( [_,_], 4). 64 | false. 65 | 66 | ?- length( [X|Xs], -3). 67 | ERROR: Domain error: `not_less_than_zero' expected, found `-3' 68 | ERROR: In: 69 | ERROR: [9] throw(error(domain_error(not_less_than_zero,-3),context(...,_5604))) 70 | ERROR: [7] 71 | ERROR: 72 | ERROR: Note: some frames are missing due to last-call optimization. 73 | ERROR: Re-run your program in debug mode (:- debug.) to get more detail. 74 | ?- length( [X|Xs], N). 75 | Xs = ([]), 76 | N = 1 ; 77 | Xs = [_], 78 | N = 2 ; 79 | Xs = [_, _], 80 | N = 3 ; 81 | Xs = [_, _, _], 82 | N = 4 ; 83 | Xs = [_, _, _, _], 84 | N = 5 . 85 | Here's a CLP(BNR) constraint based version (but an equivalent version could be just as easily constructed using CLP(FD) or a similar finite domain constraint system): 86 | .pl 87 | list_length([],0). % length of empty list is 0. 88 | list_length([X|Xs],N):- % length of non-empty list [X|Xs] is N if ... 89 | [N,N1]::integer(0,_), % list lengths are non-negative and finite integers, and 90 | {N1 is N - 1}, % N1 is one less than N, and 91 | list_length(Xs,N1). % length of Xs is N1 92 | Testing with the same set of queries: 93 | eg 94 | ?- list_length([1,2,3,4],N). 95 | N = 4. 96 | 97 | ?- list_length(X,4). 98 | X = [_, _, _, _] ; 99 | false. 100 | 101 | ?- list_length([X|Xs],4). 102 | Xs = [_, _, _] ; 103 | false. 104 | 105 | ?- list_length([_,_],4). 106 | false. 107 | 108 | ?- list_length([X|Xs],-3). 109 | false. 110 | 111 | ?- list_length([X|Xs],N). 112 | Xs = ([]), 113 | N = 1 ; 114 | Xs = [_], 115 | N = 2 ; 116 | Xs = [_, _], 117 | N = 3 ; 118 | Xs = [_, _, _], 119 | N = 4 ; 120 | Xs = [_, _, _, _], 121 | N = 5 . 122 | So what we've ended up with is a simple implementation that's semantically equivalent to the system predicate (modulo error handling) but is orders of magnitude worse in performance. Why did we bother? So we can do things like taming the infinite backtracker : 123 | eg 124 | ?- N::integer(5,7), list_length(L,N). 125 | N = 5, 126 | L = [_, _, _, _, _] ; 127 | N = 6, 128 | L = [_, _, _, _, _, _] ; 129 | N = 7, 130 | L = [_, _, _, _, _, _, _] ; 131 | false. 132 | The point of this exercise was not to invent a better version of `length/2`. The built-in version is very efficient and generally works the way you want. But if code complexity and readability are issues, which is usually the case for Prolog users, constraints restore the ability to write programs that are dependent on arithmetic in a very "logical" way. 133 | 134 | <#TableOfContents> 135 | 136 | #### Example 2, Summing a List 137 | 138 | Consider a predicate to sum a list of numeric quantities. Using standard arithmetic, the simple and obvious solution is: 139 | .pl 140 | sum([],0). 141 | sum([X|Xs], Sum):- sum(Xs,S), Sum is X+S. 142 | Note that we have to calculate `S` before performing the addition because the right hand side expression must be grounded. This version also has the drawback that it is not tail-recursive, consuming more stack storage, so it's not a great solution for long lists. A tail-recursive version introduces an auxiliary predicate and an accumulator variable: 143 | .pl 144 | sum(List,Sum) :- sum1(List,0,Sum). 145 | sum1([],Sum,Sum). 146 | sum1([X|Xs],S,Sum) :- S1 is S+X, sum1(Xs,S1,Sum). 147 | A constraint based version which is tail-recursive without needing the auxiliary predicate can be written because the order of the goals in the second clause can be reversed: 148 | .pl 149 | sum([],S) :- {S==0}. 150 | sum([X|Xs],Sum) :- {Sum == X+S}, sum(Xs,S). 151 | This version also permits S to be a numeric expression, e.g., `sum(L,2+Y)`, but if that is not a concern `sum/2` can be more simply expressed as: 152 | .pl 153 | sum([],0). 154 | sum([X|Xs],Sum) :- {Sum == X+S}, sum(Xs,S). 155 | So this does what the simple standard version does in a space efficient manner and it can sum mixed lists since CLP(BNR) supports integers and rationals as a subset of reals,: 156 | eg 157 | ?- sum([1,2,3],S). 158 | S = 6. 159 | 160 | ?- sum([1,2/3,3],S). 161 | S = 14r3. 162 | 163 | ?- sum([1.1,2.2,3],S). 164 | S = S:: 6.30000000000000... . 165 | 166 | ?- sum([1.1,22/10,3],S). 167 | S = S:: 6.30000000000000... . 168 | Note the "fuzzing" due to the presence of floating point constants. 169 | 170 | Since the logical properties of `sum/2` have been restored, it can be used in many different ways. In the most general case it generates all lists of all sums (manually terminated after three cases): 171 | eg 172 | ?- sum(L,S). 173 | L = [], 174 | S = 0 ; 175 | L = [_A], 176 | S::real(-1.0Inf, 1.0Inf), 177 | _A::real(-1.0Inf, 1.0Inf) ; 178 | L = [_A, _B], 179 | S::real(-1.0Inf, 1.0Inf), 180 | _A::real(-1.0Inf, 1.0Inf), 181 | _B::real(-1.0Inf, 1.0Inf) . 182 | As with other predicates of this nature, e.g., `list_length/2`, the potential for non-termination exists and must be managed. 183 | eg 184 | ?- length(L,3), L::integer(0,10), sum(L,S). 185 | L = [_A, _B, _C], 186 | _A::integer(0, 10), 187 | _B::integer(0, 10), 188 | _C::integer(0, 10), 189 | S::real(0, 30). 190 | Because CLP(BNR) only constrains numbers, order is important, i.e., the list length must be defined before `sum/2` is executed. The order can be changed provided the potential non-determinism is managed, e.g., by using cut (`!`): 191 | eg 192 | ?- sum(L,S), length(L,3), L::integer(0,10), !. 193 | L = [_A, _B, _C], 194 | S::real(0, 30), 195 | _A::integer(0, 10), 196 | _B::integer(0, 10), 197 | _C::integer(0, 10) . 198 | Other possible queries: 199 | eg 200 | ?- sum(L,10), L=[1,V,3], !. 201 | L = [1, 6, 3], 202 | V = 6. 203 | 204 | ?- S::integer(0,10), sum(L,S), L=[1,V,3], !. 205 | L = [1, V, 3], 206 | S::integer(0, 10), 207 | V::real(-4, 6). 208 | Another useful technique is to avoid doing arithmetic at all; just symbolically create the sum result and then constrain it. Or not, because it works equally well with standard Prolog arithmetic or another arithmetic CLP system. A symbolic version of `sum/2`: 209 | .pl 210 | sym_sum([],0). 211 | sym_sum([X|Xs], X+S) :- sym_sum(Xs,S). 212 | And some examples: 213 | eg 214 | ?- sym_sum([1,2,3],S), Sum is S. 215 | S = 1+(2+(3+0)), 216 | Sum = 6. 217 | 218 | ?- sym_sum([1,2,3],S),{Sum == S}. 219 | S = 1+(2+(3+0)), 220 | Sum = 6. 221 | 222 | ?- Sum::integer(0,10), sym_sum([1,V,3],S), {Sum == S}. 223 | S = 1+(V+(3+0)), 224 | Sum::integer(0, 10), 225 | V::real(-4, 6). 226 | 227 | ?- sym_sum([1,V-3,3],S), {Sum == S}, Sum::integer(0,10). 228 | S = 1+(V-3+(3+0)), 229 | Sum::integer(0, 10), 230 | V::real(-1, 9). 231 | A possible downside, from a pure logic programming perspective, is that the expression (`S` in the examples) must be instantiated before the constraint. Otherwise, `sym_sum` will try to unify an interval with the expression which fails. (Intervals can only be unified with numeric values and other intervals.) 232 | 233 | This pattern of working symbolically and only afterwards converting to constraints enables the same predicate to be used with different arithmetic systems, e.g., CLP(FD). It can be helpful during development because the expressions may be more readable before the logic variables are constrained. 234 | 235 | Counting booleans{#id cardinality} is an important use of integer constraints and since `boolean` variables are implemented as numeric intervals (`integer(0,1)`), `sym_sum/2` can be used to define a cardinality predicate which constrains the members of a list of boolean variables: 236 | .pl 237 | cardinality(Blist, L, H) :- sym_sum(Blist, Sum), C::integer(L,H), {C is Sum}. 238 | For a list of booleans, `Bs`, of length `N`: 239 | eg 240 | cardinality(Bs, 1, 1) % true if exactly one B is true (1) 241 | cardinality(Bs, M, M) % true if exactly M B's are true 242 | cardinality(Bs, M, N) % true if at least M B's are true 243 | cardinality(Bs, 0, M) % true if at most M B's are true 244 | cardinality(Bs, N, N) % true if all B's are true 245 | Possible uses of this predicate are described elsewhere in this document. 246 | 247 | <#TableOfContents> 248 | 249 | #### Example 3, N Factorial 250 | 251 | Nothing new; just another example: 252 | .pl 253 | n_factorial(0, 1). % !0 is 1. 254 | n_factorial(N, F) :- % !N is N*!(N-1) 255 | [N,F,F1]::integer(1,inf), % all values are positive integers, and 256 | {N1 == N-1, F == N*F1}, % N1 is N-1, F is N*F1, and 257 | n_factorial(N1, F1). % F1 is !N1. 258 | and examples of usage: 259 | eg 260 | ?- n_factorial(10,F). 261 | F = 3628800 ; 262 | false. 263 | 264 | ?- n_factorial(N,3628800). 265 | N = 10 ; 266 | false. 267 | 268 | ?- n_factorial(N,F). 269 | N = 0, 270 | F = 1 ; 271 | N = F, F = 1 ; 272 | N = F, F = 2 ; 273 | N = 3, 274 | F = 6 ; 275 | N = 4, 276 | F = 24 ; 277 | N = 5, 278 | F = 120 ; 279 | N = 6, 280 | F = 720 ; 281 | N = 7, 282 | F = 5040 . 283 | 284 | ?- n_factorial(N,42). 285 | false. 286 | Note that this implementation is non-deterministic (no "green" cut in the first clause) to enable generation of solutions (last example). 287 | 288 | Hopefully these simple examples demonstrate that constraints and logic programming are complementary. Not only are they conceptually compatible, but constraints provide the sort of arithmetic capability that Prolog has arguably always needed, while Prolog provides the symbolic processing and programming environment needed to make effective use of constraints. 289 | 290 | <#TableOfContents> 291 | 292 | #### Constraints and Prolog Negation 293 | 294 | The negation-by-failure construct of Prolog (`\+` or `not`) works with constraints according to the normal rules so "`\+ {C}`" succeeds only if the constraint C fails, i.e., `C` is provably inconsistent with the constraints already in the system. For example, if, having already declared variables `X` and `Y` and established constraints involving them, we ask: 295 | eg 296 | ... \+ {X>=Y} 297 | and it succeeds, it indicates that `X` cannot possibly be larger or equal to `Y`. Furthermore, there is a proof of this fact, which was discovered by carrying out the proof. Note also that negations of constraint goals are persistent, e.g., if `X` can not be larger (or equal) to `Y`, no additional constraints are going to change that. 298 | 299 | Recall that `{X` and `<>`) and one might be tempted to use `not({X>=Y})` as a substitute. This is complete but has the property that nothing is narrowed when applying this constraint due to the nature of Prolog negation-by-failure. Thus it is a legitimate test, i.e., there are no solutions for which `X 307 | 308 | & -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_UC_linearsys.myw: -------------------------------------------------------------------------------- 1 | #### Solving Linear Systems 2 | 3 | In theory, systems of linear equations are just constraints which should be amenable to the standard CLP techniques already described. In the real world, just as in solving for the roots of polynomials, things are not quite so simple. Consider the following two examples of simple linear systems in two variables: 4 | eg 5 | ?- [X,Y]::real, {X+2*Y==1,X-Y==1}. 6 | X:: 1.000000000000000..., 7 | Y::real(-1.1102230246251565e-16,5.551115123125783e-17). 8 | 9 | ?- [X,Y]::real, {X+Y==1,X-Y==1}. 10 | X::real(-1.0e+16,1.0e+16), 11 | Y::real(-1.0e+16,1.0e+16). 12 | In the first example, the fixed point iteration immediately converged to an approximate solution (`X=1, Y=0`), while in the second (almost identical) example, the fixed point iteration did not converge at all. To help explain this observation, here are the graphs of the two systems: 13 | [FPconvergance] 14 | The red and blue lines are the equations and the solid black lines represent fixed point iteration steps. In the case of no convergence, these steps form a square box (equations intersect at 90 degrees). This means that a fixed point is quickly reached but no narrowing occurs. In the convergent case, each iteration step results in a narrowing of the `X` or `Y` interval, and a solution is found quite quickly. So the first issue is that fixed point convergence is data dependent. (Note this isn't unique to linear systems but it's easy to demonstrate it with these simple examples.) 15 | 16 | Fortunately `solve/1` can be used overcome this deficiency: 17 | eg 18 | ?- [X,Y]::real, {X+Y==1,X-Y==1}, solve(X). 19 | X:: 1.00000000..., 20 | Y::real(-8.257253769627937e-10,8.257253769627937e-10). 21 | and, in this case, it works equally well if you solve for `X` or `Y` or both. However this may not always be the case. While the solution found should always be the same, the time taken to find it may vary widely (sometimes an order of magnitude or more). The following "real-world" examples were taken from [SYSTEMS OF LINEAR EQUATIONS AND MATRICES]. 22 | 23 | ##### Manufacturing: Production Scheduling 24 | > "*Ace Novelty wishes to produce three types of souvenirs: types A, B, and C. To manufacture a type-A souvenir requires 2 minutes on machine I, 1 minute on machine II, and 2 minutes on machine III. A type-B souvenir requires 1 minute on machine I, 3 minutes on machine II, and 1 minute on machine III. A type-C souvenir requires 1 minute on machine I and 2 minutes each on machines II and III. There are 3 hours available on machine I, 5 hours available on machine II, and 4 hours available on machine III for processing the order. How many souvenirs of each type should Ace Novelty make in order to use all of the available time?*" 25 | 26 | This can be simply modeled by: 27 | .pl 28 | ace_produce([A,B,C],[MI,MII,MIII]) :- 29 | [A,B,C]::integer(0,_), % numbers of each type to produce 30 | { 31 | MI == 2*A + B + C, % jobs for machine I 32 | MII == A + 3*B + 2*C, % jobs for machine II 33 | MIII == 2*A + B + 2*C % jobs for machine III 34 | }. 35 | Fixed point iteration alone is insufficient but `solve` quickly finds the solution: 36 | eg 37 | ?- ace_produce([A,B,C],[180,300,240]). 38 | A::integer(0,90), 39 | B::integer(0,100), 40 | C::integer(0,120). 41 | 42 | ?- ace_produce([A,B,C],[180,300,240]),solve(A). 43 | A = 36, 44 | B = 48, 45 | C = 60 ; 46 | false. 47 | So Ace Novelty should make 36 type-A souvenirs, 48 type-B souvenirs, and 60 type-C souvenirs. 48 | 49 | ##### Capital Expenditure Planning 50 | > "*The management of Hartman Rent-A-Car has allocated $1.5 million to buy a fleet of new automobiles consisting of compact, intermediate-size, and full-size cars. Compacts cost $12,000 each, intermediate-size cars cost $18,000 each, and full-size cars cost $24,000 each. If Hartman purchases twice as many compacts as intermediate-size cars and the total number of cars to be purchased is 100, determine how many cars of each type will be purchased. (Assume that the entire budget will be used.)*" 51 | eg 52 | ?- [C,I,F]::integer(0,_), {C+I+F==100,12000*C+18000*I+24000*F==1500000,C==2*I}. 53 | C::integer(0,100), 54 | I::integer(0,50), 55 | F::integer(0,62). 56 | 57 | ?- [C,I,F]::integer(0,_), {C+I+F==100,12000*C+18000*I+24000*F==1500000,C==2*I}, solve(C). 58 | C = 60, 59 | I = 30, 60 | F = 10 ; 61 | false. 62 | As above, the dependency issue requires the use of `solve/1` to generate a solution. 63 | 64 | ##### Simple D.C. Circuit Analysis 65 | This example has been included because [Towards Practical Interval Constraint Solving in Logic Programming] documents it as a linear system problem that is not handled well (at all?) by the CLP techniques described so far. Consider the D.C. circuit: 66 | [SimpleCircuit] 67 | The problem as stated is to solve for the currents flowing through the resistors assuming {`V=10`} volts and {`R_i = i Omega "for" i=1,2,...,9`}. The arrows indicate the direction of positive current flow. For the most part they are somewhat arbitrary; a negative value just means the flow is in the opposite direction. Applying Kirchhoff's laws yields the following equations: 68 | .am 69 | I_s-I_1-I_2-I_8 = 0, I_1 = 10, 70 | -I_s+I_1+I_7 = 0, 2I_2-3I_3-8I_8 = 0, 71 | I_2+I_3-I_5 = 0, 3I_3+5I_5-9I_9 = 0, 72 | -I_3-I_4+I_8-I_9 = 0, -4I_4+6I_6+9I_9 = 0, 73 | I_4+I_6-I_7 = 0, -I_1+4I_4+7I_7+8I_8 = 0, 74 | I_5-I_6+I_9 = 0 75 | As the paper states, there are 11 equations in 10 unknowns, but it's not obvious where the redundancy occurs. (Further it is suggested that initial ranges of `[-100,100]` be used for all intervals, although except for one case, this is not really necessary.) 76 | 77 | However consider {`I_s`}. From the equations, it is not obvious that it cannot be negative. But that implies that positive current can flow from the negative to the positive terminals of the voltage supply. Furthermore any search, e.g., using `solve/1`, will spend considerable effort attempting to find solutions where this can occur. (The paper cited indicates either no answer was achieved within 24 hours on a 2 MIP CPU or no narrowing occurs even using `solve`.) But adding the simple constraint {`Is>=0`} generates the answer within a second or two (machine dependent). The CLP(BNR) code to define the circuit: 78 | .pl 79 | simpleCircuit(Vs) :- 80 | simpleCircuitDef(Vs,EQs), % EQs is list of constraint equations in Vs 81 | Vs::real, % declare so intervals are finite 82 | {EQs}. % activate constraints 83 | 84 | simpleCircuitDef([Is,I1,I2,I3,I4,I5,I6,I7,I8,I9], 85 | [ 86 | Is-I1-I2-I8 == 0, I1 == 10, 87 | -Is+I1+I7 == 0, 2*I2-3*I3-8*I8 == 0, 88 | I2+I3-I5 == 0, 3*I3+5*I5-9*I9 == 0, 89 | -I3-I4+I8-I9 == 0, -4*I4+6*I6+9*I9 == 0, 90 | I4+I6-I7 == 0, -I1+4*I4+7*I7+8*I8 == 0, 91 | I5-I6+I9 == 0 92 | ]). 93 | Again, no narrowing occurs in the initial fixed point iteration, but it's not obvious which variables to use with `solve`. As `solve` takes a list of variables (breadth first across all intervals named) there's no reason not to use the entire set of interval values: 94 | eg 95 | ?- Vs=[Is,I1,I2,I3,I4,I5,I6,I7,I8,I9], simpleCircuit(Vs). 96 | Vs = [Is, 10, I2, I3, I4, I5, I6, I7, I8|...], 97 | I1 = 10, 98 | Is::real(-9.99999999999999e+15, 1.0e+16), 99 | I2::real(-1.0e+16, 1.0e+16), 100 | I3::real(-1.0e+16, 1.0e+16), 101 | I4::real(-1.0e+16, 1.0e+16), 102 | I5::real(-1.0e+16, 1.0e+16), 103 | I6::real(-1.0e+16, 1.0e+16), 104 | I7::real(-1.0e+16, 9.99999999999999e+15), 105 | I8::real(-6.25e+15, 6.25e+15), 106 | I9::real(-8.888888888888889e+15, 8.888888888888889e+15). 107 | 108 | ?- Vs=[Is,I1,I2,I3,I4,I5,I6,I7,I8,I9], simpleCircuit(Vs), solve(Vs). 109 | Vs = [Is, 10, I2, I3, I4, I5, I6, I7, I8|...], 110 | I1 = 10, 111 | Is:: 10.8282986..., 112 | I2:: 0.5690898..., 113 | I3:: -0.3118300..., 114 | I4:: 0.5320600..., 115 | I5:: 0.2572598..., 116 | I6:: 0.2962385..., 117 | I7:: 0.8282986..., 118 | I8:: 0.2592087..., 119 | I9:: 0.0389787... ; 120 | false. 121 | Using `solve` finds the solution in a few seconds, but adding the constraint that {`Is`} is positive further reduces the execution time by over a factor of three: 122 | eg 123 | ?- Vs=[Is,I1,I2,I3,I4,I5,I6,I7,I8,I9], simpleCircuit(Vs), time(solve(Vs)). 124 | % 8,887,111 inferences, 1.550 CPU in 1.554 seconds (100% CPU, 5731804 Lips) 125 | Vs = [Is, 10, I2, I3, I4, I5, I6, I7, I8, I9], 126 | I1 = 10, 127 | Is:: 10.8282986..., 128 | I2:: 0.5690898..., 129 | I3:: -0.3118300..., 130 | I4:: 0.5320600..., 131 | I5:: 0.2572598..., 132 | I6:: 0.2962385..., 133 | I7:: 0.8282986..., 134 | I8:: 0.2592087..., 135 | I9:: 0.0389787... . 136 | 137 | ?- Vs=[Is,I1,I2,I3,I4,I5,I6,I7,I8,I9], simpleCircuit(Vs), {Is>=0}, time(solve(Vs)). 138 | % 2,258,071 inferences, 0.437 CPU in 0.438 seconds (100% CPU, 5169742 Lips) 139 | Vs = [Is, 10, I2, I3, I4, I5, I6, I7, I8, I9], 140 | I1 = 10, 141 | Is:: 10.828298..., 142 | I2:: 0.5690898..., 143 | I3:: -0.3118300..., 144 | I4:: 0.5320600..., 145 | I5:: 0.2572598..., 146 | I6:: 0.2962385..., 147 | I7:: 0.828298..., 148 | I8:: 0.2592087..., 149 | I9:: 0.0389787... . 150 | This example demonstrates the importance of ensuring that all known constraints are explicit to minimize execution time, even if some of the constraints seem redundant. In this case `solve` can spend much wasted time searching for solutions for negative `Is`. 151 | 152 | ##### Under and Overdetermined Systems 153 | The examples above are nicely behaved but what happens if the system of equations is under or overdetermined? In case of the latter, incompatible additional constraints should result in failure: 154 | eg 155 | ?- [X,Y]::real, {X+2*Y==4,X-2*Y==0,4*X+3*Y==12}. 156 | false. 157 | In the under-determined case, little or no narrowing occurs regardless of how hard `solve` tries to find a solution: 158 | eg 159 | ?- [X,Y,Z,W]::real, {X+2*Y-3*Z+W== -2, 3*X-Y-2*Z-4*W==1, 2*X+3*Y-5*Z+W== -3}, solve([X,Y,Z,W]). 160 | X::real(-1.0e+16, 1.0e+16), 161 | W::real(-1.0e+16, 1.0e+16), 162 | Y::real(-1.0e+16, 1.0e+16), 163 | Z::real(-1.0e+16, 1.0e+16). 164 | But there are cases where under-determined still yield meaningful results. From the same source as previous examples: 165 | > "*Traffic Control (The figure) shows the flow of downtown traffic in a certain city during the rush hours on a typical weekday. The arrows indicate the direction of traffic flow on each one-way road, and appears beside each road. 5th Avenue and 6th Avenue can each handle up to 2000 vehicles per hour without causing congestion, whereas the maximum capacity of both 4th Street and 5th Street is 1000 vehicles per hour. The flow of traffic is controlled by traffic lights installed at each of the four intersections.*" 166 | [Figure 7 Traffic] 167 | "Without congestion" implies that all traffic entering an intersection must leave the intersection. Including the street volume limits, the system can be modelled as: 168 | ``` 169 | ?- [X1,X3]::integer(0,2000), [X2,X4]::integer(0,1000), {X1+X4==1500, X1+X2==1300, X2+X3==1800, X3+X4==2000}. 170 | X1::integer(500, 1300), 171 | X3::integer(1000, 1800), 172 | X2::integer(0, 800), 173 | X4::integer(200, 1000). 174 | ``` 175 | Note that `solve` is unnecessary here. This isn't a unique solution (the system is under-determined) but the results are useful in that they specify limits to traffic flows while avoiding congestion. Further constraints can be applied to model other possible scenarios, e.g., 176 | > "*Suppose the part of 4th Street between 5th Avenue and 6th Avenue is to be resurfaced and that traffic flow between the two junctions must therefore be reduced to at most 300 vehicles per hour.*" 177 | yielding: 178 | ``` 179 | ?- [X1,X3]::integer(0,2000), [X2,X4]::integer(0,1000), {X1+X4==1500, X1+X2==1300, X2+X3==1800, X3+X4==2000}, {X4=<300}. 180 | X1::integer(1200, 1300), 181 | X3::integer(1700, 1800), 182 | X2::integer(0, 100), 183 | X4::integer(200, 300). 184 | ``` 185 | Congestion does not occur as long as `X2` remains below 100, since exceeding that will cause congestion at the intersection of 5th Street and 5th Avenue. 186 | 187 | ##### Cramer's Rule 188 | 189 | Solving systems of linear equations by specifying the equations as constraints and performing a bifurcating search (`solve/1`) is a rather brute force approach when there are several [well known algorithms] for doing the same thing. One such algorithm is [Cramer's rule] which states that if you can represent the system of equations in matrix form {`A*bb"x"=bb"b"`} where {`A`} is an {`n xx n`} matrix of coefficients, and {`bb"b"`} and {`bb"x"`} are {`n`}-dimensional column vectors, then {`bb"x" = A^-1*bb"b"`}. 190 | 191 | Using `library(type_ndarray)`in the [`arithmetic_types`] pack for matrix support (Note: some examples below reflect changes mad for v0.1.0 of `arithmetic_types`), the "Production Scheduling" problem described above can be simply solved (without constraints): 192 | eg 193 | ?- set_prolog_flag(prefer_rationals,true). 194 | true. 195 | 196 | ?- use_module(library(type_ndarray)). 197 | true. 198 | 199 | ?- Am is ndarray([[2,1,1],[1,3,2],[2,1,2]]), Bs is ndarray([[180],[300],[240]]), Xs is ndarray([[A],[B],[C]]), 200 | Xs is dot(inverse(Am),Bs). 201 | Am = #(#(2, 1, 1), #(1, 3, 2), #(2, 1, 2)), 202 | Bs = #(#(180), #(300), #(240)), 203 | Xs = #(#(36), #(48), #(60)), 204 | A = 36, 205 | B = 48, 206 | C = 60. 207 | The function `ndarray/1` creates an N dimensional array from a nested list representation while the `dot/2` and `inverse/1` functions calculate the dot product and matrix inverse. (Rational number calculations are enabled to avoid floating point rounding errors.) Functions on type `ndarray` are constraint aware, so as long as the array structure has been defined, the order of the goals can be changed enabling the common test and generate pattern: 208 | eg 209 | ?- Am is ndarray([[2,1,1],[1,3,2],[2,1,2]]), Bs is new(ndarray,[3,1]), 210 | ndarray([[A,B,C]]) is transpose(dot(inverse(Am),Bs)), Bs is transpose(ndarray([[180,300,240]])). 211 | Am = #(#(2, 1, 1), #(1, 3, 2), #(2, 1, 2)), 212 | Bs = #(#(180), #(300), #(240)), 213 | A = 36, 214 | B = 48, 215 | C = 60. 216 | Note the use of transpose to convert between a single row matrix and a single column matrix (row matrix is simpler to read and write). 217 | 218 | The "Expenditure Planning" problem is similarly straight forward: 219 | eg 220 | ?- Am is ndarray([[1,1,1],[12000,18000,24000],[1,-2,0]]), Bs is ndarray([[100],[1500000],[0]]), 221 | ndarray([[C],[I],[F]]) is dot(inverse(Am),Bs). 222 | 223 | Am = #(#(1, 1, 1), #(12000, 18000, 24000), #(1, -2, 0)), 224 | Bs = #(#(100), #(1500000), #(0)), 225 | C = 60, 226 | I = 30, 227 | F = 10. 228 | 229 | The solution to the "Simple Circuit" analysis isn't quite so clear. Cramer's rule requires a square coefficient matrix, while the simple circuit has 11 equations in 10 unknowns. On obvious approach might be to discard the `I1 = 10` equation and using the remaining 10 (while unifying `I1` with `10`). But there is no inverse for the resulting coefficient matrix (`Am`), as the determinant of the matrix is `0` (using the coefficient ordering corresponding to `[I1,I2,I3,I4,I5,I6,I7,I8,I9,Is]`): 230 | eg 231 | ?- Am is ndarray([ 232 | [-1,-1, 0, 0, 0, 0, 0,-1, 0, 1], 233 | [ 1, 0, 0, 0, 0, 0, 1, 0, 0,-1], 234 | [ 0, 1, 1, 0,-1, 0, 0, 0, 0, 0], 235 | [ 0, 0,-1,-1, 0, 0, 0, 1,-1, 0], 236 | [ 0, 0, 0, 1, 0, 1,-1, 0, 0, 0], 237 | [ 0, 0, 0, 0, 1,-1, 0, 0, 1, 0], 238 | [ 0, 2,-3, 0, 0, 0, 0,-8, 0, 0], 239 | [ 0, 0, 3, 0, 5, 0, 0, 0,-9, 0], 240 | [ 0, 0, 0,-4, 0, 6, 0, 0, 9, 0], 241 | [-1, 0, 0, 4, 0, 0, 7, 8, 0, 0] 242 | ]), Det is determinant(Am). 243 | Am = #(#(-1, -1, 0, 0, 0, 0, 0, -1, 0, 1), #(1, 0, 0, 0, 0, 0, 1, 0, 0, -1), #(0, 1, 1, 0, -1, 0, 0, 0, 0, 0), #(0, 0, -1, -1, 0, 0, 0, 1, -1, 0), #(0, 0, 0, 1, 0, 1, -1, 0, 0, 0), #(0, 0, 0, 0, 1, -1, 0, 0, 1, 0), #(0, 2, -3, 0, 0, 0, 0, -8, 0, 0), #(0, 0, 3, 0, 5, 0, 0, 0, -9, 0), #(0, 0, 0, -4, 0, 6, 0, 0, 9, 0), #(-1, 0, 0, 4, 0, 0, 7, 8, 0, 0)), 244 | Det = 0. 245 | Since it's not obvious which equation may be redundant, experimentation can be used to find a non-zero determinant by replacing the first equation by `I1 = 10`. A new predicate to support the matrix form of the circuit definition: 246 | .pl 247 | simpleCircuitDef(Currents,Am,Bs,Xs) :- 248 | Xs is transpose(ndarray([Currents])), 249 | Am is ndarray([ 250 | [ 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], 251 | [ 1, 0, 0, 0, 0, 0, 1, 0, 0,-1], 252 | [ 0, 1, 1, 0,-1, 0, 0, 0, 0, 0], 253 | [ 0, 0,-1,-1, 0, 0, 0, 1,-1, 0], 254 | [ 0, 0, 0, 1, 0, 1,-1, 0, 0, 0], 255 | [ 0, 0, 0, 0, 1,-1, 0, 0, 1, 0], 256 | [ 0, 2,-3, 0, 0, 0, 0,-8, 0, 0], 257 | [ 0, 0, 3, 0, 5, 0, 0, 0,-9, 0], 258 | [ 0, 0, 0,-4, 0, 6, 0, 0, 9, 0], 259 | [-1, 0, 0, 4, 0, 0, 7, 8, 0, 0] 260 | ]), 261 | Bs is transpose(ndarray([[10.0,0,0,0,0,0,0,0,0,0]])). 262 | Now: 263 | eg 264 | ?- simpleCircuitDef([I1,I2,I3,I4,I5,I6,I7,I8,I9,Is],Am,Bs,Xs), Xs is dot(inverse(Am),Bs). 265 | I1 = 10.0, 266 | I2 = 0.5690898460339116, 267 | I3 = -0.3118300526213214, 268 | I4 = 0.5320600272851296, 269 | I5 = 0.25725979341259014, 270 | I6 = 0.2962385499902553, 271 | I7 = 0.8282985772753849, 272 | I8 = 0.25920873124147337, 273 | I9 = 0.038978756577665176, 274 | Is = 10.828298577275387, 275 | Am = #(#(1, 0, 0, 0, 0, 0, 0, 0, 0, 0), #(1, 0, 0, 0, 0, 0, 1, 0, 0, -1), #(0, 1, 1, 0, -1, 0, 0, 0, 0, 0), #(0, 0, -1, -1, 0, 0, 0, 1, -1, 0), #(0, 0, 0, 1, 0, 1, -1, 0, 0, 0), #(0, 0, 0, 0, 1, -1, 0, 0, 1, 0), #(0, 2, -3, 0, 0, 0, 0, -8, 0, 0), #(0, 0, 3, 0, 5, 0, 0, 0, -9, 0), #(0, 0, 0, -4, 0, 6, 0, 0, 9, 0), #(-1, 0, 0, 4, 0, 0, 7, 8, 0, 0)), 276 | Bs = #(#(10.0), #(0), #(0), #(0), #(0), #(0), #(0), #(0), #(0), #(0)), 277 | Xs = #(#(10.0), #(0.5690898460339116), #(-0.3118300526213214), #(0.5320600272851296), #(0.25725979341259014), #(0.2962385499902553), #(0.8282985772753849), #(0.25920873124147337), #(0.038978756577665176), #(10.828298577275387)). 278 | Where applicable, using standard numerical methods, with or without constraints, is usually a more optimal approach than a general purpose search technique like `solve/1`, yielding more precise results in less time. On the other hand, if the problem consists of mixed linear and non-linear equations or fails to meet the necessary requirements, e.g., the coefficient matrix isn't square, "constrain and search" may offer a feasible alternative. 279 | 280 | <#TableOfContents> 281 | 282 | & 283 | [SYSTEMS OF LINEAR EQUATIONS AND MATRICES] <- link https://docplayer.net/21711942-The-linear-equations-in-two-variables-studied-in.html 284 | [Towards Practical Interval Constraint Solving in Logic Programming] <- link https://pdfs.semanticscholar.org/1dca/e2a910184c4b2f9d770f054168150c6d0bde.pdf 285 | [FPconvergance] <- image images/linearConvergance.png width=50% height=50% style="margin-left:200px" 286 | [SimpleCircuit] <- image images/SimpleCircuit.png width=50% height=50% style="margin-left:200px" 287 | [Figure 7 Traffic] <- image images/TrafficExample5.png width=30% height=30% style="margin-left:200px" 288 | [Appendix 1] <- link #toc4Appendix_1_-__linear.pl__Source 289 | [well known algorithms] <- link https://en.wikipedia.org/wiki/System_of_linear_equations 290 | [Cramer's rule] <- link https://en.wikipedia.org/wiki/Cramer%27s_rule 291 | [`arithmetic_types`] <- link https://www.swi-prolog.org/pack/list?p=arithmetic_types -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_UC_optionTrading.myw: -------------------------------------------------------------------------------- 1 | #### Options Trading (from CLP({`frR`})) 2 | 3 | CLP({`frR`}) was the first implementation of CLP in the domain of real numbers. This section explores a couple of examples (Binomial Option Pricing and Algebraic Combinations) from [Constraint Logic Programming and Option Trading] which described the use of CLP({`frR`}) to build components of an expert options analysis system. From the paper: 4 | 5 | > "(*(An option is a contract whose value is contingent upon the value of an underlying asset. We will focus on the most common type of options which are those on common stocks. A **call** option is a contract which gives the owner the right to buy a fixed number of shares (exercising the option) at a fixed price (exercise/strike price) until a certain (maturity/expiration) date. Similarly a **put** option gives the owner the right to sell the share at a fixed price. An option contract is usually in lots of 100 shares of the underlying stock. The individual who creates or issues the option is known as the seller/writer and the purchaser as the holder/buyer of the option. For example, a call option on "XYZ Jan 50" at a cost of $800 will give the buyer the right to purchase 100 shares of XYZ at $50 a share until January 18, 1988. At any time before this, the holder can sell the call at the current market price, exercise it by paying $5000 for 100 shares of XYZ, or do nothing. If by January 18, the call has not been exercised then it will either expire worthless (because XYZ is below $50) with a loss of $800 or if XYZ is above $50 (say at $60) it will generate a profit of (60-50)\*100-800 = $200.)*)" 6 | & \* <- × 7 | 8 | Although CLP({`frR`}) and CLP(BNR) have significant differences syntactically (and, to some degree, semantically), it's a straight forward process to re-implement these examples in CLP(BNR). 9 | 10 | ##### The Binomial Option Pricing Formula 11 | 12 | The [Binomial Formula] is used to determine the value of a put or call option on a stock. It assumes that at the end of a time period the stock will take one of two values, one greater than the current value and one less than the current value. These are represented by two factors: {`u`} and {`d`} where {`0 =< d =< 1 =< u`}. Other values in the model include: 13 | - {`S`}, the price of the underlying stock 14 | - {`K`}, the option exercise price (strike price) 15 | - {`R`}, the risk free interest rate (e.g., 5% = 5.0) 16 | Considering a call option, if there is an upward movement of stock value (to {`u*S`}), the payoff is the maximum of 0 and the difference between the stock price and the strike price: 17 | math> C^+=max(0,u*S-K) 18 | Similarly in the case of a downward movement, the payoff is: 19 | math> C^(-)=max(0,d*S-K) 20 | The value of the option is: 21 | math> C=(pi*C^+ + (1-pi)*C^-)/(1+R//100) 22 | where {`pi`} is the probability of an upward movement. There are various ways of determining a value for {`pi`}, but using a commonly used formula: 23 | math> pi = ((1+R//100) - d)/(u-d) 24 | This also applies to put options except the two payoff values are: 25 | math> 26 | P^+=max(K-u*S,0), P^(-)=max(K-d*S,0) 27 | A fairly literal translation of this to CLP(BNR) constraints produces `option_value/8` which can evaluate either type of option: 28 | .pl 29 | option_value(Put_Call,V,S,Pr,R,K,Up,Down) :- 30 | [S,K,R]::real(0,_), % all positive 31 | [Pr,Down]::real(0,1), [Rf,Up]::real(1,_), 32 | o_sign(Put_Call,Sign), 33 | { 34 | Oplus == max(0, Sign*(Up*S - K)), 35 | Ominus == max(0, Sign*(Down*S - K)), 36 | Rf == 1+R/100, 37 | Pr == (Rf-Down)/(Up-Down), 38 | V == (Pr*Oplus + (1-Pr)*Ominus)/Rf 39 | }. 40 | 41 | o_sign(call, 1). 42 | o_sign(put, -1). 43 | An example from the paper: 44 | eg 45 | ?- S=80, K=80, R=10, Up=1.6, Down=0.5, 46 | option_value(call,V,S,Pr,R,K,Up,Down). 47 | S = K, K = 80, 48 | R = 10, 49 | Up = 1.6, 50 | Down = 0.5, 51 | Pr:: 0.54545454545454..., 52 | V:: 23.801652892562... . 53 | Using constraints results in a true relational model; the order of the goals is unimportant and, for example, `Up` and `Down` could be calculated from the other values using the same predicate. 54 | eg 55 | ?- eval_option(put,1,Up,Down,S,Put,K,R), 56 | S=60, K=60, Up=1.15, Down=0.85, R=6. 57 | Up = 1.15, 58 | Down = 0.85, 59 | S = K, K = 60, 60 | R = 6, 61 | Put:: 2.5471698113207... . 62 | 63 | ?- S=80, K=80, R=10, V=23.80, 64 | option_value(call,V,S,Pr,R,K,Up,Down), 65 | Pr=0.5. 66 | S = K, K = 80, 67 | R = 10, 68 | V = 23.8, 69 | Pr = 0.5, 70 | Down:: 0.54550000000000..., 71 | Up:: 1.654500000000000... . 72 | 73 | The binomial model is obtained by iterating this evaluation rule over `N` time periods. At each step a new up and down set of stock and option values are defined for a subsequent step resulting in a binary tree pair, one for stocks and one for options. The values at the terminal leaves of the tree can be calculated directly using the `Oplus/Ominus` constraints from `option_value` and any changes will be propagated back up the tree. 74 | 75 | Assuming a simple `n(V,Left,Right)` and `l(V)` respectively for nodes and leaves of the tree, `eval_option/8` calculates the value of an option after `N` time periods. The same up and down factors and interest rate apply to each period. 76 | .pl 77 | eval_option(Put_Call, N, Up,Down, S, V, K, R) :- 78 | Down::real(0,1), Up::real(1,_), [K,R]::real(0,_), 79 | { 80 | Rf == 1+R/100, 81 | Pr == (Rf-Down)/(Up-Down) 82 | }, 83 | eval_option_tree(N, Put_Call, Up,Down, Pr, S, V, Stree, Otree, K, Rf). 84 | 85 | eval_option_tree(0, Put_Call, _Up,_Down, _Pr, S, V, l(S), l(V), K, _R) :- 86 | o_sign(Put_Call,Sign), 87 | {V == max(0, Sign*(S-K))} , !. 88 | eval_option_tree(N,Put_Call, Up, Down, Pr, S, V, n(S,SLeft,SRight), n(V,CLeft,CRight), K, Rf) :- 89 | N>0, 90 | [OUp,ODown]::real(0,_), 91 | { 92 | SUp == Up*S, SDown == Down*S, 93 | V == (Pr*OUp + (1-Pr)*ODown)/Rf 94 | }, 95 | N1 is N-1, 96 | eval_option_tree(N1,Put_Call,Up,Down,Pr,SDown,ODown,SLeft, CLeft, K,Rf), 97 | eval_option_tree(N1,Put_Call,Up,Down,Pr,SUp, OUp, SRight,CRight,K,Rf). 98 | The values of `Rf` and `Pr` are the same for all nodes, so they're defined just once in `eval_option`; the constraints in `eval_option_tree` define the up and down values for the stock (`S`) and option (`V`); the leaf nodes just relate `S` and `V` (`{V == max(0, Sign*(S-K))}`). As above, this is a true relational model; any of the values can be computed given that a sufficient set of the remaining values is defined. 99 | 100 | As an example, consider a call option expiring in two years with strike price of $30 on a stock which currently trades at $34. The stock is expected to increase by a factor of 1.25 and decrease by a factor of 0.70 and the annual interest rate is 3%. Using a time period of one year: 101 | eg 102 | ?- Up=1.25, Down=0.7, eval_option(call,2,Up,Down,Stock,Call,K,R), 103 | R = 3, Stock=34, K=30. 104 | Up = 1.25, 105 | Down = 0.7, 106 | Stock = 34, 107 | K = 30, 108 | R = 3, 109 | Call:: 7.8471109435385... . 110 | The current value of the call option is $7.85. There are many similar examples on the internet. The main "variable" across the examples are how the upward movement probability is determined. Or a "volatility" value may be provided instead of up and down factors. These will impact the details but not the high level (binomial) structure of the algorithm. 111 | 112 | ##### Algebraic Combinations of Options 113 | 114 | A particular trading position is usually a combination of options (derivatives) and stocks/bonds (which could be considered as special kinds of ). The total "payoff" is the sum of the individual payoffs. 115 | 116 | A payoff can be represented by a piecewise linear function, one linear function if the stock price is less than the strike price and another if it's greater than the strike price. Any piecewise linear function can be expressed in terms of two elementary functions: the Heaviside function {`h(X,Y)`} and the ramp function {`r(X,Y)`}: 117 | .pl 118 | h(X,Y,0) :- {Y=X}. % h(X,Y) = 1 120 | 121 | r(X,Y,0) :- {Y=X}. % r(X,Y) = Y-X 123 | Note that these evaluate to either a numeric value (`0` or `1`) or a symbolic expression (`Y-X`) while imposing the necessary constraint between `X` and `Y`. 124 | 125 | All payoff options can be described by a matrix product of the form: 126 | math> "Option payoff" = [H1,H2,H3,H4]xx[(h(B1,S)),(h(B2,S)),(r(B1,S)),(r(B2,S))] 127 | where {`S`} is the current stock price, {`B1`} and {`B2`} are the breakpoints equal to either {`0`} or the strike price, and {`H1`}, {`H2`}, {`R1`} and {`R2`} are parameters of the options. To summarize the option payoff used to evaluate the end-of-period position: 128 | .pl 129 | payoff(Type, BuyorSell, S, C, P, R, K, B, Sign*(H1*T1 + H2*T2 + R1*T3 + R2*T4)) :- 130 | [S, C, P, R, K, B]::real(0,_), 131 | get_sign(BuyorSell, Sign), 132 | opt_def(Type,S,C,P,R,K,B,B1,B2,H1,H2,R1,R2), 133 | h(B1,S,T1), h(B2,S,T2), r(B1,S,T3), r(B2,S,T4). 134 | 135 | get_sign(buy, -1). 136 | get_sign(sell, 1). 137 | 138 | opt_def(stock, S, C, P, R, K, B, 0, 0, S*(1+R/100), 0, -1, 0). 139 | opt_def( call, S, C, P, R, K, B, 0, K, C*(1+R/100), 0, 0,-1). 140 | opt_def( put, S, C, P, R, K, B, 0, K, P*(1+R/100)-K, 0, 1,-1). 141 | opt_def( bond, S, C, P, R, K, B, 0, 0, B*(1+R/100), 0, 0, 0). 142 | `S, B, C, P` are respectively the current stock, bond, call and put price, `K` is the strike price for call or put, and R is the current risk-free interest rate. The payoff value (last argument) is in symbolic form. `payoff/9` is non deterministic since different values of the payoff expression will be generated depending on the stock price. A constraint is used to evaluate the expression as shown in the following examples. 143 | 144 | A simple query which evaluates the sale of a call option that expires *in-the-money*, i.e., the strike price is less than the stock price: 145 | eg 146 | ?- CALL=5, K=50, R=5.0, S=60, 147 | payoff(call, sell, S,CALL,_,R,K,_,PExp), 148 | {PayOff==PExp}. 149 | CALL = 5, 150 | K = 50, 151 | R = 5.0, 152 | S = 60, 153 | PExp = 1*(5*(1+5.0/100)*1+0*1+0*(60-0)+ -1*(60-50)), 154 | PayOff = -4.75. 155 | 156 | To ask what the underlying stock price should be at expiration in order for the payoff of selling a call option to exceed $5: 157 | eg 158 | ?- CALL=5, K=50, R=5.0, {PayOff>=5}, 159 | payoff(call, sell, S, CALL, _, R, K, _, PExp), {PayOff==PExp}. 160 | CALL = 5, 161 | K = 50, 162 | R = 5.0, 163 | PayOff = 5.25, 164 | PExp = 1*(5*(1+5.0/100)*1+0*0+0*(S-0)+ -1*0), 165 | S::real(0, 49.99999999999999) ; 166 | CALL = 5, 167 | K = 50, 168 | R = 5.0, 169 | PExp = 1*(5*(1+5.0/100)*1+0*1+0*(S-0)+ -1*(S-50)), 170 | PayOff::real(5, 5.25), 171 | S::real(50, 50.25). 172 | In this case there are two values of `S` that meet the criteria: `S::real(0, 49.99999999999999)` and `S::real(50, 50.25000000000001)`, produced on backtracking. Unlike CLP({`frR`}) which provides answers in symbolic form, e.g., `PayOff = 55.25 - S`, CLP(BNR) normally provides answers expressed as intervals. (A rough symbolic version could be derived from verbose answer format using flag `clpBNR_verbose`.) 173 | 174 | Now consider a combination strategy. A *straddle* is composed of the purchase price of a call and a put option. The following query determines the stock price at expiration for the payoff to exceed $3 with a strike value of $50: 175 | eg 176 | ?- CALL=4, PUT=3, R=5.0, K=50, {PayOff>=3}, 177 | payoff(call, buy, S, CALL, _, R, K, _, V1), 178 | payoff( put, buy, S, _, PUT, R, K, _, V2), 179 | {PayOff==V1+V2}. 180 | CALL = 4, 181 | PUT = 3, 182 | R = 5.0, 183 | K = 50, 184 | V1 = -1*(4*(1+5.0/100)*1+0*0+0*(S-0)+ -1*0), 185 | V2 = -1*((3*(1+5.0/100)-50)*1+0*0+1*(S-0)+ -1*0), 186 | PayOff::real(3, 42.650000000000006), 187 | S::real(0, 39.650000000000006) ; 188 | CALL = 4, 189 | PUT = 3, 190 | R = 5.0, 191 | K = 50, 192 | V1 = -1*(4*(1+5.0/100)*1+0*1+0*(S-0)+ -1*(S-50)), 193 | V2 = -1*((3*(1+5.0/100)-50)*1+0*1+1*(S-0)+ -1*(S-50)), 194 | PayOff::real(3, 9.999999999999944e+15), 195 | S::real(60.349999999999994, 1.0e+16). 196 | Again there are two ranges of the stock value for the payoff to exceed 3: `S::real(0, 39.650000000000006)` for a payoff of `PayOff::real(3, 42.650000000000006)` (`S=<3.65`) and `S::real(60.349999999999994, 1.0e+16)` for a payoff of `PayOff::real(3, 9.999999999999944e+15)` (`S>=60.35`). Put another way, the payoff is less than `3` if `39.65 =< S =< 60.35`. (It's a trivial modification to ask this question.) 197 | 198 | There is a subtle issue here; the final constraint, e.g., `{PayOff==V1+V2}`, must follow the call to `payoff` which defines the symbolic expressions. Otherwise `V1` and `V2` are defined as intervals which can't be unified with the expressions. Now the reason `payoff` is implemented to generate expressions is to avoid separate constraints (for `V1` and `V2`) both containing `S`. This will give rise to the previously mentioned dependency problem if they're "compiled" separately. Combining the expressions symbolically provides CLP(BNR) with the opportunity to simplify the expression and avoid this problem. In general, it's advantageous to delay constraint evaluation until the most complicated expression is constructed rather than building them up from sub-constraints. 199 | 200 | A final example of a more complex combination of two put and two call options with automatic backtracking using `format` to output solutions: 201 | eg 202 | ?- R=10.0, 203 | P1=10, K1=20, payoff(put, sell, S, _, P1, R, K1, _, V1), 204 | P2=18, K2=40, payoff(put, buy, S, _, P2, R, K2, _, V2), 205 | C3=15, K3=60, payoff(call, buy, S, C3, _, R, K3, _, V3), 206 | C4=10, K4=80, payoff(call, sell, S, C4, _, R, K4, _, V4), 207 | {PayOff == V1+V2+V3+V4}, 208 | format("PayOff::~p,\tStock::~p\n",[PayOff,S]), 209 | fail. 210 | PayOff::_1708{real(5.699999999999999,5.7)}, Stock::_676{real(0,19.999999999999996)} 211 | PayOff::_1666{real(-14.299999999999997,5.700000000000003)}, Stock::_670{real(20,39.99999999999999)} 212 | PayOff::_1564{real(-14.3,-14.299999999999999)}, Stock::_670{real(40,59.99999999999999)} 213 | PayOff::_1462{real(-14.300000000000011,5.699999999999989)}, Stock::_670{real(60,79.99999999999999)} 214 | PayOff::_1338{real(5.699999999999999,5.700000000000001)}, Stock::_670{real(80,1.0e+16)} 215 | false. 216 | There are 5 answers corresponding to the full range of stock values: `0-20, 20-40, ... 80+`. As before, it's fairly simple to add additional constraints to only consider pre-defined payoff ranges. 217 | 218 | Although trading in options can involve complex mechanisms (see the [original paper] for more), CLP over the domain of reals offers an integrated framework of symbolic and numeric processing facilitating a simple and natural design for modelling these mechanisms. (See also [CLP({`frR`}) and Some Electrical Engineering Problems] for more examples of CLP({`frR`}) that could be easily adapted to execute as CLP(BNR) programs.) 219 | 220 | <#TableOfContents> 221 | 222 | & 223 | [original paper] <- link https://ieeexplore.ieee.org/abstract/document/4307090 224 | [Constraint Logic Programming and Option Trading] <- link https://ieeexplore.ieee.org/abstract/document/4307090 225 | [Binomial Formula] <- link https://medium.com/magnimetrics/understanding-the-binomial-option-pricing-model-44ec28d6e9af 226 | [CLP({`frR`}) and Some Electrical Engineering Problems] <- link http://citeseerx.ist.psu.edu/viewdoc/download;jsessionid=15A73BEF14501213F6F372FB2FDD4FAA?doi=10.1.1.32.7617&rep=rep1&type=pdf -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_UC_polynomialRoots.myw: -------------------------------------------------------------------------------- 1 | #### Solving Polynomial Equations in one Variable 2 | 3 | Finding the roots of a univariate polynomial equation is the same as asking what is the value of `X` given the constraint `{P(X)==0}`. (Some of this section was covered earlier in "Constraints on Real Intervals".) A simple quadratic with two real roots (1 and 3): 4 | eg 5 | ?- X::real, {X**2-4*X+3 == 0}. 6 | X::real(0.9999999999999998, 3.0000000000000018). 7 | The interval result must include both roots. To produce a single value, the problem must be further constrained: 8 | eg 9 | ?- X::real(2,_), {X**2-4*X+3 == 0}. 10 | X:: 3.00000000000000... . 11 | or to find both: 12 | eg 13 | ?- X::real, {X**2-4*X+3 == 0}, ({X=<2};{X>=2}). 14 | X:: 1.000000000000000... ; 15 | X:: 3.00000000000000... . 16 | Since we're looking for point solutions, `solve/1` is better than guessing at additional constraints to isolate roots: 17 | eg 18 | ?- X::real, {X**2-4*X+3 ==0}, solve(X). 19 | X:: 1.000000000000000... ; 20 | X:: 3.00000000000000... . 21 | 22 | Formulas exist for solving equations of low degree, so this may hardly seem to be worth the trouble, but this same technique works for pretty much any polynomial: 23 | eg 24 | ?- X::real, {17*X**256+35*X**17-99*X == 0}, solve(X). 25 | X:: 0.0000000000000000... ; 26 | X:: 1.005027892894011... . 27 | 28 | ?- X::real, {35*X**256-14*X**17+X == 0}, solve(X). 29 | X:: -0.847943660827315... ; 30 | X:: 0.0000000000000000... ; 31 | X:: 0.847943660827315... ; 32 | X:: 0.995842494200498... . 33 | A fourth degree polynomial equation with two real roots: 34 | eg 35 | ?- X::real, {X**4-4*X**3+4*X**2-4*X+3 == 0}, solve(X). 36 | X:: 1.000000... ; 37 | X:: 1.0000000... ; 38 | X:: 3.000000... ; 39 | X:: 3.000000... ; 40 | false. 41 | What happened here? The two roots (`X=1` and `X=3`) produced four answers. `solve` works by splitting intervals into pieces and discarding those pieces which provably do not contain solutions. Pieces which *may* contain solutions are split again, and the process is repeated. However to keep execution and time space under control, there is a minimum size interval which can be split as determined by the Prolog environment flag `clpBNR_default_precision` (default value=6) which roughly approximates the number of digits of precision. More precise answers can only be achieved by the basic fixed point iteration driven by the interval primitive operations (as indicated by the first example), or by changing the default precision (Prolog flag `clpBNR_default_precision`). 42 | eg 43 | ?- set_prolog_flag(clpBNR_default_precision,7). 44 | true. 45 | 46 | ?- X::real, {X**4-4*X**3+4*X**2-4*X+3 == 0}, solve(X). 47 | X:: 1.00000000... ; 48 | X:: 3.0000000... ; 49 | false. 50 | The flip side says that the precision limit can cause the iteration to terminate without disproving that a solution exists, which is the case for the second and fourth solution. 51 | 52 | The CLP(BNR) deterministic predicate `absolve` can be used to alleviate this problem (at a cost in performance). It "nibbles" away at the boundaries of an interval that fail to contain a solution and often removes these "false positives": 53 | eg 54 | ?- X::real, {X**4-4*X**3+4*X**2-4*X+3 == 0}, solve(X), absolve(X). 55 | X:: 1.00000000... ; 56 | X:: 3.00000000... ; 57 | false. 58 | But it is data dependent; sometimes both mechanisms are needed, as in this polynomial equation with roots at 0, 3, 4, and 5: 59 | eg 60 | ?- X::real, {X**4-12*X**3+47*X**2-60*X==0}, solve(X,7), absolve(X,9). 61 | X:: 0.0000000000000000... ; 62 | X:: 3.00000000... ; 63 | X:: 4.00000000... ; 64 | X:: 5.00000000... ; 65 | false. 66 | The underlying cause of the generation of redundant positives is the multiple occurrence of `X` in the constraint set (the *dependency* problem discussed previously) which limits the narrowing possible via fixed point iteration. In addition, the shape of the polynomial in the vicinity of the X axis may mean a small `X` interval corresponds to a larger interval value of the polynomial, in fact, large enough to include 0. And to make matters worse, it can take a considerable amount of work to generate these redundant positives. 67 | 68 | The inability to isolate roots, often accompanied by longer execution times, may indicate that constraint propagation isn't particularly efficient. In such cases, it can be advantageous to explore alternative representations of the constraint. In this particular case (finding roots of polynomials) a common representation is the Horner form (see [Horner's Method]), as it usually not only sharpens the results but takes less time: 69 | eg 70 | ?- X::real, {3+X*(-4+X*(4+X*(-4+X)))==0}, solve(X). 71 | X:: 1.0000000... ; 72 | X:: 3.00000000000000... ; 73 | false. 74 | 75 | ?- X::real, {X*(-60+X*(47+X*(-12+X)))==0}, solve(X). 76 | X = 0.0 ; 77 | X:: 3.000000... ; 78 | X:: 4.00000000000... ; 79 | X:: 5.000000... ; 80 | false. 81 | One observation is that the Horner form requires fewer operator instances (e.g., 3 less in the case of `X**4-4*X**3+4*X**2-4*X+3`) which generally means the number of nodes executed in the fixed point iteration loop is small, leading to improved performance. As a general rule re-writing constraints to use fewer operations leads to better performance and narrower results. 82 | 83 | The ultimate "pathological" case is {`x^2-2x+1=0`}. This is one of a family of similar quadratics that just reach the X axis, i.e., the X axis is a tangent to the bottom of the parabola, so it takes a reasonably large change in `X` to get appreciably closer to a RHS value of 0. Just using constraint propagation produces a reasonable approximation, dependant on throttling (see flag `clpBNR_iteration_limit`): 84 | eg 85 | ?- X::real, {X**2-2*X+1==0}, writeln(X). 86 | _168{real(0.997359377688694,1.002708358193705)} 87 | X:: 1.00... . 88 | But what if more precision is required? You could increase the throttling limit but it takes about ten times the default (i.e., 3000 to 30000) to achieve an extra digit of precsion and it may have a detrimental impact on the propagation of other constraints. 89 | 90 | You could try using `solve` to discard portions of the interval, but here again you run into precision limits. This curtails splitting `X` when the sub-intervals are smaller than the limit and, in this particular case, that happens before the propagation (which could prune the sub-interval in question) fails. So on backtracking this results in the generation of hundreds of non-solutions in addition to the real solution (smallest interval containing 1). (Try it!) 91 | 92 | So for this problem, `absolve` is one of the most efficient ways to narrow {`x`} without requiring any changes to default throttling. A higher precision value can also be used to effect more narrowing. 93 | eg 94 | % default precision for absolve is flag `clpBNR_default_precision` + 2 = 8 95 | ?- X::real, {X**2-2*X+1==0}, absolve(X), writeln(X). 96 | _178{real(0.9998273237777738,1.000211980803252)} 97 | X:: 1.000... . 98 | 99 | ?- X::real, {X**2-2*X+1==0}, absolve(X,13), writeln(X). 100 | _180{real(0.9999672180005927,1.000039249404795)} 101 | X:: 1.0000... . 102 | But `absolve` by itself is deterministic and doesn't separate roots (not required for this pathological case with only one root). In general the best approach may be to use `solve` with a relatively low precision to separate the roots and `absolve` to subsequently refine the individual solutions. 103 | eg 104 | ?- X::real, {X**2-2*X+1==0}, solve(X,3), absolve(X), writeln(X). 105 | _190{real(0.9999755884293553,1.0000246969493995)} 106 | X:: 1.0000... ; 107 | false. 108 | As before, expressing the constraint in an equivalent but different form can be useful, e.g., the Horner form for more precision (but note the additional "non-solution"): 109 | eg 110 | ?- X::real, {1+X*(-2+X) == 0}, solve(X,3), absolve(X), writeln(X). 111 | _184{real(0.9999877319556869,1.0000002209746888)} 112 | X:: 1.0000... ; 113 | _180{real(1.0000002241604191,1.0000117130335848)} 114 | X:: 1.0000... ; 115 | false. 116 | Of course had the constraint been entered in a factored form (eliminating the dependency issue), it takes just a few operations to converge (no solve necessary), but then you already knew the answer: 117 | eg 118 | ?- {(X-1)**2==0}. 119 | X = 1. 120 | By applying these general mechanisms, a satisfactory solution can usually be found but different use cases may require different strategies. As is often the case in using constraints, performance can often be improved (space and/or time) by adding constraints to enhance pruning. This will be explored further in [Using Metalevel Contractors]. 121 | 122 | <#TableOfContents> 123 | 124 | & 125 | [Using Metalevel Contractors] <- link #toc4Using_Metalevel_Contractors 126 | [Horner's Method] <- link https://en.wikipedia.org/wiki/Horner's_method 127 | -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLPBNR_UC_scheduling.myw: -------------------------------------------------------------------------------- 1 | #### Scheduling Problems 2 | 3 | Scheduling and related resource allocation problems are common problems which are also technically hard (NP hard or NP complete). It's also an area for which the application of CLP techniques has proved beneficial. The focus here will be on complete or optimal solutions of relatively small problems rather than heuristics which aim to provide "good enough" results. (Consolidated code for examples in this section can be found in [Appendix 3]). 4 | 5 | ##### Critical Path Scheduling 6 | 7 | The "Critical Path Method" (CPM) is an important traditional scheduling methodology. The problem is described by a set of activities, each with a duration (time), and a set of precedence rules which define a required order between two tasks. That is, given that task P must be completed before starting task Q, then the finish time of P must be less than or equal to the start time of Q (`FinishP =< StartQ`, where `FinishX == StartX + Duration`). 8 | 9 | As with most scheduling problems there is a feasibility variant and an optimization variant. Feasibility determines whether it is possible to finish the required tasks within some specified deadline. Optimization determines the minimum time to complete all the required tasks. With an interval solution, the start and finish times will be intervals for non-critical activities and point (or very narrow) values for activities on the critical path. 10 | 11 | It's fairly straight forward to express CPM problems directly in CLP(BNR). Consider the following example project: 12 | .myw 13 | .tsv 14 | Activity Duration Follows 15 | `a` `10` `start` 16 | `b` `20` `start` 17 | `c` `30` `start` 18 | `d` `18` `a,b` 19 | `e` ` 8` `b,c` 20 | `f` ` 3` `d` 21 | `g` ` 4` `e,f` 22 | `finish` ` ` `g` 23 | Representing a task by the term `task(ID, Start, Finish)` the constraints can be defined as: 24 | ``` 25 | activity(ID, Duration, task(ID, Start, Finish)) :- 26 | {Finish == Start + Duration}. 27 | 28 | order(task(_,_,Finish), task(_,Start,_)) :- 29 | {Finish =< Start}. 30 | ``` 31 | and the example project as: 32 | ``` 33 | project(StartT, FinishT, Schedule) :- 34 | Start = task(start,StartT,StartT), Finish = task(finish,FinishT,FinishT), 35 | Schedule = [A,B,C,D,E,F,G], 36 | activity(a, 10, A), order(Start,A), 37 | activity(b, 20, B), order(Start,B), 38 | activity(c, 30, C), order(Start,C), 39 | activity(d, 18, D), order(A,D), order(B,D), 40 | activity(e, 8, E), order(B,E), order(C,E), 41 | activity(f, 3, F), order(D,F), 42 | activity(g, 4, G), order(E,G), order(F,G), order(G,Finish). 43 | 44 | print_with_intervals(List) :- 45 | foreach(member(Item,List), (print_interval(Item),nl)). 46 | ``` 47 | Feasibility problem: can the project be completed in 60 time units? Omitting (redundant) top level output: 48 | ``` 49 | ?- project(0, 60, Schedule), print_with_intervals(Schedule), nl. 50 | task(a,V0::real(0,25),V1::real(10,35)) 51 | task(b,V0::real(0,15),V1::real(20,35)) 52 | task(c,V0::real(0,18),V1::real(30,48)) 53 | task(d,V0::real(20,35),V1::real(38,53)) 54 | task(e,V0::real(30,48),V1::real(38,56)) 55 | task(f,V0::real(38,53),V1::real(41,56)) 56 | task(g,V0::real(41,56),V1::real(45,60)) 57 | ``` 58 | The output indicates this is possible with significant "floats" for the task start and finish times. Replacing the query finish time with a variable: 59 | ``` 60 | ?- project(0, Finish, Schedule), print_with_intervals(Schedule), nl. 61 | task(a,V0::real(0,1.0Inf),V1::real(10,1.0Inf)) 62 | task(b,V0::real(0,1.0Inf),V1::real(20,1.0Inf)) 63 | task(c,V0::real(0,1.0Inf),V1::real(30,1.0Inf)) 64 | task(d,V0::real(20,1.0Inf),V1::real(38,1.0Inf)) 65 | task(e,V0::real(30,1.0Inf),V1::real(38,1.0Inf)) 66 | task(f,V0::real(38,1.0Inf),V1::real(41,1.0Inf)) 67 | task(g,V0::real(41,1.0Inf),V1::real(45,1.0Inf)) 68 | 69 | Schedule = [task(a, _A, _B), task(b, _C, _D), task(c, _E, _F), task(d, _G, _H), task(e, _I, _J), task(f, _K, _L), task(g, _M, _N)], 70 | _B::real(10, 1.0Inf), 71 | _A::real(0, 1.0Inf), 72 | _D::real(20, 1.0Inf), 73 | _C::real(0, 1.0Inf), 74 | _F::real(30, 1.0Inf), 75 | _E::real(0, 1.0Inf), 76 | _H::real(38, 1.0Inf), 77 | _G::real(20, 1.0Inf), 78 | _J::real(38, 1.0Inf), 79 | _I::real(30, 1.0Inf), 80 | _L::real(41, 1.0Inf), 81 | _K::real(38, 1.0Inf), 82 | _N::real(45, 1.0Inf), 83 | _M::real(41, 1.0Inf), 84 | Finish::real(45, 1.0Inf). 85 | ``` 86 | Because there was no "deadline" specified, all the task finish times have an infinite upper bound. However "`Finish`" has a lower bound of 45, suggesting this may be the optimal finish value, so we'll try setting the finish time to its lower bound to see: 87 | ``` 88 | ?- project(0, Finish, Schedule), lower_bound(Finish), print_with_intervals(Schedule), nl. 89 | task(a,V0::real(0,10),V1::real(10,20)) 90 | task(b,0,20) 91 | task(c,V0::real(0,3),V1::real(30,33)) 92 | task(d,20,38) 93 | task(e,V0::real(30,33),V1::real(38,41)) 94 | task(f,38,41) 95 | task(g,41,45) 96 | 97 | Finish = 45, 98 | Schedule = [task(a, _A, _B), task(b, 0, 20), task(c, _C, _D), task(d, 20, 38), task(e, _E, _F), task(f, 38, 41), task(g, 41, 45)], 99 | _B::real(10, 20), 100 | _A::real(0, 10), 101 | _D::real(30, 33), 102 | _C::real(0, 3), 103 | _F::real(38, 41), 104 | _E::real(30, 33). 105 | ``` 106 | Indeed, `45` is the minimal project finish time. Some of the tasks (`a`, `c`, and `e`) still have "floats" in their start/finish time while the others (`b`, `d`, `f` and `g`) are on the "critical path". 107 | 108 | In general, optimization is more difficult than feasibility and may require branch-and-bound or directed search techniques described in earlier sections ([Travelling Salesman] or [Global Optimzation]) so some justification of just setting the `Finish` time to its lower bound is required. We know that there is no guarantee that the lower bound of a constrained interval is, in fact, a point solution. Indeed it usually isn't due to outward rounding of calculated floating point values and the effect of the dependency problem which prevent optimal narrowing during constraint propagation. However, it is valid in this case due to two reasons. First the interval bounds are precise (integer values) so rounding is not an issue. Second all the interval relations (addition and inequality) used by the constraints are "interval convex". Without going into the theory, this ensures that the bounds are in fact solutions. 109 | aside> Aside: More on interval convexity can be found in [Applying Interval Arithmetic to Real, Integer and Boolean Constraints]. Convex relations and solvable relations are interval convex. A relation is convex if for every two points in it, the line segment between them is also in it; it is solvable if it can be uniquely solved for each of its variables as a continuous function of the remaining variables. Many of the the narrowing primitives are interval convex everywhere (`==, =<, +, -, exp, log, max, min,` odd powers) and the rest (`*, /, abs,' odd powers and trigonometric functions) over smaller states. 110 | 111 | ##### The Job Shop Scheduling Problem (Disjunctive Scheduling) 112 | 113 | The job shop scheduling problem adds resource competition to task serialization (as just described). Each job in a project consists of a sequence of tasks that must be executed in a given order. In addition, each task requires a resource, e.g., a machine in an assembly line, such that two tasks requiring the same machine cannot be executed at the same time. 114 | 115 | Similar to critical path scheduling, the individual tasks will be specified as `task(ID,Machine,Duration)` and ordering as `task_order(ID1,ID2)` (i.e., task `ID1` precedes task `ID2`). Further we'll put all task specifications for a "project" in a module. For example, the problem described in the [Google Job Shop Problem] will be specified as: 116 | .pl 117 | :- module(ggl_1,[]). 118 | 119 | task( start,s_,0). 120 | task(finish,f_,0). 121 | 122 | task(j00,m0,3). 123 | task(j01,m1,2). 124 | task(j02,m2,2). 125 | 126 | task(j10,m0,2). 127 | task(j11,m2,1). 128 | task(j12,m1,4). 129 | 130 | task(j20,m1,4). 131 | task(j21,m2,3). 132 | 133 | 134 | task_order(start,j00). 135 | task_order(j00,j01). 136 | task_order(j01,j02). 137 | task_order(j02,finish). 138 | 139 | task_order(start,j10). 140 | task_order(j10,j11). 141 | task_order(j11,j12). 142 | task_order(j12,finish). 143 | 144 | task_order(start,j20). 145 | task_order(j20,j21). 146 | task_order(j21,finish). 147 | 148 | Note that the job shop schedule is bookended by two "special" tasks of duration `0`: 149 | .pl 150 | task( start,s_,0). 151 | task(finish,f_,0). 152 | 153 | Task serialization imposed by the sequence of tasks in a job can be done much like the critical path scheduling. Ensuring that two tasks, e.g., `P` and `Q`, requiring the same resource can be implemented with a `boolean` which, if true, forces `P` to be done before `Q`, and, if false, `Q` is done before `P` as in: 154 | .pl 155 | disjunct(task(P, SP, FP), task(Q, SQ, FQ), B) :- 156 | B::boolean, 157 | {B == (FP =< SQ), ~B == (FQ =< SP)}. 158 | Enumerating `B` forces a choice between the two alternatives. 159 | 160 | Finally it is advantageous to order the "disjunct" booleans so that those for the busiest machines (total load for all jobs) are enumerated first to favour early failure. This is worthwhile because even the smaller job shop scheduling problems can result in a significant number of booleans. Looking at some [JSS test data], the smallest example of 6 jobs and 6 machines generates 90 booleans or more than {`10^27`} possibilities. (One estimate of the number of stars in the universe at {`10^21`}, so {`10^27`} is a very large number indeed. Only the smaller JSS examples in the referenced respository are feasible using this Prolog constraint based implementation.) And knowing what the maximum load on any machine, a minimum value for the finish time can be specified, if not already defined by task ordering constraints for each job. As is generally the case, the more constraints that can be applied, the higher the chances are of an early failure leading to better pruning of the search space. 161 | 162 | Constructing the list of tasks and necessary constraints from a project (i.e., module): 163 | .pl 164 | tasks(Mod, Tasks, Bs, MaxWeight) :- 165 | findall(Mod:ID,Mod:task(ID,_,_),TIDs), % list of task ID's 166 | maplist(def_task,TIDs,Tasks), % define tasks 167 | serialize(Tasks,[],MBs), % apply constraints, collect (Machine,Boolean) pairs 168 | findall((M,T),Mod:task(_,M,T),Ms), % calculate total load on each machine 169 | collect_times(Ms,[],MTs), 170 | sort(1,@>=,MTs,Weights), % sorted by decreasing load 171 | ordered_booleans(Weights,MBs,L/L,Bs), % order booleans by machine load 172 | Weights = [(MaxWeight,_)|_]. % return max load on any machine 173 | 174 | def_task(Mod:ID,task(Mod:ID,Start,Finish)) :- % define a task with a Start and Finish 175 | Mod:task(ID,_Res,Dur), 176 | [Start,Finish]::integer(0,_), 177 | {Finish == Start+Dur}. 178 | 179 | serialize([], Bs, Bs). 180 | serialize([T|Ts], Bs, AllBs) :- 181 | serialize_(Ts, T, Bs, E), % apply constraints between T and rest of tasks Ts 182 | serialize(Ts, E, AllBs). 183 | 184 | serialize_([], _, Bs, Bs). 185 | serialize_([T|Ts], T0, Bs, AllBs) :- 186 | sequence(T0,T), !, 187 | serialize_(Ts, T0, Bs, AllBs). 188 | serialize_([T|Ts], T0, Bs, AllBs) :- 189 | disjunct(T0,T,B), !, 190 | serialize_(Ts, T0, [B|Bs], AllBs). 191 | serialize_([_|Ts], T0, Bs, AllBs) :- 192 | serialize_(Ts, T0, Bs, AllBs). 193 | 194 | sequence(task(Mod:P,_SP,FP),task(Mod:Q,SQ,_FQ)) :- % task ordering constraint 195 | task_ordered(Mod,P,Q), % P precedes Q 196 | (Mod:task_order(P,Q) -> {FP =< SQ} ; true). % apply constraint if immediate predecessor 197 | sequence(task(Mod:P,SP,_FP),task(Mod:Q,_SQ,FQ)) :- 198 | task_ordered(Mod,Q,P), % P succeeds Q 199 | (Mod:task_order(Q,P) -> {FQ =< SP} ; true). % apply constraint if immediate successor 200 | 201 | task_ordered(Mod,P,Q) :- Mod:task_order(P,Q), !. 202 | task_ordered(Mod,P,Q) :- 203 | Mod:task_order(P,T), 204 | task_ordered(Mod,T,Q). 205 | 206 | disjunct(task(Mod:T1, S1, F1), task(Mod:T2, S2, F2), (M,B)) :- % resource competition constraint 207 | Mod:task(T1,M,_), Mod:task(T2,M,_), % T1 and T2 require the same resource 208 | B::boolean, 209 | {B == (F1= Acc is T+AccIn 227 | ; Acc = T, TMs = TMsIn 228 | ), 229 | collect_times(MTs,[(Acc,M)|TMs],TMsOut). 230 | Note that any tasks already serialized because they're part of the same job, need not be further constrained by resource requirements. 231 | 232 | With this infrastructure, a predicate can be implemented to test the feasibility of a specified deadline: 233 | .pl 234 | schedule(Mod,Finish,Deadline,Tasks) :- 235 | tasks(Mod,Tasks,Bs,_), 236 | memberchk(task(Mod:start,0,_),Tasks), % set start time to 0 237 | memberchk(task(Mod:finish,_,Finish),Tasks), % set finish time =< Deadline 238 | {Finish= (Constraint = {Objective =< Last} -> true ; Last = L), % relax last constraint 310 | nxt_target(Type,Last,B,NxtVal) 311 | ; {Objective =< Val}, % else constrain Objective to better 312 | nb_setval('$min_ratchet', (_,BGoal)), % mark as old 313 | nxt_target(Type,L,Val,NxtVal) 314 | ), 315 | !, 316 | min_ratchet_B(Goal,Objective,{Objective =< NxtVal}). % continue with new constraint 317 | min_ratchet_B(Goal,_Objective,_Constraint) :- % final solution? 318 | catch(nb_getval('$min_ratchet', (_,BGoal)),_,fail), % fail if no solution 319 | nb_delete('$min_ratchet'), % trash global var 320 | Goal = BGoal. % unify Goal with final result 321 | 322 | nxt_target(integer,Lo,Hi,Target) :- 323 | Target is div(Lo+Hi+1,2), % round towards +inf 324 | Target < Hi. 325 | nxt_target(real,Lo,Hi,Target) :- 326 | Target is (Lo+Hi)/2.0, 327 | current_prolog_flag(clpBNR_default_precision,P), 328 | (Hi - Target)/Hi > 10.0**(-P). 329 | Describing how this works is a bit complicated. `Goal` to used to generate a value for `Objective`. If successful, the process is repeated with a lower upper bound for `Objective`. On failure, the upper bound of `Objective` is relaxed, and Goal is retried. When further relaxation is not possible, the last successful solution becomes the (optimal) answer. 330 | 331 | Use `opt_schedule/4` to generate the optimal solution: 332 | .pl 333 | opt_schedule(Mod,Finish,Deadline,Tasks) :- 334 | tasks(Mod,Tasks,Bs,Min), 335 | memberchk(task(Mod:start,0,_),Tasks), % set start time to 0 336 | memberchk(task(Mod:finish,_,Finish),Tasks), % set finish time =< Deadline 337 | {Min =< Finish,Finish =< Deadline}, 338 | min_ratchet_B((enumerate(Bs), 339 | lower_bound(Finish)),Finish). 340 | as in (again with some toplevel output removed for brevity): 341 | eg 342 | ?- opt_schedule(ggl_1,F,13,Tasks),print_with_intervals(Tasks), nl. 343 | task(ggl_1:start,0,0) 344 | task(ggl_1:finish,11,11) 345 | task(ggl_1:j00,2,5) 346 | task(ggl_1:j01,5,7) 347 | task(ggl_1:j02,V0::integer(7,9),V1::integer(9,11)) 348 | task(ggl_1:j10,0,2) 349 | task(ggl_1:j11,V0::integer(2,5),V1::integer(3,6)) 350 | task(ggl_1:j12,7,11) 351 | task(ggl_1:j20,V0::boolean,V1::integer(4,5)) 352 | task(ggl_1:j21,V0::integer(4,6),V1::integer(7,9)) 353 | 354 | F = 11, 355 | So the smallest possible finish time is indeed `11`. 356 | 357 | The job shop scheduling problem has been the subject of much research and the intent of this section is to describe how CLP can be used to solve such problems. The smallest of examples in [JSS test data] can be solved in less than a second to a minute or two (data dependent) but the combinatorial explosion of solutions for the larger data sets makes a pure CLP approach infeasible. As with many applications, the challenge in applying CLP is to correctly model the problem statement formally in CLP, applying as many constraints as possible to prune the search space (which is potentially enormous for JSS). 358 | 359 | <#TableOfContents> 360 | 361 | & 362 | [Applying Interval Arithmetic to Real, Integer and Boolean Constraints] <- link https://ridgeworks.github.io/BNRProlog-Papers/docs/ApplyingInterval.pdf 363 | [Travelling Salesman] <- link #toc4Pruning_the_Seach_Tree_-_The__Travelling_Salesman__Problem 364 | [Global Optimzation] <- link #toc4Global_Optimization 365 | [Google Job Shop Problem] <- link https://developers.google.com/optimization/scheduling/job_shop 366 | [JSS test data] <- link https://github.com/tamy0612/JSPLIB 367 | [Appendix 3] <- link #toc4Appendix_3_-_Scheduling_Code -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLP_BNR_Guide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
Rendering CLP_BNR_Guide.myw
9 | 10 | 11 | -------------------------------------------------------------------------------- /docs/CLP_BNR_Guide/CLP_BNR_Guide.myw: -------------------------------------------------------------------------------- 1 | [MyWordDocStyle] 2 | title> A Guide to CLP(BNR) 3 | centreblock> 4 | **Constraint Logic Programming over Booleans, Integers and Reals** 5 | **Version 0.12.2, May 2025** 6 | 7 | The word "constraint" is used in a variety of contexts from everyday life to physics, mathematics, and other sciences with much the same intuitive meaning, even when the formalization is different. A bead on a wire, a weight on a string, a beaker open to the atmosphere, a space mission limited by onboard fuel, a desert ecology limited by the lack of water are all examples of constrained systems. 8 | 9 | [Constraint logic programming] (CLP) refers to the combination of constraints and logic programming. A constraint is declaratively just a relation, usually a mathematical relation, but it differs operationally from other ways of treating relations. As relations, constraints fit very well into logic programming framework which is also based on relations. CLP(X) is a CLP language over a domain of discourse "X". Prolog itself is an instance of CLP over the domain of Herbrand terms with unification as a special case of constraint solving. 10 | 11 | CLP(BNR) is an instance of CLP({`RR`}), i.e., CLP over the domain of real numbers. It differs from some other CLP({`RR`})'s in that: 12 | * CLP(BNR) is complete in that any real number can be finitely represented even though the set of reals is infinite. It does this by sacrificing precision: a real number {`R`} is represented by an interval ({`L,U`} where {`L= AA x_j in X_j, 1= Aside: This document was authored using "MyWord", a web publishing application loosely described as a user extensible light weight markup language (think MarkDown). See [MyWord] for more information. 27 | 28 | ##### Acknowledgements 29 | The original CLP(BNR) on BNR Prolog using interval arithmetic was developed by Bill Older, John Rummell, and Andre Vellino. Fredéric Benhamou extended it to finite domains during a sabbatical to BNR's Computing Research Lab in 1993. Many of the example applications in this guide were taken from a [Carleton University graduate level course] given by Bill Older in 1995. Additional capabilities from the larger CLP community have been incorporated and are referenced in the relevant sections of this User Guide. 30 | 31 | This version of CLP(BNR) would not have been possible without an active SWI-Prolog development community led by Jan Wielemaker. In particular, the addition of IEEE compatible floating point arithmetic (non-numbers, rounding modes) and native support for rational numbers resulted in a greatly simplified and more efficient implementation. 32 | 33 | TOC> {#id TableOfContents}` `**Contents** 34 | 35 | @include 36 | CLPBNR_Module.myw 37 | CLPBNR_SynSem.myw 38 | CLPBNR_PrgCons.myw 39 | CLPBNR_UsingConstraints.myw 40 | CLPBNR_AlternativeSearch.myw 41 | CLPBNR_Reference.myw 42 | CLPBNR_DevTools.myw 43 | CLPBNR_Appendices.myw 44 | & 45 | [Constraint logic programming] <- link https://dl.acm.org/doi/abs/10.1145/41625.41635 46 | [Cleary (1986)] <- link https://prism.ucalgary.ca/handle/1880/45818 47 | [BNR Prolog Papers] <- link https://ridgeworks.github.io/BNRProlog-Papers 48 | [BNR Prolog source] <- link https://github.com/ridgeworks/BNRProlog-Source-Archive 49 | [CLP(FD)] <- link https://www.metalevel.at/prolog/clpz 50 | [CLIP] <- link https://scholar.lib.vt.edu/ejournals/JFLP/jflp-mirror/articles/2001/S01-02/JFLP-A01-07.pdf 51 | [Newton] <- link https://www.sciencedirect.com/science/article/pii/S0167642397000087 52 | [Numerica] <- link https://www.sciencedirect.com/science/article/pii/S0004370298000538 53 | [Interval Arithmetic: from Principles to Implementation] <- link http://fab.cba.mit.edu/classes/S62.12/docs/Hickey_interval.pdf 54 | [SWI-Prolog] <- link https://www.swi-prolog.org/ 55 | [MyWord] <- link https://github.com/ridgeworks/MyWord 56 | [Introduction to INTERVAL ANALYSIS] <- link https://dl.acm.org/doi/10.5555/1508122 57 | [Carleton University graduate level course] <- link https://www.softwarepreservation.org/projects/prolog/bnr/doc/Older-Introduction_to_CLP%28BNR%29-1995.pdf/view 58 | 59 | @import MyWordDocStyle.mmk pkgs/demo.mmk pkgs/toc.mmk pkgs/asciimath.mmk pkgs/tsv.mmk 60 | {` .. `} <- asciimath 61 | .myw .. <- myword 62 | // Turn off fancy apostrophe so it can be used as single quote 63 | // .. <-