Academia.eduAcademia.edu

A Rule-based Modelling Language for Constraint Programming

CREAM (“Constraints with Rules to EAse Modelling”) is a general rule-based modelling language designed to make easy to use for engineers the formulation of combinatorial problems and the integration of domain-specific knowledge in libraries. This work had a practical application within the European STReP Net-WMS, which aimed at tackling complex real-world 3D packing problems (with rotations) subject to weight and stability constraints from the automotive industry (PSA, FIAT).

THÈSE DE DOCTORAT DE L’UNIVERSITÉ PIERRE ET MARIE CURIE Spécialité : Informatique (EDITE de Paris) Présentée par Julien Pierre MARTIN Pour obtenir le grade de DOCTEUR DE L’UNIVERSITÉ PIERRE ET MARIE CURIE Un Langage de Modélisation à base de Règles pour la Programmation Par Contraintes Présentée le 8 juillet 2010 devant le jury composé de : M. François Clautiaux, Maître de conférences (Examinateur) M. Yves Deville, Professeur (Rapporteur) M. François Fages, Directeur de recherche (Directeur de thèse) M. Daniel Goossens, Maître de conférences (Examinateur) M. Christian Queinnec, Professeur (Président du jury) M. Michel Rueher, Professeur (Rapporteur) Université Pierre & Marie Curie - Paris 6 Bureau d’accueil, inscription des doctorants Esc G, 2ème étage 15 rue de l’école de médecine 75270-PARIS CEDEX 06 Tél. Secrétariat : 01 44 27 28 10 Fax : 01 44 27 23 95 Tél. pour les étudiants de A à EL : 01 44 27 28 07 Tél. pour les étudiants de EM à MON : 01 44 27 28 05 Tél. pour les étudiants de MOO à Z : 01 44 27 28 02 E-mail : [email protected] Thèse préparée à l’INRIA Paris-Rocquencourt EPI CONTRAINTES Domaine de Voluceau, Rocquencourt, BP 105 78 153 LE CHESNAY CEDEX i À Roger et à Rachel ii iii Résumé La programmation par contraintes (PPC) est un style de programmation déclaratif qui connaît un grand succès pour la spécification et la résolution de problèmes combinatoires, y compris en milieu industriel. La PPC est fondée sur deux composantes : une composante contraintes et une composante recherche [Van99]. Les langages de modélisation se concentrent sur l’expression des contraintes du problème. Ils fournissent des constructions de haut niveau et une notation algébrique proche de la notation mathématique. Néanmoins, les constructions proposées et les concepts nécessaires peuvent être parfois nombreux et difficiles d’accès au non-programmeur. De plus, l’expression de la recherche et des heuristiques demeure une tâche de programmation spécifique difficile à appréhender pour le modélisateur, ce qui limite l’application de ces principes de programmation. Cette thèse présente un langage de modélisation général à base de règles, nommé Cream (pour Constraints with Rules to EAse Modelling), qui a été conçu pour rendre accessible aux non-programmeurs la formulation de problèmes combinatoires et l’intégration de connaissances spécifiques à un domaine d’activité dans des bibliothèques de règles. En pratique, les problèmes réels se réduisent rarement à des problèmes purs, et pour un ingénieur il est souvent plus facile de comprendre ou de décrire une structure complexe en petits fragments plutôt qu’en un tout monolithique. C’est pourquoi le langage est fondé sur la définition de règles et leur organisation en bibliothèques, ce qui permet précisément d’exprimer et de composer facilement des fragments de connaissance dans un domaine métier. Dans le même souci d’accessibilité, le langage adopte des structures de données simples basées sur les enregistrements et les listes données avec des itérateurs, intègre contraintes réifiées et globales, et n’autorise pas les définitions récursives. Nous montrons de plus que dans ce formalisme de règles, la composante recherche peut être spécifiée de façon déclarative. Plus précisément, les stratégies de branchement y sont définies par des formules logiques et des critères heuristiques complexes d’ordonnancement des sous-formules dissociés des stratégies de branchement peuvent y être définis de façon originale par filtrage sur les parties gauches des règles. La compilation des modèles Cream produit des programmes de contraintes sur domaines finis avec contraintes réifiées et contraintes globales. Le schéma de compilation fondamental, dit statique, est formalisé par un système de réécriture de termes qui procède par expansion des règles du modèle. Nous décrivons une sémantique déclarative pour les modèles Cream vis-à-vis de laquelle nous prouvons la correction de la transformation, et nous montrons la confluence, la terminaison et la complexité possiblement exponentielle de cette transformation. Certains problèmes sont de trop grande taille pour être complètement développés ou dépendent d’une valeur inconnue au moment de la compilation. À l’égard de tels problèmes dont les modèles ne peuvent être compilés selon le schéma statique, nous étudions un deuxième schéma par génération de code procédural qui conserve la structure de règles du modèle et produit du code légèrement moins efficace mais montré linéaire en la taille du modèle originel. Nous évaluons ensuite l’expressivité du langage et l’efficacité des programmes générés sur des problèmes de placement non seulement académiques mais aussi industriels. Les modèles évalués reposent sur une bibliothèque de modélisation des connaissances en placement appliquée à des problèmes purs et des problèmes de colisage dans l’industrie automobile. Mots-clefs : programmation par contraintes, langage de modélisation, heuristiques déclaratives, bin packing, colisage. iv A Rule-Based Modelling Language for Constraint Programming Abstract Constraint programming (CP) is a declarative programming paradigm which aims at specifying and solving combinatorial problems. CP relies on two components: a constraint component and a search component [Van99]. Modelling languages focus on the constraint component. They provide high-level constructions and a mathematical-like notation. However, the constructions and concepts involved may be numerous and not easily accessible to a non-programmer. Moreover, search and heuristics remain a difficult programming task which limits the applicability of these principles on a larger scale. This thesis presents a general rule-based modelling language, named Cream (for Constraints with Rules to EAse Modelling), designed to make easy to use for ingineers the formulation of combinatorial problems and the integration of domain-specific knowledge in libraries. From a practical point of view, real problems seldom reduce to pure academic problems, and an engineer frequently finds it easier to understand or describe a complex design in small pieces than as a monolithic whole. Cream is based on the definition of rules and library of rules which allow to easily express and compose pieces of knowledge. In the same matter of concern, the language adopts simple data-structures based on records, lists given with iterators, integrates reified constraints and global constraints, and prohibits recursive definitions. Moreover, we show that a rule-based formalism permits to specify in a declarative manner the search component. More precisely, branching strategies can be defined as logical formulae and complex heuristics criteria for ordering sub-formulae may be defined separately from branching strategies by pattern-matching on the rule left-hand sides. The compilation of Cream models yields constraint programs over finite domains with reified and global constraints. The fundamental static compilation scheme is formalised by a term rewriting system that operates by rule expansion of the model. We describe a declarative semantics for Cream models with respect to which we prove the correctness of the compilation. We prove the confluence, the termination and an exponential complexity bound for this transformation. Furthermore, when a problem is too large to be fully expanded or when it depends on a value which is unknown at compile-time, we provide a second scheme generating procedural code. This scheme keeps the rule structure of the model and produces less efficient code but is shown to be linear in the original model size. We evaluate the expressiveness and the efficiency of generated programs not only on academic but also on real-size industrial bin packing problems. The evaluated models rely on a placement modelling library used on pure problems and real business problems coming from the automotive and logistic industry. Keywords : constraint programming, modelling language, declarative heuristics specification, bin packing. v vi Remerciements Je tiens à remercier mon directeur de thèse, François Fages, pour m’avoir offert la chance de me confronter à des questions stimulantes et enrichissantes et pour m’avoir permis d’aboutir. Je lui sais gré du privilège qu’il m’a accordé en me permettant de travailler avec lui dans un cadre idéal, au sein de l’équipe-projet Contraintes du centre de recherche de l’INRIA Paris-Rocquencourt. Merci de m’avoir fait profiter de son expérience, de sa créativité, de sa rigueur et de son optimisme. Je le remercie également de m’avoir guidé dans mes questionnements et de m’avoir aidé à produire, (ré)organiser et mettre en relief les réponses proposées. Merci à Yves Deville et à Michel Rueher qui m’ont fait l’honneur de bien vouloir être rapporteurs et qui par leurs commentaires, remarques et critiques m’ont permis d’améliorer la qualité de mon mémoire de thèse. Je remercie tous les autres membres du jury. Merci à François Clautiaux pour sa disponibilité et l’attention qu’il a porté à ma thèse. Merci à Daniel Goossens qui a été le premier à me transmettre le goût de la recherche via les problématiques fondamentales liées à la résolution du problème SAT et avec qui je prends toujours beaucoup de plaisir à échanger. Merci aussi de m’avoir fait confiance à deux reprises pour prendre en charge un cours. Mes remerciements vont aussi à Christian Queinnec dont j’ai déjà pu apprécier l’écoute, le support et la bienveillance en sa qualité de directeur de mon école doctorale, l’EDITE de Paris. Cette thèse doit beaucoup au projet européen Net-WMS qui s’est attaché à traiter des problèmes de colisage issus de l’industrie automobile. Ce projet fut une chance formidable pour concevoir et évaluer un langage de modélisation et de nouvelles techniques de programmation par contraintes. En effet, il a fournit à la fois les spécifications et des instances de problèmes réels et aussi les exigences et les pratiques d’ingénieurs experts en logistique mais non-programmeurs. En outre, j’ai apprécié travailler et me détendre avec tous les membres du projet dont je salue la compétence et la bonne humeur. En particulier, merci à Mats Carlsson du SICS (Uppsala) et à Nicolas Beldiceanu du LINA (Nantes) avec qui j’ai eu l’honneur et le plaisir de travailler plus étroitement. Mats et Nicolas forment une équipe de chercheurs hors pair et la passion qui les anime est communicative. Ils ont été pour moi des exemples de rigueur et de précision. Pendant ces quatre dernières années (stage de master inclus), j’ai partagé mes journées avec les descendants des irréductibles Voluçois du fameux bâtiment 8. Il s’agit en premier lieu de tous les membres passés et actuels du projet Contraintes que j’ai eu l’occasion de côtoyer. Je remercie particulièrement Thierry Martinez pour ses années d’activité dans notre « groupe de travail ». Nos nombreuses discussions à propos de nos thèses respectives et bien d’autres sujets ont été pour moi extrêmement instructives, fertiles, et plaisantes ; j’espère pouvoir les poursuivre. Merci à Sylvain Soliman pour sa vii viii disponibilité, pour tous ses conseils et ses critiques avisés, pour nos régulières parties de squash et pour avoir su infailliblement animer le quotidien. Ma gratitude s’adresse aussi à Grégory Batt pour nos discussions impromptues certains week-ends travaillés ; à Elisabetta De Maria pour son aménité ; à Pierre Deransart pour m’avoir aidé à me maintenir en forme ; à Steven Gay pour avoir relu spontanément une partie de l’ébauche de ce mémoire ; à Rémy Haemmerlé pour avoir partagé son bureau et son temps lors de mon arrivée ; à Domitille Heitzler, Dragana Jovanovska et Faten Nabli pour leur bonne humeur ; à András Kovács, qui a commencé le projet européen avec nous en tant que post-doctorant, pour son aide et ses encouragements et pour sa générosité et sa gentillesse ; à Aurélien Risk pour n’avoir pas pris la navette de retour sans moi lors des JFPC’08 et enfin à Surinderjeet Singh, Akhil Deshmukh et Curtis Fonger pour avoir été les premiers utilisateurs de Cream et avoir permis de rendre son implantation moins boguée. Merci beaucoup à Nadia Mesrar pour le sérieux et l’amabilité avec lesquels elle m’a toujours soulagé des préoccupations administratives. Merci et bon courage à Roxane Bonin qui lui a succédé. De la même façon, merci à Myriam Brettes et Catherine Verhaeghe pour leur support. J’aimerais ne pas oublier les autres occupants du bâtiment 8. Merci à Luc Maranget pour avoir répondu volontiers et gentiment à une ou deux questions bêtes de compilation, ainsi que pour nos discussions de toutes natures qui me furent si profitables et plaisantes. Merci à Jade Alglave pour sa solidarité et pour avoir partagé avec moi un certain nombre de ces moments qui tendent à être interdits. Merci à Bernard Lang, Jean-Jacques Lévy, et Pierre Weis pour la transmission d’un fragment de leur savoir et pour leur humour. Merci à Philippe Deschamp de s’être préoccupé de ma santé et d’avoir essayé de réveiller ma fibre artistique. Bien que Benoît Sagot soit devenu rare à Rocquencourt, je n’ai pas oublié son entrain et son goût pour la langue française. Merci à Éric Villemonte de la Clergerie pour son affabilité et pour avoir continué, après mon stage de master dans son équipe, à me faire partager l’avancement de ses travaux de recherche avec tant de passion et de pédagogie. Éric et Xavier Leroy ont été mes rapporteurs internes à l’INRIA, qu’ils en soient remerciés. Merci à mes amis pour tous nos moments partagés ; qu’il se soit agi d’activités saines ou un peu moins saines, toutes ont su me ressourcer et me rappeler à l’essentiel. Il serait trop long d’exprimer ici à quel point je suis reconnaissant et redevable envers mes parents et mes soeurs. Je les remercie alors le plus simplement ainsi que le reste de ma famille, entendue au sens propre comme au sens figuré. Je remercie Ghada pour ce qu’elle est, pour le bonheur qu’elle me procure et pour son indéfectible soutien. Je la remercie pour ce qu’elle me permet d’être et surtout de devenir. Beaucoup de choses de la vie seraient plus ternes, moins savoureuses sans elle et de nombreuses autres que j’apprécie tant n’existeraient simplement pas. ix x Table des matières 1 Introduction 1 I 7 État de l’art 2 Programmation par contraintes (CP) 2.1 Problème d’optimisation combinatoire (COP) . . . . . . . . . . . . . . . 10 2.2 Problème de satisfaction de contraintes (CSP) . . . . . . . . . . . . . . . 11 2.3 Programmation logique avec contraintes (CLP) . . . . . . . . . . . . . . 12 2.4 Programmation impérative avec contraintes (CIP) . . . . . . . . . . . . . 16 3 Langages de modélisation 19 3.1 OPL et COMET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 3.2 ZINC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 3.3 ESSENCE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 3.4 s-COMMA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 3.5 Langages CLP et CIP . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28 3.6 Langages BRMS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 4 Procédures de recherche II 9 33 4.1 Parcours d’arbres de recherche . . . . . . . . . . . . . . . . . . . . . . . . 33 4.2 Stratégies de branchement . . . . . . . . . . . . . . . . . . . . . . . . . . 37 4.3 Heuristiques d’ordonnancement de choix . . . . . . . . . . . . . . . . . . 38 Langage de Modélisation à base de règles 5 Syntaxe et sémantique 41 43 xi TABLE DES MATIÈRES xii 5.1 Syntaxe de Cream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 5.2 Sémantique déclarative des modèles Cream . . . . . . . . . . . . . . . . . 48 6 Compilation 6.1 6.2 53 Compilation vers CSP par expansion des règles . . . . . . . . . . . . . . 54 6.1.1 Transformation en code déterministe . . . . . . . . . . . . . . . . 54 6.1.2 Transformation en code non déterministe . . . . . . . . . . . . . . 59 6.1.3 Confluence, terminaison, complexité et correction . . . . . . . . . 61 Compilation vers CLP par génération de code procédural . . . . . . . . . 65 6.2.1 Transformation en code déterministe . . . . . . . . . . . . . . . . 65 6.2.2 Transformation en code non déterministe . . . . . . . . . . . . . . 66 6.2.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 7 Heuristiques de recherche déclaratives 75 7.1 Heuristiques pour les formules conjonctives et disjonctives . . . . . . . . 76 7.2 Heuristiques pour les affectations de variables . . . . . . . . . . . . . . . 79 7.3 Heuristiques pour des problèmes de placement . . . . . . . . . . . . . . . 80 III Évaluation 81 8 Bibliothèque pour la modélisation de connaissances en placement 8.1 8.2 8.3 Formes et objets k-dimensionnels . . . . . . . . . . . . . . . . . . . . . . 85 8.1.1 Assemblages de formes k-dimensionnelles . . . . . . . . . . . . . . 85 8.1.2 Alternatives d’assemblages de formes . . . . . . . . . . . . . . . . 86 Relations et règles de placement génériques . . . . . . . . . . . . . . . . . 87 8.2.1 Relations d’Allen . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 8.2.2 Relations du Region Connection Calculus (RCC) . . . . . . . . . 88 8.2.3 Règles de placement . . . . . . . . . . . . . . . . . . . . . . . . . 89 Règles de placement spécifiques . . . . . . . . . . . . . . . . . . . . . . . 90 8.3.1 Règles relatives au poids . . . . . . . . . . . . . . . . . . . . . . . 90 8.3.2 Règles relatives aux longueurs et aux surfaces . . . . . . . . . . . 91 9 Modèles et résolution de problèmes de placement 9.1 83 Problème de placement optimal de carrés dans un rectangle 95 . . . . . . . 96 TABLE DES MATIÈRES 9.2 9.3 xiii 9.1.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 9.1.2 Modèle et évaluation . . . . . . . . . . . . . . . . . . . . . . . . . 96 Problème de chargement de palette . . . . . . . . . . . . . . . . . . . . . 101 9.2.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 9.2.2 Modèle et évaluation . . . . . . . . . . . . . . . . . . . . . . . . . 101 Problème de chargement de container . . . . . . . . . . . . . . . . . . . . 105 9.3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 9.3.2 Modèle et évaluation . . . . . . . . . . . . . . . . . . . . . . . . . 106 10 Conclusion 113 Bibliographie 117 xiv TABLE DES MATIÈRES Chapitre 1 Introduction La programmation par contraintes (PPC) est un paradigme de programmation déclaratif qui s’attache à exprimer et à résoudre une grande variété de problèmes combinatoires. Depuis de nombreuses années, la PPC rencontre un grand succès dans la résolution de problèmes concrets de planification, d’ordonnancement, de configuration et de placement. La PPC s’articule autour d’une architecture simple à deux composantes : une composante contraintes et une composante recherche [Van99]. Un programme de contraintes décrit à la fois la forme des solutions d’un problème à l’aide de relations sur des variables et la stratégie de recherche à adopter pour explorer l’espace des combinaisons. Les langages de programmation logique avec contraintes (PLC), comme Prolog avec contraintes, sont des représentants anciens et populaires de la PPC qui ont largement participé à son succès. Mais ces langages ne sont pas aussi déclaratifs et d’aussi haut niveau qu’un modélisateur non-programmeur le voudrait. Notamment, ils n’offrent pas de quantificateurs ou d’itérateurs génériques qu’il faut écrire avec des définitions récursives. De plus, ils introduisent des constructions de contrôle qui sortent du paradigme déclaratif. Enfin, la formulation des problèmes et l’expression des stratégies de recherche ne sont pas clairement séparées. Par ailleurs, les langages de PLC ne garantissent pas le terminaison des programmes. La non-terminaison de l’exécution n’est pas une propriété nécessaire pour la modélisation et rend d’autant plus difficile l’accès à ces langages pour les non-programmeurs. Les langages de modélisation répondent aux besoins d’abstraction nécessaires pour se concentrer sur la formulation des problèmes dans un cadre purement déclaratif. OPL, Comet, Zinc et Essence forment l’état de l’art des langages de modélisation. Ils offrent tous des constructions de haut niveau et une notation algébrique proche de la notation mathématique. Parmi ces langages, OPL et Comet permettent à la fois de formuler le problème et, séparément, d’exprimer la stratégie de recherche. OPL est historiquement le premier langage de modélisation à offrir d’un côté un langage de contraintes riche et aussi des constructions de programmation pour écrire la recherche. Comet est un langage de modélisation orienté objet, qui peut être vu comme une évolution d’OPL vers l’hybridation des techniques de PPC et de recherche locale. Ces deux langages sont compilés en du code procédural conservant la structure des modèles (sans expansion des modèles) et la terminaison de la compilation est garantie. Ce code est ensuite exécuté par des machines virtuelles dédiées efficaces mais il n’y a pas de garantie de la terminaison de l’exécution, les modèles pouvant faire usage de boucles « tant que » (while) ou 1 2 de définitions récursives. Zinc et Essence, quant à eux, génèrent du code « plat » par expansion des modèles et n’offrent pas les moyens d’exprimer les stratégies de recherche. Ils garantissent la terminaison de la compilation et de l’exécution de ses modèles. Le point fort de Zinc est sa compatibilité avec la majorité des solveurs de CSPs. Il permet par ailleurs d’étendre le langage par des définitions utilisateur non récursives. Essence propose des structures très riches (multiensembles, fonctions, relations, partitions, etc.) qui autorisent l’utilisateur à déclarer des variables dont les valeurs sont des objets combinatoires. C’est le compilateur qui prend soin de reformuler et transformer en des CSPs les contraintes des modèles Essence impliquant de tels objets très structurés. Mais ce langage ne peut être étendu par des définitions utilisateur de nouvelles contraintes. Formuler des problèmes combinatoires reste un art difficile. En pratique, un problème issu de l’industrie est plus riche et sujet au changement que la définition idéalisée étudiée dans le monde académique. Les contraintes rencontrées dans les problèmes industriels sont des règles de bon sens ou des règles dites métier. Ces dernières représentent les connaissances spécifiques d’une entité industrielle sur son domaine d’application, c’est-àdire son expertise et ses exigences. Pour l’homme de métier, il est évidemment plus facile de comprendre ou de décrire une structure complexe en petits fragments plutôt qu’en un tout monolithique. De ce point de vue, il est essentiel de se donner les moyens d’exprimer de façon simple et compositionnelle les contraintes afin de faciliter l’expression et la modification des connaissances. Il reste que dans la plupart des cas, une fois modélisés, les problèmes ne sont pas résolus par une stratégie de recherche standard. Alors, sans les moyens de définir et d’expérimenter rapidement une stratégie, les ingénieurs non-experts du domaine ne vont pas plus loin avec la PPC. OPL et Comet offrent une composante pour programmer la recherche, ce qui constitue un obstacle conséquent pour les non-informaticiens. Une partie des travaux présentés ici a été réalisée dans le cadre du projet Européen Strep FP7 Net-WMS 1 qui traite des problèmes de colisage [FAB+ 07] dans l’industrie automobile. Contributions de la thèse Cette thèse présente un langage de modélisation à base de règles pour la programmation par contraintes, baptisé Cream, conçu pour rendre accessible aux non-programmeurs la formulation et la résolution de problèmes combinatoires. Le langage adopte des structures de données simples basées sur les enregistrements et les listes données avec des itérateurs, interdit les définitions multiples et les définitions récursives et garantit la terminaison. La compilation des modèles Cream produit des programmes de contraintes sur domaines finis avec contraintes réifiées et contraintes globales comme par exemple la contrainte géométrique geost [BCP+ 07] consacrée aux problèmes de placement en grande dimension. 1. http://net-wms.ercim.org CHAPITRE 1. INTRODUCTION 3 Les contributions de cette thèse sont les suivantes : 1. La conception et l’implantation d’un langage de modélisation par règles, nommé Cream 2 , permettant d’exprimer des connaissances sur des problèmes spécifiques par fragments plutôt qu’en un tout monolithique, de les intégrer dans des bibliothèques métier, et de les composer. 2. La démonstration que dans un tel langage de modélisation par règles, les heuristiques d’ordre de recherche peuvent être exprimées de manière déclarative, dissociées de l’expression de la stratégie de branchement, par pattern-matching sur les têtes de règles. 3. L’étude par un système formel de deux schémas de compilation : un schéma statique fondamental de compilation qui procède par expansion des règles des modèles et un schéma de compilation dynamique par génération de code procédural. La compilation et l’exécution des modèles Cream sont garanties de se terminer. 4. La conception d’une bibliothèque de règles dédiée à la modélisation des connaissances en problèmes de placement multi-dimensionnels, et son évaluation sur des problèmes industriels en taille réelle issus du projet européen. Plan de la thèse La première partie de la thèse introduit les notions de problèmes de satisfaction de contraintes, de programmation logique par contraintes, et de stratégies de recherche qui sont les fondements de la discipline qui nous intéresse. Dans le chapitre 3, un rapide état de l’art des langages de modélisation pour la programmation par contraintes montre quelles réponses les auteurs ont apportées aux différents aspects de la problématique de modélisation. Cette revue amène à conclure que ces langages facilitent considérablement le travail des membres de la communauté mais qu’ils restent difficiles d’accès aux ingénieurs non spécialistes en programmation. Le dernier chapitre de la partie présente les principaux types d’exploration de l’espace de recherche et d’heuristiques d’ordonnancement de choix. La deuxième partie décrit formellement le langage de modélisation Cream. Le chapitre 5 commence avec la présentation de la syntaxe du langage qui introduit les choix des constructions (du langage) et donne des intuitions sur leur interprétation. La section 5.2 définit une sémantique déclarative pour les modèles clos. Le chapitre suivant (Chap. 6) présente la compilation du langage des modèles par deux schémas qui produisent un code intermédiaire destiné à être transformé en programmes de contraintes avec contraintes réifiées et contraintes globales. Le schéma de compilation fondamental par expansion de règles (Sec. 6.1) définit inductivement un système de réécriture de termes qui développe complètement les définitions du langage et profite d’un mécanisme d’évaluation partielle. La définition est accompagnée de résultats de confluence, de terminaison, de complexité et de correction de la transformation vis-à-vis de la sémantique déclarative. Suit la présentation du schéma par génération 2. Une implantation du compilateur Cream, jusqu’à présent nommé Rules2CP, est diffusée sur le web depuis 2009 à l’URL suivante : http://contraintes.inria.fr/rules2cp 4 de code procédural (Sec. 6.2) qui conserve la structure de règles du modèle compilé et produit du code moins efficace mais montré linéaire en la taille du modèle original. Le chapitre 7 montre comment écrire des heuristiques de recherche déclarativement dans un tel langage à base de règles. La dernière partie de la thèse propose une évaluation de l’expressivité du langage et de l’efficacité des programmes générés. Le chapitre 8 décrit une bibliothèque dédiée à la modélisation des connaissances en placement fondée sur des relations entre formes en dimensions supérieures. Elle fournit un ensemble de définitions de règles de placement générales associées aux problèmes noyaux et un ensemble de règles spécifiques impliquées dans les problèmes industriels réels. Enfin, le chapitre 9 présente l’évaluation du langage à la fois sur des problèmes de placement académiques et sur des problèmes réels de colisage dans l’industrie automobile issus du projet Net-WMS. CHAPITRE 1. INTRODUCTION 5 6 Première partie État de l’art Sommaire 2 3 4 Programmation par contraintes (CP) 2.1 Problème d’optimisation combinatoire (COP) . . 2.2 Problème de satisfaction de contraintes (CSP) . . 2.3 Programmation logique avec contraintes (CLP) . 2.4 Programmation impérative avec contraintes (CIP) . . . . 9 10 11 12 16 . . . . . . 19 21 24 26 27 28 29 Procédures de recherche 4.1 Parcours d’arbres de recherche . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Stratégies de branchement . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 Heuristiques d’ordonnancement de choix . . . . . . . . . . . . . . . . . . 33 33 37 38 Langages de modélisation 3.1 OPL et COMET . . . . 3.2 ZINC . . . . . . . . . . . 3.3 ESSENCE . . . . . . . . 3.4 s-COMMA . . . . . . . . 3.5 Langages CLP et CIP . 3.6 Langages BRMS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 SOMMAIRE Chapitre 2 Programmation par contraintes (CP) Sommaire 2.1 Problème d’optimisation combinatoire (COP) . . . . . . . . 10 2.2 Problème de satisfaction de contraintes (CSP) . . . . . . . . 11 2.3 Programmation logique avec contraintes (CLP) . . . . . . . 12 2.4 Programmation impérative avec contraintes (CIP) . . . . . 16 La programmation par contraintes (PPC), ou constraint programming (CP), est un paradigme de programmation déclaratif issu de l’intelligence artificielle, de la programmation logique et de l’optimisation combinatoire. La PPC a apporté des progrès considérables dans la résolution de problémes de planification [VG06], d’ordonnancement [BPN01], de configuration [Jun98] et de placement [Kor04, BCP+ 07, CJCM08, SO08]. L’intérêt d’un langage déclaratif repose sur le fait que pour un ingénieur nonprogrammeur, il est souvent plus naturel de décrire une exigence plutôt qu’une séquence d’actions à mener pour la satisfaire, en particulier quand un aspect combinatoire est impliqué. En pratique, la PPC n’est pas aussi abordable et déclarative que le voudraient ses utilisateurs, potentiellement non-programmeurs. La partition entre le quoi et le comment est fondamentale pour les langages déclaratifs. Ce clivage est exprimé dans le slogan « Algorithme = Logique + Contrôle » de Kowalski qui proposait en 1979 de distinguer deux parties dans un algorithme : la partie logique qui spécifie les connaissances nécessaires à la résolution d’un problème (les contraintes et leur composition dans le cas de la PPC) et la partie contrôle qui décrit comment utiliser cette connaissance pour résoudre le problème (la résolution des contraintes et la recherche en PPC). De ce point de vue, un langage est d’autant plus déclaratif qu’il soulage l’utilisateur de la tâche de contrôle. En outre, une spécification a l’avantage d’être plus simple à maintenir et à communiquer qu’un algorithme. Malgré tout, il est à noter que le contrôle ne disparaît jamais complètement dans les langages déclaratifs et se montre souvent crucial en terme d’efficacité. La PPC permet d’exprimer la classe de problèmes combinatoires NP-complets regroupés sous le nom de problèmes de satisfaction de contraintes (CSP). Un programme de contraintes décrit les propriétés souhaitées des solutions à un problème sous la forme 9 Problème d’optimisation combinatoire (COP) 10 d’une conjonction de contraintes potentiellement définies inductivement (CLP). La composante recherche de la PPC est considérée dans le chapitre 4. 2.1 Problème d’optimisation combinatoire (COP) Un problème d’optimisation combinatoire consiste à trouver les éléments d’une structure finie S respectant un ensemble de propriétés P et minimisant (ou maximisant) une fonction f , dite objectif. Les problèmes de décision associés sont souvent des problèmes NP-complets. Définition 1 (Problème de décision combinatoire). Un problème de décision (i.e. dont la réponse est oui ou non) pose la question de l’existence d’un élément pour lequel la fonction objectif est supérieure (ou inférieure) à une valeur donnée. Il sont souvent NP-complets. Définition 2 (Problème d’optimisation combinatoire). Un problème d’optimisation combinatoire p est défini par le triplet (N, P, f ) où N est un ensemble fini, f : 2N 7→ R, et P ⊆ 2N . L’ensemble des solutions d’un problème p est {S ∈ P | f (S) = max f (S ′ )} ′ S ∈P Problèmes NP-complets La théorie de la complexité organise les problèmes algorithmiques par niveau, classe de « difficulté ». Intuitivement, un problème dans la classe NP peut être résolu par l’énumération de l’ensemble des solutions possibles puis par un test de validité effectué par un algorithme polynomial. Définition 3 (Classe P ). Un problème de décision est dans la classe P des problème polynomiaux si et seulement s’il peut être résolu par un algorithme de complexité en temps polynomial en la taille de l’instance. Définition 4 (Classe NP). Un problème de décision est dans la classe NP des problèmes non déterministes polynomiaux si et seulement s’il peut être résolu par un algorithme non déterministe polynomial. Définition 5 (Problème NP-complet). Un problème NP-complet est un problème de décision dans NP qui permet de résoudre tout autre problème dans NP par un codage polynomial. Les problèmes NP-difficiles sont au moins aussi difficiles que les problèmes NP. La classe NP-difficile contient la classe NP et peut être utilisée pour qualifier la complexité d’un problème d’optimisation dont le problème de décision associé est NP-complet. Problèmes de placement Considérons le problème classique de placement et sa généralisation à un nombre de dimensions quelconque. CHAPITRE 2. PROGRAMMATION PAR CONTRAINTES (CP) 11 Définition 6 (Problème de Placement 1D). Un problème de placement unidimensionnel (Bin-Packing Problem) consiste à trouver le plus petit entier m tel que la partition I1 ∪ · · · ∪ Im de n articles de tailles s1 , . . . , sn dans mXrécipients de taille c respecte la capacité des récipients, c’est-à-dire : ∀b ∈ {1, . . . , m}, si ≤ c i∈Ib Définition 7 (Problèmes de Placement kD). Le problème de décision de placement kdimensionnel consiste à déterminer s’il existe un placement de n orthotopes (boîtes) de tailles (s11 , . . . , s1k ), . . . , (sn1 , . . . , snk ) dans b orthotopes de taille (s1 , . . . , sk ) tel qu’aucune paire d’orthotopes placés ne se chevauche dans les k dimensions à la fois. Le problème d’optimisation associé consiste à trouver le placement qui minimise le nombre b de contenants utilisés. Proposition 1. Le problème de décision de placement k-dimensionnel est NP-complet. 2.2 Problème de satisfaction de contraintes (CSP) De nombreux problèmes combinatoires peuvent être formulés comme des problèmes de satisfaction de contraintes. Informellement, résoudre un CSP consiste à trouver les affectations d’un ensemble de variables à domaines finis qui satisfont un ensemble de contraintes portant sur ces variables. Définition 8 (Problème de Satisfaction de Contraintes). Un CSP P est défini par le triplet (X, D, C) où X = {X1 , . . . , Xn } est un ensemble de variables, D = {D1 , . . . , Dn } est un ensemble de domaines finis tel que ∀i, Xi ∈ Di , et C = {C1 , . . . , Cm } un ensemble de contraintes. Chaque contrainte Ci est un couple (Ri , Si ), où Ri est une relation Ri ⊆ Di1 × · · · × Dik définie sur un sous-ensemble des variables Si = {Xi1 , . . . , Xik }, nommé portée de Ci , qui détermine tous les n-uplets de valeurs pour (Xi1 , . . . , Xik ) qui sont compatibles entre eux. Définition 9 (Réseau de contraintes). Un réseau de contraintes est un hypergraphe N = (V, E) où chaque noeud de V représente une variable Xi ∈ X, et où chaque hyperarête de E lie les variables Sj apparaissant dans la même contrainte Cj ∈ C. Définition 10 (Solution). Une solution à un CSP P est une affectation des variables a : X → D telle que toutes les contraintes soient satisfaites :   ∀Ci ∈ C, a(Xi1 ), . . . , a(Xik ) ⊆ Ri Définition 11 (Espace de recherche). L’espace de recherche désigne l’ensemble des combinaisons possibles de valeurs pour les variables, c’est-à-dire le produit cartésien de leur domaine D1 × · · · × Dn . Exemple 1 (Problème des n-reines). Le problème des n-reines consiste à placer n reines sur un échiquier de taille n, de telle sorte qu’aucune paire de reines ne puisse s’attaquer. Soit X = {Q1 , . . . , Qn } les n variables du problème, chaque variable Qi représente la ligne de l’échiquier de la reine placée sur la colonne i. Soit D = {1, . . . , n}, le domaine des variables. Les contraintes imposent que chaque reine soit sur une ligne différente (C1 ), et qu’aucune paire de reines ne soit sur une même diagonale (C2 et C3 ) : – C1 = ∀i < j ∈ {1, . . . , n}, Qi 6= Qj – C2 = ∀i < j ∈ {1, . . . , n}, Qi 6= Qj + j − i – C3 = ∀i < j ∈ {1, . . . , n}, Qi 6= Qj + i − j Programmation logique avec contraintes (CLP) 12 Résolution de contraintes Dans un système de contraintes donné, à chaque contrainte primitive est associé un algorithme de propagation qui réduit l’ensemble des valeurs possibles des variables. Afin de réduire l’ensemble des valeurs possibles pour les variables d’un réseau de contraintes, la PPC se fonde sur des algorithmes génériques de consistance de noeuds, d’arcs et de chemins dans les réseaux [Mac77, MF85, VHDT92]. On distingue dans l’ensembles des contraintes les contraintes binaires et les contraintes n-aires dites globales. Ces dernières exploitent l’efficacité d’algorithmes spécialisés dans le traitement de sous-problèmes récurrents plus structurés. D’un point de vue opérationnel, étant données une contrainte et les variables y apparaissant, la propagation élimine du domaine de chacune de ces variables les valeurs incompatibles avec la contrainte, compte tenu du domaine des autres. Certaines variables pouvant apparaître dans d’autres contraintes, l’opération est répétée pour ces contraintes, et ainsi de suite jusqu’à atteindre un point fixe. En général, la propagation n’élimine pas assez de valeurs du domaine des variables pour obtenir une solution et doit être combinée à une procédure de recherche (cf. Chap. 4) dans l’espace des combinaisons possibles. La propagation est itérée jusqu’au point fixe à chaque étape de la recherche. 2.3 Programmation logique avec contraintes (CLP) Programmation logique La programmation logique (PL), ou logic programming (LP) [CKPR72, Kow74, Col84] est un paradigme de programmation déclaratif. Un programme logique définit inductivement un ensemble de relations ou prédicats par des clauses logiques qui sont exécutées selon un principe de déduction automatique. Un programme logique, comme un programme de contraintes, est une formule écrite dans une logique donnée et son exécution correspond à une recherche de preuve. En programmation logique, incarnée par le langage Prolog [CKPR72, Col84]), le choix du domaine des variables est restreint à l’univers de Herbrand. Définition 12 (Clause de Horn). Une clause de Horn est une clause qui contient au plus un littéral positif : C ∨¬H1 ∨· · ·∨¬Hn , ou de façon équivalente (H1 ∧· · ·∧Hn ) ⇒ C où C, H1 . . . Hn sont des atomes. Définition 13 (Fait). Un fait est une clause de Horn positive, i.e. qui ne contient qu’un littéral positif et aucun atome négatif, e.g. C. Définition 14 (But). Un but est une clause de Horn négative, i.e. qui ne contient que des littéraux négatifs et pas de littéral positif, e.g. ¬H1 ∨ ¬H2 . Une clause Prolog concrète est de la forme C :- H1 , . . . , Hn . et on appelle tête ou partie gauche de la clause l’atome C et corps ou partie droite de la clause la conjonction H1 , . . . , Hn . Prolog autorise la définition de clauses récursives et la portée des variables CHAPITRE 2. PROGRAMMATION PAR CONTRAINTES (CP) 13 est fixée dans la définition même des clauses de Horn (du premier ordre). On appelle prédicat p, l’ensemble des clauses qui le définissent, i.e. l’ensemble des clauses dont le symbole de tête est p. D’un point de vue opérationnel, une clause exprime qu’à partir de la preuve de la conjonction H1 , . . . ,Hn , C peut être déduit. Définition 15 (Programme Prolog). Un programme logique Prolog est un ensemble de clauses de Horn. Le mécanisme d’évaluation d’un programme Prolog, appelé SLD-résolution, consiste à prouver qu’un but est une conséquence du programme en construisant un arbre de recherche de preuve. La résolution commence par essayer d’unifier le but à la partie gauche de chaque clause du programme. Les clauses d’un même prédicat représentent des choix et sont exécutées de manière non déterministe. Lorsque l’unification réussit, la résolution est alors appliquée récursivement en prenant comme nouveaux sous-buts la partie droite de la clause sélectionnée, jusqu’à ce que l’ensemble des sous-buts soit vide (succès) ou qu’aucune nouvelle unification ne puisse être faite (échec). Le contrôle n’est pas exprimé par la SLD-résolution et Prolog adopte la stratégie de recherche en profondeur et à gauche d’abord. Cette stratégie est incomplète (cf. Sec. 4.1) puisqu’une clause récursive peut engendrer un nombre infini d’étapes de résolution. En ce qui concerne le contrôle et, à chaque étape de résolution, les sous-buts en conjonction sont sélectionnés de gauche à droite. Pour le contrôle ou, les clauses sont essayées dans l’ordre d’apparition dans le programme. Lorsqu’un sous-but mène à un échec, la recherche de preuve backtrack, elle revient sur la dernière alternative de clauses et choisit la suivante jusqu’à épuisement. Prolog n’est pas purement déclaratif, il fournit un opérateur de contrôle, nommé cut. Cet opérateur extra-logique, dont nous n’illustrons pas l’usage ici, permet au programmeur d’élaguer un arbre de recherche, c’est-à-dire de ne pas parcourir une partie de l’arbre qu’il sait ne pas mener à succès. Exemple 2. Considérons le programme Prolog suivant, dédié à la généalogie. Faits Soit Pierre le père de Paul, Paul le père de Jacques et de Marie et Marie la mère de Jeanne. Cet arbre généalogique est décrit par l’ensemble de faits suivants : pere(pierre, paul). pere(paul, jacques). pere(paul, marie). mere(marie, jeanne). Clauses Les deux clauses suivantes définissent le prédicat parent/2 en exprimant qu’un individu X est le parent d’un individu Y si X est le père de Y ou si X la mère de Y. parent(X, Y) :- pere(X, Y). parent(X, Y) :- mere(X, Y). Enfin, le prédicat ancetre/2 est défini récursivement comme suit : X est ancêtre de lui-même ou bien X est l’ancêtre de Y si X est parent d’un tiers Z qui est lui-même ancêtre de Y. Programmation logique avec contraintes (CLP) 14 ancetre(X, X). ancetre(X, Y) :- parent(X, Z), ancetre(Z, Y). But et résolution Les preuves du but suivant déterminent tous les ancêtres X de Jeanne. ?- ancetre(X, jeanne). Selon le programme, tous les individus sont des ancêtres de Jeanne. Ce but accepte donc plusieurs preuves, chacune donnant une valeur différente à X qu’on appellera solution. Les deux premières solutions et arbres de preuve associés sont les suivants : 1. X = jeanne Cette première solution est obtenue simplement par unification du but avec la première clause du prédicat ancetre/2. ancetre(jeanne, jeanne) 2. X = paul C’est le deuxième choix possible pour le prédicat ancetre/2 qui amène à cette deuxième solution. Le but est donc ici unifié à la partie gauche de la deuxième clause du prédicat ancetre/2. La résolution par cette clause produit deux nouveaux buts intermédiaires à prouver : parent(X, Z) et ancetre(Z, jeanne). Le premier sous-but parent(X, Z) est prouvé par le fait pere(paul, marie) unifiant X à paul et Z à marie. Il reste donc à prouver ancetre(marie, jeanne). Le deuxième sous-but ancetre(marie, jeanne) est prouvé par parent(marie, jeanne) (prouvé par le fait pere(paul, marie)) et ancetre(jeanne, jeanne) pere(paul, marie) parent(paul, marie) mere(marie, jeanne) parent(marie, jeanne) ancetre(jeanne, jeanne) ancetre(marie, jeanne) ancetre(paul, jeanne) Programmation logique avec contraintes La programmation logique par contraintes (PLC), ou constraint logic programming (CLP) [JL87, JM94] est un développement de la programmation logique qui généralise le problème de la résolution de l’égalité entre termes du premier ordre (l’unification) au problème de la résolution de contraintes sur un domaine fixé, par exemple les contraintes linéaires sur le nombres rationnels. De nombreuses implantations de langages de PLC ont vues le jour, depuis Prolog III [Col87] et Prolog IV [Col96] jusqu’à Gnu-Prolog [DC01], Eclipse [AW07] ou SICStus-Prolog [C+ 07]. Comme en programmation logique, l’exécution de toute autre stratégie de recherche que la recherche en profondeur et à gauche d’abord nécessite de programmer un méta-interpréteur [AW07]. CHAPITRE 2. PROGRAMMATION PAR CONTRAINTES (CP) 15 La programmation logique par contraintes combine en quelque sorte le meilleur de la programmation logique et de la recherche opérationnelle : un langage déclaratif qui permet d’exploiter une algorithmique spécifique à certains domaines de calcul. À chaque contrainte primitive est associé un algorithme de propagation prédéfini qui élimine une partie des valeurs du domaine des variables et pour atteindre une solution une procédure de recherche doit être définie. La PLC reprend le schéma d’exécution de la programmation logique et offre des constructions non déterministes supplémentaires pour écrire de telles stratégies de recherche. La PLC a connu plusieurs succès dans différents domaines d’application en commençant par la conception et l’analyse de circuits électroniques [JM94, Wal96]. La PLC a aussi été appliquée pour la résolution de problèmes de planification, d’ordonnancement et de placement. Exemple 3. Considérons le programme SICStus-Prolog du problème des n-reines (cf. Sec. 2.2). La bibliothèque CLP(FD) relative au système de contraintes sur domaines finis est importée. :- use_module(library(clpfd)). La clause principale du programme est queens(N, L). Elle génère une liste de N variables qui représentent le placement des n reines sur les lignes de l’échiquier (chaque reine est associée à une colonne). Puis elle initialise le domaine de chaque variable à l’intervalle allant de 1 à N, et fait appel au prédicat all_safe/1 qui pose les contraintes et au prédicat labeling/1 qui s’occupe de la recherche d’une solution. queens(N, L) :length(L, N), domain(L, 1, N), all_safe(L, 1), labeling(L). Le prédicat all_safe/2 est une définition récursive qui code une quantification universelle sur la liste des variables du problème. Chaque itération de all_safe/2 consomme un élément de la liste, la tête de liste X, et appelle all_safe_2/4 qui code aussi une quantification universelle mais sur la queue de la liste Xs. Chaque itération de all_safe_2/4 fait appel au prédicat safe/3. all_safe([], _). all_safe([X | Xs], I) :J is I + 1, all_safe_2(X, Xs, I, J), all_safe(Xs, J). all_safe_2(_, [], _, _). all_safe_2(X, [Y | Ys], I, J) :safe(X, Y, I, J), J1 is J + 1, all_safe_2(X, Ys, I, J1). Le prédicat safe/4 pose effectivement les contraintes de non-capture entre un paire de reines donnée (Q1, Q2) grâce à la contrainte binaire de non-égalité #\= entre variables de domaines finis. Programmation impérative avec contraintes (CIP) 16 safe(Q1, Q2, I, J) :Q1 #\= Q2, Q1 #\= Q2 + J - I, Q1 #\= Q2 + I - J. Le prédicat labeling/1 définit la recherche de solution. Cette clause récursive développe l’arbre élémentaire des affectations possibles des variables du problème (cf. Chap. 4). En effet, pour chaque variable H, labeling/1 appelle indomain(H) de la bibliothèque CLP(FD) qui énumère de façon non déterministe l’ensemble des valeurs du domaine de H. labeling([]). labeling([H | T]):indomain(H), labeling(T). Du point de vue du modélisateur non-programmeur, la PLC n’est pas aussi déclarative et d’aussi haut niveau qu’il le voudrait. La PPC propose de dissocier la tâche de modélisation des problèmes, qui doit être purement déclarative, de la tâche de recherche. Or, la PLC hérite de la programmation logique l’opérateur de contrôle non déclaratif cut et souffre d’un manque de séparation entre les deux composantes contraintes et recherche. Les contraintes et les stratégies de recherche sont définies dans un ensemble uniforme de clauses qui peuvent faire usage d’opérateurs extra-logiques. Par ailleurs, des constructions logiques basiques comme les quantificateurs doivent être codées avec des clauses récursives, ce qui n’est pas naturel pour un non-programmeur. Nous comparons les langages de PLC avec les langages dits de modélisation dans le chapitre 3. 2.4 Programmation impérative avec contraintes (CIP) La programmation impérative avec contraintes (PIC) [AS99], ou constraint imperative programming (CIP), est le résultat de la combinaison des techniques de la programmation par contraintes et de la programmation impérative (et orientée objet). La PIC est une réponse aux préoccupations d’ingénierie logicielle et de diffusion de la PPC dans la communauté des programmeurs. En effet, la plupart des programmeurs sont formés et rompus à la programmation impérative qui jouit d’une littérature abondante, dont les différents langages sont largement diffusés, réputés performants et viennent avec de nombreuses bibliothèques en standard et des environnements de développement évolués. Cette approche de la PPC fait un pas vers les langages impératifs en y embarquant un système de contraintes pour convaincre l’industrie d’utiliser les techniques de PPC. Le projet Alma [AS99] est un exemple de langage de PIC et il existe par ailleurs un certain nombres de bibliothèques de contraintes implantées dans des langages impératifs comme Ilog Solver [ILO] et Gecode [SLM] en C++ ou Choco [Lab00] en JAVA. Les langages de PIC sont très puissants mais proposent des constructions de programmation de bas niveau et, comme les langages de PLC, ils ne distinguent pas clairement composante contraintes et composante recherche. Nous proposons de les comparer aux langages dits de modélisation dans la chapitre 3. CHAPITRE 2. PROGRAMMATION PAR CONTRAINTES (CP) 17 Conclusion La PPC est un modèle de langage déclaratif qui fournit un cadre théorique élégant et unificateur pour exprimer et résoudre une grande variété de problèmes d’optimisation combinatoire. La programmation impérative avec contraintes fait le choix d’embarquer un système de contraintes au sein d’un langage de programmation impérative pour encourager l’usage de la PPC dans la communauté des programmeurs. Cependant, le bon modélisateur n’est pas nécessairement programmeur, et la formulation d’un problème se fait mieux dans un cadre purement déclaratif. En ce sens, la programmation impérative avec contraintes n’est pas une incarnation idéale de la PPC. La programmation logique par contraintes est un représentant historique et populaire de la PPC et elle est appliquée avec succès depuis longtemps dans de nombreux domaines d’activité. Elle intègre dans un langage logique les deux composantes de la PPC : un langage de contraintes riche, où chaque contrainte est associée à un propagateur efficace ; et des constructions non déterministes qui soulagent le programmeur pour le développement des stratégies de recherche. Mais du point de vue du modélisateur encore, la PLC souffre d’un manque d’abstraction et n’est pas assez déclarative. En particulier, les langages de programmation logique n’offrent pas certaines constructions essentielles à la modélisation comme par exemple les quantificateurs. Par ailleurs, la composante formulation des problèmes et la composante recherche de solutions ne sont pas clairement distingués au sein de l’ensemble des clauses. Enfin, la programmation d’une stratégie de recherche particulière peut nécessiter l’écriture d’un méta-interpréteur. 18 Programmation impérative avec contraintes (CIP) Chapitre 3 Langages de modélisation Sommaire 3.1 3.2 3.3 3.4 3.5 3.6 OPL et COMET ZINC . . . . . . . ESSENCE . . . . s-COMMA . . . Langages CLP et Langages BRMS . . . . . . . . . . . . CIP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 24 26 27 28 29 Les langages de modélisation répondent à un besoin d’abstraction conceptuelle et méthodologique pour formuler des problèmes d’optimisation combinatoire dans un cadre purement déclaratif. OPL, Comet, Zinc et Essence constituent l’état de l’art. Ils fournissent tous une notation algébrique et différentes structures de haut niveau d’abstraction. OPL propose, et de manière dissociée de la formulation, les moyens de programmer des stratégies de branchement et des heuristiques d’ordonnancement (cf. Sec. et 4.3). Comet, évolution d’OPL, donne en plus les moyens à l’utilisateur de programmer ses propres parcours d’arbres à l’aide de fermetures et de continuations. Nous décrivons dans ce chapitre les traits spécifiques aux différents langages de modélisation de l’état de l’art et donnons pour chacun d’entre eux le modèle du problème des n-reines correspondant. Les deux dernières sections montrent pourquoi nous considérons pas comme des langages de modélisation les langages de programmation logique (ou impérative) avec contraintes et les langages des systèmes de gestion des règles métier. Un tableau de comparaison qualitative Afin de comparer et de différencier ces langages de manière synthétique, nous proposons un tableau récapitulatif de 13 traits bipartis en caractéristiques des langages et caractéristiques de leur compilation et de l’exécution des programmes générés. 19 20 Les caractéristiques des langages sont partitionnées en caractéristiques relatives à la composante contraintes et à la composante recherche. Composante contraintes – extensible : le langage permet à l’utilisateur de définir de nouvelles contraintes. – règles : les définitions de contraintes reposent sur le formalisme de règles. – modulaire : le langage est doté d’un système de modules permettant d’organiser les définitions utilisateur. Composante recherche – stratégie de branchement : le langage permet à l’utilisateur d’écrire ses propres stratégies de branchement, de décider des contraintes (en disjonction et en conjonction) posées lors de la recherche (par exemple énumération des affectations possibles, le labeling, ou découpage de domaines, le domain-splitting, etc ; cf sections 4.2 et 4.3). – heuristiques d’ordonnancement : l’utilisateur peut écrire ses propres heuristiques d’ordonnancement des choix (conjonctifs, par exemple first-fail ; et disjonctifs, par exemple middle-out). – parcours d’arbres : le langage permet à l’utilisateur d’écrire ses propres parcours d’arbres de recherche (par exemple une recherche par approfondissements successifs, IDDFS, ou une recherche avec écarts limités, LDS, etc.). Les caractéristiques de la compilation et de l’exécution des programmes générés sont partitionnées en caractéristiques relatives à la compilation (schéma adopté et vérification de types), à l’automatisation de certains choix de modélisation et à l’exécution des programmes générés : Compilation – statique : les modèles peuvent êtres compilés selon le schéma par expansion des expressions. – dynamique : les modèles peuvent êtres compilés selon le schéma par génération de code procédural qui conserve la structure de définitions des modèles. – typage : le langage est muni d’un système de types et la compilation vérifie la validité des expressions des modèles vis-à-vis du système de type afin d’éviter à l’utilisateur de mauvais usages des structures de données. – reformulation : la compilation peut reformuler le modèle, faire des choix de modélisation et des optimisations pour l’utilisateur. Exécution, résolution des modèles – hybridation : les modèles peuvent faire coopérer plusieurs techniques de résolution différentes (par exemple recherche globale avec contraintes et recherche locale). – multi-solveurs : un même modèle peut être compilé pour différents solveurs, plusieurs implantations de systèmes de résolution différents, appartenant à une même famille de technique ou pas. – termine : la terminaison de l’exécution des programmes générés est assurée afin d’éviter à l’utilisateur d’écrire des modèles de problèmes combinatoires qui ne terminent pas à causes d’erreurs de programmation. Cette garantie permet donc à l’utilisateur de se concentrer purement sur la formulation et la résolution de ses problèmes. CHAPITRE 3. LANGAGES DE MODÉLISATION 3.1 21 OPL et COMET OPL (Optimization Programming Language) OPL [Van99] est un langage pour formuler et résoudre les problèmes d’optimisation combinatoire. OPL a rencontré un succès particulier dans la résolution des problèmes de planification et d’ordonnancement. C’est le premier langage de modélisation à fournir à la fois une notation algébrique de haut niveau avec un langage de contraintes (composante de modélisation par contraintes) et les moyens programmer des stratégies de recherche (composante de programmation de la recherche) [HPP00]. Les expressions d’un modèle portent des types qui sont donnés explicitement et vérifiés à la compilation. Les stratégies de branchement peuvent être exprimées grâce au quantificateur universel forall pour le séquencement et à l’opérateur spécifique de choix non déterministe tryall. Des heuristiques peuvent être écrites comme des ordres (lexicographiques) sur les éléments des listes qui paramètrent les quantificateurs universels forall et les itérateurs non déterministes tryall. Le langage n’est pas extensible (pas de définitions utilisateur), les contraintes sont exprimées de façon monolithique, et le langage ne dispose pas de combinateurs génériques (fold) sur les listes. Exemple 4. Le modèle OPL du problème des n-reines [HPP00]. Le type intervalle (range) est utilisé pour construire le tableau queen de type variable (var) qui représente le placement des n reines sur les lignes l’échiquier (chaque reine est implicitement sur une colonne différente). int n = ...; range Domain 1..n; var Domain queen[Domain]; C’est exclusivement dans le bloc monolithique solve que les contraintes du problème sont exprimées. solve { forall(ordered i,j in Domain) { queen[i] <> queen[j]; queen[i] + i <> queen[j] + j; queen[i] - i <> queen[j] - j } }; La composante recherche est quant à elle exprimée dans le bloc search. Ici, la stratégie de branchement est un labeling (cf. Sec 4.3) avec une heuristique d’ordonnancement des variables et une heuristique d’ordonnancement de valeurs. search { forall(i in Domain ordered by increasing dsize(queen[i])) tryall(v in Domain ordered by increasing abs(n/2-v)) queen[i] = v; }; OPL et COMET 22 L’heuristique de sélection de variables applique le principe first-fail (cf. Sec 4.3) sur la liste Domain qui paramètre le quantificateur universel forall. L’heuristique ordonne les éléments i de cette liste selon l’ordre croissant des tailles de domaine des variables queen[i]. Ici, les i jouent le rôle d’index pour accéder aux reines dans le tableau queen. L’heuristique de sélection de valeurs s’applique sur la liste Domain qui paramètre l’itérateur non déterministe tryall. Elle ordonne les éléments v de cette liste par ordre croissant de distance au milieu du domaine n/2. Bien que cet exemple simple ne l’illustre pas en profondeur, la composante recherche d’OPL est une composante de programmation. Elle intègre notamment, outre le séquencement et la manipulation d’indices et de tableaux, des structures de contrôle comme les conditionnelles (if...then...else...) et les boucles « tant que » (while). Composante Contraintes extensible règles modulaire non non non Composante Recherche strat. branch. heur. ordo. parc. d’arb. oui oui non Table 3.1: Récapitulatif des traits d’OPL statique non Compilation dynamique vérif. typ. oui oui reform. non Exécution hybrid. m-solveurs non non term. non Table 3.2: Traits de la compilation et de l’exécution des modèles OPL COMET Comet [MH05] est un langage orienté objet riche qui peut être vu comme une évolution d’OPL vers l’hybridation des techniques de programmation par contraintes et de recherche locale, nommée constraint-based local search (CBLS). Comme dans OPL, les deux composantes contraintes et recherche sont présentes et dissociées. Comet propose un langage de contraintes et des constructions pour programmer des stratégies de branchement, des heuristiques d’ordonnancement (de listes), et aussi des parcours d’arbres grâce aux fermetures et aux continuations. Comet est un langage extensible, il autorise les définitions utilisateur (récursives) mais les contraintes doivent toujours être composées dans le bloc solve. En Comet, les contraintes utilisateur sont définies comme des classes qui étendent la classe UserConstraint<CP>. Le langage offre des facilités pour l’ingénierie et permet par exemple différentes entrées/sorties pour se connecter à des bases de données ou lire des fichiers XML. Comet permet aussi de construire des interfaces utilisateur pour observer graphiquement l’évolution de l’exécution des modèles compilés. CHAPITRE 3. LANGAGES DE MODÉLISATION 23 Exemple 5. Le modèle Comet du problème des n-reines est très proche du modèle OPL. On remarque l’usage de modules import cotfd; et de l’opérateur d’écriture sur la sortie standard cout. L’objet m représente un solveur qui est ici de type CP 1 c’est-à-dire un solveur suivant les principes de la PPC. Il est à noter que COMET, comme OPL, fait usage de ses propres solveurs et le compilateur ne permet pas d’exécuter les modèles sur d’autres plateformes. import cotfd; Solver<CP> m(); int n = 120; range S = 1..n; var<CP>{int} q[i in S](m,S); Integer np(m.getNPropag()); Comme dans OPL, les contraintes sont composées exclusivement dans le bloc solve. On note ici l’usage de la contrainte globale alldifferent et qu’il est possible de régler la force de propagation des contraintes avec le second argument de la méthode m.post. solve<m> { m.post(alldifferent(all(i in S) q[i] + i), onDomains); m.post(alldifferent(all(i in S) q[i] - i), onDomains); m.post(alldifferent(q), onDomains); } Enfin, la composante recherche, exprimée dans le bloc using, est similaire à celle d’OPL. Les heuristiques d’ordonnancement sont indissociables de la définition des stratégies de branchement. Les ordres heuristiques ne peuvent être définis que vis-àvis d’une liste de quantificateur, comme c’est le cas pour l’heuristique de type first-fail (q[i].getSize()) vis-à-vis des éléments de la liste i in S qui paramètre le forall dans l’exemple suivant. using { forall(i in S) by (q[i].getSize()) { tryall<m>(v in S : q[i].memberOf(v)) q[i] = v; } } cout << "Solution = " << q << endl; cout << "#choices = " << m.getNChoice() << endl; cout << "#fail = " << m.getNFail() << endl; cout << "#propag = " << m.getNPropag() - np << endl; 1. Les solveurs peuvent aussi être de type LS (pour local search) et interagir avec les solveurs CP. De bonnes solutions peuvent être trouvées rapidement par recherche locale et servir de base pour une preuve d’optimalité avec un solveur de type CP. ZINC 24 Composante Contraintes extensible règles modulaire oui non oui Composante Recherche strat. branch. heur. ordo. parc. d’arb. oui oui oui Table 3.3: Récapitulatif des traits de Comet statique non Compilation dynamique vérif. typ. oui oui reform. non Exécution hybrid. m-solveurs oui non term. non Table 3.4: Traits de la compilation et de l’exécution des modèles Comet Les programmes OPL et Comet sont compilés à la volée (Just-In-Time compilation) en code procédural interprété par des machines virtuelles dédiées efficaces et ne souffrent donc pas d’explosion en taille du code généré. De plus, la terminaison de la compilation des modèles OPL et Comet est garantie précisément car elle procède par génération de procédures sans expansion des expressions. Par contre, il n’y a pas de garantie de terminaison de l’exécution de ces programmes qui peuvent par exemple faire usage de constructions while ou de définitions récursives. Enfin, ces langages prennent le parti de programmer la recherche. Ils offrent des structures de contrôle comme les conditionnelles et les boucles « tant que » et demandent à déclarer, transformer ou manipuler les structures de données de telle sorte qu’elles soient exploitables par des heuristiques de réordonnancement de listes. 3.2 ZINC Zinc [dlBMRW06, RdlBMW07] est un langage de modélisation qui entend rendre ses modèles résolubles par le plus de solveurs possibles et donc établir un standard de fait. Le langage donne les moyens à l’utilisateur d’étendre le langage avec ses propres définitions de fonctions et prédicats qui peuvent être surchargées. Zinc permet l’inclusion de fichiers qui a pour effet d’insérer les définitions contenues à l’endroit de l’inclusion. Tous les identifiants (variables, fonctions, prédicats) ont une portée globale, ils ne sont pas préfixés par un nom de module. Par ailleurs, la terminaison de la compilation et de l’exécution des programmes générés est garantie. Zinc intègre des mécanismes de coercition comme la réification mais celle-ci doit être explicitée par la fonction bool2int/1, ce qui oblige à manipuler des constructions de bas niveau dans les définitions de contraintes. La compilation des modèles Zinc génère des programmes de contraintes « plats ». Il inclut contraintes ensemblistes et combinateurs de listes génériques. Le langage est typé explicitement et le compilateur est capable de lever des problèmes d’instanciation des modèles. Le point fort de Zinc est son indépendance vis-à-vis des différents types et implantations de solveurs, de plateformes de résolution. Un fragment du langage, nommé MiniZinc [NSB+ 07], et un langage intermédiaire « plat », nommé FlatZinc, ont été développés et permettent aujourd’hui au langage d’utiliser les solveurs GeCode [SLM], Eclipse CHAPITRE 3. LANGAGES DE MODÉLISATION 25 [AW07], ILOG solver [ILO], Minion [GJM06], Choco-Java [Lab00] et SICStus-Prolog [C+ 07]. Les programmes générés sont donc sujets à l’explosion en taille. Zinc ne possède pas les constructions dédiées à la recherche, et permet seulement d’annoter les modèles pour indiquer le type de méthode de résolution ou la stratégie de recherche à employer [RMdlB+ 08]. Exemple 6. Le modèle Zinc du problème des n-reines [RMdlB+ 08]. Comme illustré ci-dessous, Zinc offre sensiblement les mêmes types de base qu’OPL : type entier (int), type intervalle (..), type tableau (array), variable (var). int: n; type Domain = 1..n; array[Domain] of var Domain :q; Les contraintes actives du modèle sont préfixées par le mot clé constraint. On remarque la définition utilisateur du prédicat noattack, utilisé dans la contrainte active. predicate noattack(Domain: i,j, var Domain: qi,qj) = qi != qj /\ qi + i != qj + j /\ qi - i != qj - j; constraint forall(i,j in Domain where i<j) noattack(i,j,q[i],q[j]); Le modèle indique le type de recherche utilisée par l’annotation satisfy::backtrack (méthode arborescente par backtrack). solve satisfy::backtrack(q, std_label); L’annotation est paramétrée par la fonction std_label. À partir de la liste de variables Vs, std_label produit une liste de paires dont la première composante est la queue de Vs et la seconde l’affectation de la variable en tête. La liste de paires produite par std_label sert de base à un labeling sans heuristique. On peut remarquer l’usage du type générique $T pour le premier argument d’entrée et l’argument de retour de la fonction. function list of tuple(list of $T, var bool): std_label(list of $T:Vs) = if Vs = [] then [] else [ (tail(Vs), head(Vs) == d) | d in domain(head(Vs))] endif; Composante Contraintes extensible règles modulaire oui non oui Composante Recherche strat. branch. heur. ordo. parc. d’arb. non non non Table 3.5: Récapitulatif des traits de Zinc ESSENCE 26 statique oui Compilation dynamique vérif. typ. non oui reform. non Exécution hybrid. m-solveurs oui oui term. oui Table 3.6: Traits de la compilation et de l’exécution des modèles Zinc 3.3 ESSENCE Essence [FHJ+ 08] est un langage de modélisation qui se concentre sur les problématiques d’abstraction et de reformulation des modèles. Il offre une grande variété de structures de haut niveau ((multi)ensembles, fonctions, relations, partitions, etc.) et un système de type fin qui guide la compilation vers les CSPs. Le type d’une variable de décision peut être n’importe quelle (imbrication de) structure(s). Par conséquent, le langage permet de déclarer des variables dont les valeurs sont directement des objets combinatoires et de laisser les choix de modélisation et d’optimisation vers les CSPs au compilateur. La compilation des modèles Essence procède par expansion des expressions et génère des programmes « plats » destinés aux solveurs Minion [GJM06] ou Eclipse [AW07]. La taille des CSPs générés peut donc exploser malgré les différentes optimisations appliquées à la compilation. De plus, il manque des combinateurs génériques sur les structures inductives, les listes par exemple, et rien n’est prévu pour contrôler la recherche qui sera effectuée avec la stratégie par défaut du solveur utilisé. Par ailleurs, puisque que le langage n’offre ni définitions utilisateurs, ni constructions de type boucle while, la compilation et l’exécution sont garanties de se terminer. La reformulation consiste à transformer un modèle posant des contraintes arbitraires sur des structures complexes en un CSP valide pour des solveurs de contraintes. La compilation par expansion des expressions du modèle qui aplatit des contraintes avec quantificateurs sur des objets très structurés a l’inconvénient de générer du code de grande taille. Mais ce code contient un certain nombre de redondances et de sous-expressions communes. La reformulation optimise une transformation naïve en une transformation de moins grande taille avec le moins de redondances possibles et peut insérer des contraintes impliquées pour améliorer l’efficacité de la propagation. Pour limiter la taille des programmes générés, il s’agit de faire produire au compilateur un code qui contienne un maximum de sous-expressions communes puis de faire un usage intensif des techniques d’élimination des sous-expression communes (CSE) ainsi que d’optimisations de code en général (optimisation des itérateurs, analyse des invariants, etc.). Exemple 7. Le modèle Essence du problème des n-reines. Essence distingue les identifiants qui doivent être instanciés à la compilation des autres avec le mot clé given. La déclaration et l’initialisation des structures de données se fait après le mot clé letting. Dans ce modèle, la taille de l’instance n doit être connue statiquement et Index représente le domaine des variables du problème. given n : int(*1..*) letting Index be domain int(*1..n*) CHAPITRE 3. LANGAGES DE MODÉLISATION 27 Une reine est placée sur chaque ligne de l’échiquier et la variable combinatoire est ici la fonction arrangement qui représente directement le placement des reines sur les colonnes de l’échiquier. La fonction arrangement est une bijection, ce qui assure que chaque colonne contienne exactement une reine. Les contraintes du problème qui concernent les diagonales sont exprimées dans le bloc monolithique such that associé au but find. find arrangement : function Index -> (*bijective*) Index such that forall q1, q2 : Index . q1 neq q2 implies |arrangement(q1) - arrangement(q2)| neq |q1 - q2| Composante Contraintes extensible règles modulaire non non non Composante Recherche strat. branch. heur. ordo. parc. d’arb. non non non Table 3.7: Récapitulatif des traits d’Essence statique oui Compilation dynamique vérif. typ. non oui reform. oui Exécution hybrid. m-solveurs oui oui term. oui Table 3.8: Traits de la compilation et de l’exécution des modèles Essence 3.4 s-COMMA s-COMMA [Sot09] est un langage de modélisation orienté objet (simplification de Java) destiné aux programmeurs qui ne sont pas spécialistes de la PPC. Dans cet esprit, le langage offre une représentation graphique des modèles à la UML pour exprimer les problèmes. s-COMMA a pour ambition de participer à la définition d’un standard de langage de modélisation. Les structures de données du langages sont les tableaux, les énumérations, les ensembles, les booléens, et les variables à domaine fini ou variables à domaine continu. Ces dernières ne pouvant pas (encore) être des variables de décision. Le langage propose un quantificateur universel et des structures de contrôle conditionnelles. s-COMMA n’offre donc pas de combinateur générique ni de quantificateur existentiel. s-COMMA ne permet pas à l’utilisateur de définir des stratégies de branchement ni des parcours d’arbre, fixés respectivement au labeling et à DFS. Le langage propose un jeu d’options prédéfinies pour paramétrer la recherche et qui correspond à un ensemble d’heuristiques d’ordonnancement classiques. À l’instar de Zinc et d’Essence, la compilation des modèles s-COMMA procède par expansion des expressions. Le langage est compatible avec plusieurs implantations de Langages CLP et CIP 28 solveurs (Gecode, Eclipse, GNU-Prolog et RealPlayer). Il est possible d’ajouter au langage de nouvelles contraintes globales ou fonctions d’une implantation de solveur particulière grâce à des transformations de modèles qui associent des expressions s-COMMA à des expressions du solveur cible chosi. Exemple 8. Le modèle s-COMMA du problème des n-reines. La classe Queens regroupe les attributs (le tableau de reines q[n]) et les contraintes (la contrainte de non-prise noAttack) qui définissent le problème. L’ordre heuristique pour le labeling est donné par les options prédéfinies min-dom-size (plus petite cardinalité de domaine) pour la sélection des variables et med-val (valeur médiane) pour la sélection des valeurs. int n := 10; main class Queens [min-dom-size, med-val] { int q[n] in [1, n]; constraint noAttack { forall(i in 1..n) { forall(j in i+1..n) { q[i] <> q[j]; q[i] + i <> q[j] + j; q[i] - i <> q[j] - j; }} } } Composante Contraintes extensible règles modulaire oui non oui Composante Recherche strat. branch. heur. ordo. parc. d’arb. non non non Table 3.9: Récapitulatif des traits de s-COMMA statique oui Compilation dynamique vérif. typ. non oui reform. non hybrid. non Exécution m-solveurs oui term. oui Table 3.10: Traits de la compilation et de l’exécution des modèles s-COMMA 3.5 Langages CLP et CIP Les langages de programmation logique avec contraintes (PLC ou CLP) basés sur Prolog (comme SICStus-Prolog, GNU-Prolog ou Eclipse) ainsi que les langages de programmation impérative avec contraintes (PIC ou CIP), les bibliothèques de contraintes définies dans des langages impératifs (comme Choco, Gecode ou ILOG Solver) sont très CHAPITRE 3. LANGAGES DE MODÉLISATION 29 puissants et permettent notamment à l’utilisateur de définir de nouvelles contraintes de façon modulaire, et de programmer des stratégies de branchement, des heuristiques d’ordonnancement et des parcours de recherche. Mais concrètement, les langages de PLC et de PIC offrent des constructions de bas niveau, la notation qu’ils proposent n’est pas purement déclarative, ils ne dissocient pas assez clairement la modélisation des traits extra-logiques et incluent rarement des constructions logiques essentielles telles que les quantificateur universels et existentiels. Enfin, la terminaison des programmes n’est pas garantie. Les objectifs qui motivent la conception de langages de modélisation s’opposent à ces langages de programmation qui laissent implicite la modélisation induite par des constructions de programmation. Ils demandent un effort d’interprétation du code et de solides connaissances en programmation à des utilisateurs qui peuvent être ingénieurs experts d’un domaine industriel et pas nécessairement programmeurs. Composante Contraintes extensible règles modulaire oui non oui Composante Recherche strat. branch. heur. ordo. parc. d’arb. oui oui oui Table 3.11: Récapitulatif des traits des langages de PLC et PIC statique non Compilation dynamique vérif. typ. oui oui reform. non Exécution hybrid. m-solveurs oui non term. non Table 3.12: Traits de la compilation et de l’exécution des programmes PLC et PIC 3.6 Langages BRMS Le formalisme de règles métier, ou business rules, a été introduit comme un formalisme de représentation des connaissances, en particulier des connaissances spécifiques à un domaine d’application, par des règles de la forme si A alors B. En pratique, ces règles sont exploitées par des systèmes de gestion de règles métier (SGRM), ou business rules management systems (BRMS ), comme JBoss Drools [JBo] ou IBM ILOG JRules [IBM]. Dans un SGRM, les règles sont considérées comme des règles de production et exécutées par un moteur d’inférence en chaînage avant, un algorithme de type Rete [For82], et de la forme : si <Motif> <Condition> alors <Action> Une règle est déclenchée lorsqu’un terme (dans la mémoire centrale) représentant un fait est filtré par la partie <Motif> et qu’un sous-ensemble des arguments du terme respecte la partie <Condition>. L’effet de la règle est défini par la partie droite <Action> qui pourra modifier l’état de la mémoire centrale. Le moteur d’inférence s’exécute jusqu’à saturation, c’est-à-dire jusqu’à ce que plus une règle ne soit déclenchable. Les parties gauches et droites de telles règles sont exprimées dans un langage de programmation impératif tel que Java, comme c’est le cas de certains langages de PIC. Langages BRMS 30 Un inconvénient rédhibitoire des SGRM, du point de vue de la modélisation, est que l’écriture des règles dépend fortement de l’interprétation procédurale des moteurs d’inférence à la Rete qui les exécutent. Parce que cette sémantique opérationnelle et la sémantique déclarative des règles métier diffèrent notablement, et pour les mêmes raisons que les langages de PLC et de PIC, les SGRM sont inadaptés à la modélisation de problèmes d’optimisation combinatoire. Néanmoins, les SGRM peuvent trouver une application en PPC comme interface entre un système dynamique et un solveur de PPC comme proposé dans [vdKFLS10]. Conclusion Un langage de modélisation se concentre sur la tâche de formulation des problèmes combinatoires. Il est important à cet égard d’offrir des constructions de modélisation d’un niveau d’abstraction suffisant, des mécanismes de coercition implicites, notamment pour la réification, et les moyens d’étendre le langage grâce à des définitions utilisateur dans un cadre purement déclaratif. La plupart des langages de modélisation de l’état de l’art demandent à exprimer les problèmes de façon monolithique, ce qui limite la réutilisabilité des modèles et la possibilité pour l’utilisateur de définir des bibliothèques de contraintes. À l’exception d’OPL et de Comet, qui offrent une composante pour programmer la recherche, les stratégies de recherche ne peuvent être écrites dans ces langages. Par contre, OPL et Comet ne garantissent pas la terminaison de l’exécution des modèles. La stratégie de recherche adoptée lors de l’exécution des modèles des autres langages est celle qu’offre par défaut le solveur utilisé. Cela pose un réel problème d’efficacité car, hormis les problèmes académiques classiques, il est souvent indispensable de contrôler la recherche pour résoudre effectivement un problème combinatoire, a fortiori un problème industriel qui impliques de nombreuses contraintes spécifiques (des règles métiers). Chaque langage fait le choix d’une compilation par expansion des expressions ou d’une compilation par génération de code procédural qui conserve la structure des modèles, mais aucun n’a jamais présenté formellement le processus de compilation. Afin de comparer synthétiquement les différents langages de modélisation de l’état de l’art entre eux et avec Cream, la table 3.13 récapitule les traits de tous les langages plus ceux de Cream. Langage Opl Comet Zinc Essence s-Comma Cream Composante Contraintes ext. règles mod. non non non oui non oui oui non oui non non non oui non oui oui oui oui Composante Recherche branch. heur. parc. oui oui non oui oui oui non non non non non non non non non oui oui non Table 3.13: Récapitulatif des traits de tous les langages de l’état de l’art plus Cream CHAPITRE 3. LANGAGES DE MODÉLISATION Langage Opl Comet Zinc Essence s-Comma Cream stat. non non oui oui oui oui Compilation dyn. typ. oui oui oui oui non oui non oui non oui oui oui reform. non non non oui non non 31 Exécution hybrid. m-solveurs non non oui non oui oui non oui non oui non oui term. non non oui oui oui oui Table 3.14: Récapitulatif des traits de la compilation et de l’exécution de tous les langages de l’état de l’art plus Cream Nous verrons dans cette thèse que dans un langage de modélisation à base de règles comme Cream, il est possible de spécifier et de composer des connaissances par fragments plutôt qu’en un tout monolithique. Il sera aussi montré que Cream offre une composante recherche purement déclarative. Cream donne le choix entre les deux grands schémas de compilation qui seront présentés formellement. La compilation et l’exécution des modèles Cream sont garanties de se terminer. Enfin, le formalisme de règles et la modularité de Cream ont permis de construire une bibliothèque pour la modélisation des connaissances en placement qui sera présentée et utilisée pour résoudre des problèmes combinatoires issus de la logistique dans l’industrie automobile. Par ailleurs, un système de type pour Cream gérant la réification implicite a été développé par Thierry Martinez et présenté dans un article à paraître ([MMF10]). 32 Langages BRMS Chapitre 4 Procédures de recherche Sommaire 4.1 4.2 4.3 Parcours d’arbres de recherche . . . . . . . . . . . . . . . . . Stratégies de branchement . . . . . . . . . . . . . . . . . . . . Heuristiques d’ordonnancement de choix . . . . . . . . . . . 33 37 38 La propagation n’aboutit en général qu’à une valuation partielle que l’on souhaite compléter en une solution. Il est donc nécessaire, et crucial en terme d’efficacité, de définir une procédure de recherche, guidée par des heuristiques, qui explore l’espace des combinaisons de valeurs encore possibles. Nous nous intéressons essentiellement aux méthodes d’exploration arborescentes en profondeur d’abord. Une procédure de recherche décide d’une stratégie de branchement, qui induit un arbre de recherche, et d’un parcours d’arbre. Les branches de l’arbre et l’ensemble des contraintes sur chaque branche peuvent être réordonnées selon des heuristiques d’ordonnancement de choix. Les heuristiques de recherche sont des principes qui exploitent des connaissances spécifiques à un problème ou l’information localement accumulée pendant le parcours d’un arbre de recherche. Elles ont pour finalité de minimiser le nombre de noeuds explorés avant de trouver une solution ou de prouver qu’il n’en existe pas. 4.1 Parcours d’arbres de recherche L’exploration d’un espace de recherche donné décrit implicitement un arbre de recherche. En chaque noeud d’un tel arbre se présente un ensemble d’alternatives de décisions parmi lesquelles il faut faire un choix afin d’arriver au but. Dans le cadre de la PPC, le but est une réduction minimale des domaines des variables (cf. stratégies de branchement, section 4.2) qui permette de décider de la satisfaction de toutes les contraintes d’un problème donné. À chaque arête de l’arbre de recherche est associée une pose de contrainte, une affectation de variable par exemple. Il est à noter que la propagation détecte en chaque noeud les cas d’insatisfaisabilité, élaguant ainsi les branches de l’arbre de recherche avant l’énumération exhaustive. Un parcours d’arbre de recherche commence par la racine et deux décisions doivent être prises : l’axe et le sens du parcours, c’est-à-dire parcourir verticalement ou hori33 Parcours d’arbres de recherche 34 zontalement et de gauche à droite ou de droite à gauche, voire avec permutation des noeuds. Une procédure de recherche qui garantit de trouver la solution (si une solution existe) est nommée complète, incomplète sinon. Par la suite, la stratégie de branchement est fixée et nous appelons d la profondeur de l’arbre parcouru et b le facteur de branchement, le nombre maximum d’enfants d’un noeud. Recherche en largeur d’abord La recherche en largeur d’abord, breadth-first search (BFS), explore un arbre horizontalement, niveau par niveau. Tous les noeuds de l’arbre de profondeur k sont parcourus avant tous les noeuds de profondeur k + 1. La complexité en temps de BFS est en O(bd ). L’avantage de cette exploration est qu’elle est complète, même en cas de branche infinie. L’inconvénient est que la complexité en espace de BFS est exponentielle en la profondeur, en O(bd ). 0 1 4 5 2 6 7 3 8 9 Figure 4.1: BFS leftmost (à gauche d’abord). Recherche en profondeur d’abord La recherche en profondeur d’abord [Tar72], depth-first search (DFS), explore un arbre verticalement, branche par branche. À chaque noeud de l’arbre, le premier fils du noeud est parcouru récursivement, puis le second, et ainsi de suite. À chaque feuille de l’arbre, DFS rebrousse chemin, backtrack, sur le dernier noeud qui possède des fils encore inexplorés et choisit le prochain enfant. La complexité en temps de DFS est en O(bd ). Ce type de parcours a l’avantage d’être de complexité en espace linéaire, en O(d). Mais la recherche DFS n’est pas complète lorsque l’arbre considéré possède des branches infinies. 0 1 2 3 5 4 Figure 4.2: DFS à gauche d’abord. 6 7 8 9 CHAPITRE 4. PROCÉDURES DE RECHERCHE 35 Dans le contexte de la PPC, des contraintes réduisant le domaine des variables sont accumulées sur les branches. Il arrive alors que la propagation infère des domaines vides pour certaine variables avant d’arriver à une feuille, un tel échec provoque aussi un backtrack. Recherche par approfondissements successifs La recherche par approfondissements successifs [Kor85], iterative deepening depthfirst search (IDDFS), est une recherche en profondeur itérative limitée à la profondeur 1, puis à la profondeur 2, et ainsi de suite jusqu’à l’obtention d’une solution (ou l’échec de la recherche). La complexité en temps de IDDFS est en O(bd+1 ). La limitation de chaque itération à une profondeur finie rend la recherche en profondeur complète. La complexité en espace de IDDFS est linéaire, en O(d). 0 1 2 0 3 1 2 3 5 4 6 7 8 9 Figure 4.3: IDDFS à gauche d’abord, de profondeur 1 et profondeur 2. Recherche en profondeur avec écarts limités La recherche en profondeur avec écarts limités [HG95, Kor96], limited discrepancy search (LDS), est une recherche en profondeur d’abord qui explore l’arbre avec un nombre croissant d’écarts, de décisions contradictoires avec une heuristique donnée. Cette méthode échantillonne l’espace de recherche et repose sur une heuristique donnée pour explorer la fraction de l’espace qui est probable de contenir des solutions. L’intuition est qu’une bonne heuristique amène souvent à une solution, mais pas toujours. 0 0 1 1 2 2 Parcours d’arbres de recherche 36 0 0 1 1 2 2 Figure 4.4: Itérations de LDS avec l’heuristique qui ordonne les noeuds de gauche à droite, avec 1 écart (première ligne) et 2 écarts (deuxième ligne). Recherche du meilleur d’abord La recherche par meilleur d’abord [DP85, VKK91, ZK93], best-first search (BFS), est une recherche générique qui explore les noeuds dans un ordre non décroissant de coût. À chaque noeud de l’arbre, c’est le fils de plus petit coût vis-à-vis d’une fonction d’évaluation f qui est exploré d’abord, puis le deuxième meilleur, et ainsi de suite. En particulier, si la fonction de coût est f (n) = profondeur(n), alors c’est la recherche en largeur d’abord qui est appliquée. De façon générale, la fonction d’évaluation f (n) exploite les informations de description d’un noeud n, l’information collectée jusqu’en n et les connaissances spécifiques au problème considéré. Recherche de l’optimum par séparation et évaluation La recherche de solution optimale par séparation et évaluation progressive [LD60], branch and bound (B&B), est une approche arborescente pour la recherche de solution optimale, vis-à-vis d’une fonction objectif, de problèmes d’optimisation combinatoire. En théorie, il est toujours possible de trouver la solution optimale à un problème (s’il existe une solution) par une énumération explicite et exhaustive des combinaisons de valeurs possibles pour les variables. En pratique, on cherche à l’éviter car il existe potentiellement un nombre exponentiel de solutions en la taille du problème. On fait alors usage de méthodes qui garantissent l’optimalité sans explorer explicitement tout l’espace de recherche. B&B à base de contraintes [Van89] est une telle méthode adaptée à la PPC qui consiste en une (séquence de) recherche avec une borne inférieure (supérieure) de la fonction objectif progressivement (dé)croissante. Supposons, sans perte de généralité, que l’on souhaite trouver la solution à un problème de contraintes P qui minimise une fonction f (X). Opérationnellement, une première recherche sur le problème P1 = P est lancée et aboutit à une solution dont le coût c1 est évaluer par la fonction objectif. Ensuite, la contrainte f (X) < c1 est ajoutée au problème, donnant le problème P2 = P1 ∧ f (X) < c1 , pour exclure toute solution de coût supérieur à c1 . Selon le schéma adopté, soit la recherche B&B backtrack et continue sur P2 , soit la recherche est réexécutée complètement (restart) pour P2 . La recherche globale termine lorsque le problème Pi est insatisfaisable, ce qui prouve que la solution optimale est de coût ci−1 . CHAPITRE 4. PROCÉDURES DE RECHERCHE 4.2 37 Stratégies de branchement Une stratégie de branchement détermine les (alternatives de) contraintes posées par la procédure de recherche. Dans le cadre d’une recherche en profondeur d’abord, par « backtrack », chaque noeud p à un niveau j de l’arbre de recherche correspond à un ensemble de contraintes dites de branchement {c1 , . . . , cj } où chaque ci , 1 ≤ i ≤ j, est la contrainte posée au niveau i de l’arbre. Un noeud p est développé en créant k nouvelles branches p ∪ {c1j+1 }, ..., p ∪ {ckj+1 } pour de nouvelles contraintes cij+1 , 1 ≤ i ≤ k. Trois stratégies de branchement sont fréquemment utilisées : – Énumération des affectations de variables (labeling n-aire, cf. Fig. 4.5) qui développe un arbre dans lequel à chaque niveau est décidé quelle variable affecter et en chaque branchement quelle valeur choisir. Les contraintes de branchement en chaque noeud p sont donc de la forme {X1 = v1 , . . . , Xj = vj }, où vi ∈ Di , et 1 le noeud p est développé en créant n nouvelles branches p ∪ {Xj+1 = vj+1 }, . . . , n p ∪ {Xj+1 = vj+1 } pour la prochaine variable Xj+1 sélectionnée. – Choix binaires (labeling binaire) qui développe un arbre dans lequel en chaque noeud p deux nouvelles branches sont créées : p ∪ {Xj+1 = v} et p ∪ {Xj+1 6= v}, où v ∈ Dj+1 . – Découpage de domaine (domain splitting) pose des contraintes d’inégalité sur les variables. Par exemple une telle stratégie génère au noeud p, pour une variable Xi ∈ {0, . . . , 9}, les alternatives p∪{Xi < 3}, p∪{3 ≤ Xi , Xi < 6}, et p∪{6 ≤ Xi }. 1 = 2 1 = 2 X 1 X 1 1 = 2 X 2 2 2 2 X = = = X2 X2 = = 2 =4 3 X2 X2 X X1 = = 2 =1 X1 X1 Figure 4.5: Un arbre de recherche généré par une stratégie de branchement par énumération des affectations, ou labeling, sur 2 variables X1 ∈ {1, 2, 3, 4} et X2 ∈ {1, 2}. Les stratégies de branchement ne se limitent pas à la pose de contraintes sur les domaines comme c’est le cas des trois stratégies présentées ci-dessus. Il est par exemple plus opportun et plus efficace dans les problèmes d’ordonnancement disjonctifs de brancher sur des contraintes de précédence entre tâches. Comme l’exemple de la section 7.1 l’illustre, les nouvelles branches créées en chaque noeud p d’un arbre de recherche induit par de telles stratégies sont alors de la forme p∪{X1 +d1 ≤ X2 }, p∪{X2 +d2 ≥ X2 } où Xi et di représentent respectivement la date début et la durée de la tâche ti . Heuristiques d’ordonnancement de choix 38 4.3 Heuristiques d’ordonnancement de choix Une heuristique d’ordonnancement réordonne les (alternatives de) contraintes déterminées par une stratégie de branchement afin de réduire la taille de l’arbre de recherche effectivement développé. Explorer un sous-arbre sans solutions peut être extrêmement coûteux et les décisions prises aux premiers niveaux de l’arbre de recherche sont particulièrement importantes. Il existe deux grands types d’heuristiques d’ordonnancement : – les heuristiques conjonctives qui décident de l’ordre des contraintes posées le long de chaque branche, des contraintes en conjonction (l’ordre des variables à affecter dans la figure 4.6) ; – les heuristiques disjonctives qui décident de l’ordre des branches à essayer, des contraintes en disjonction (l’ordre des affectations pour chaque variable dans la figure 4.6 ou l’ordre des intervalles pour le découpage de domaine, cf. Chap. 9). 3 = X 1 = 4 1 1 X1 2 1 4 = X = = X2 = 2 = 1 1 = X X 1 X1 2 X1 X1 = 3 X2 = Figure 4.6: Un arbre de recherche généré par un labeling sur 2 variables X1 ∈ {1, 2, 3, 4} et X2 ∈ {1, 2} avec l’heuristique (conjonctive) de sélection de variables min domain et l’heuristique (disjonctive) de sélection de valeurs middle out. Les heuristiques conjonctives comme les heuristiques disjonctives permettent de réduire la taille de l’arbre exploré lorsqu’il existe une solution, mais seules les heuristiques conjonctives sont utiles lorsqu’il n’en n’existe pas. Aux deux types de choix (conjonctif ou disjonctif) correspondent deux principes heuristiques. Le principe général régissant l’ordonnancement de contraintes en conjonction, est celui de l’échec d’abord ou first-fail. Pour éviter d’explorer les sous-arbres de recherche privés de solution, il faut s’apercevoir au plus vite de l’échec et donc commencer par traiter les contraintes « difficiles », les moins faciles à satisfaire. En pratique, on choisit souvent d’abord la variable de plus petit domaine (min domain), présente dans le plus de contraintes (most-constrained, max degree), ou encore la variable de plus petit domaine et présente dans le plus de contraintes (min domain-over-degree). Au contraire, parmi les contraintes en disjonction, on choisit d’abord la plus « facile », la plus probable de mener à une solution, c’est le principe succeed first. Par exemple, on sélectionne la valeur qui a le plus grand support. On peut aussi énumérer le domaine dans le sens naturel (up), de la borne supérieure vers la borne inférieure (down), du milieu vers les bornes (middle out), ou encore des bornes vers le milieu. Par ailleurs, les heuristiques peuvent être de deux natures : CHAPITRE 4. PROCÉDURES DE RECHERCHE 39 – heuristiques statiques : l’ordre est fixé une fois pour toutes avant de commencer la recherche ; – heuristiques dynamiques : l’ordre peut varier pendant la recherche, il est recalculer en chaque noeud de l’arbre notamment en fonction des effets de la propagation. Il arrive qu’un critère d’ordre heuristique, comme le plus petit domaine par exemple, ne distingue pas deux variables. Si assez d’information est disponible, il est utile de les départager avec un second critère et l’on forme alors un ordre lexicographique. Si des heuristiques (lexicographiques) sont définissable pour un problème, les procédures de recherche particulièrement adaptées en programmation par contraintes sont les procédures depth-first best-first search (DFBFS) et limited discrepancy search (LDS). Le chapitre 7, et particulièrement son introduction, présente comment des heuristiques classiques en programmation par contraintes sont exprimées en Cream. Conclusion En PPC, la composante recherche s’attache à l’exploration de l’espace de recherche d’un problème combinatoire. En particulier, les heuristiques guident les choix effectués pendant la recherche pour réduire la taille de l’arbre de recherche exploré. C’est une préoccupation indispensable pour résoudre effectivement les problèmes combinatoires et de nombreuses stratégies efficaces ont vu le jour ces dernières décades. Cependant, dans les langages de PPC, les arbres de recherche sont le résultat de l’exécution des différentes procédures de recherche, comme le labeling par exemple, paramétrées par des heuristiques qui ne sont pas exprimées déclarativement. Nous verrons dans la partie suivante que dans un langage de modélisation à base de règles, il est possible de spécifier des arbres de recherche comme des formules logiques du modèle et de les réordonner de façon déclarative selon des heuristiques définies sur la structure de la formule, par pattern-matching sur les têtes de règles. 40 Heuristiques d’ordonnancement de choix Deuxième partie Langage de modélisation à base de règles Sommaire 5 Syntaxe et sémantique 43 5.1 Syntaxe de Cream . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43 5.2 Sémantique déclarative des modèles Cream . . . . . . . . . . . . . . . . . 48 6 Compilation 53 6.1 Compilation vers CSP par expansion des règles . . . . . . . . . . . . . . 54 6.2 Compilation vers CLP par génération de code procédural . . . . . . . . . 65 7 Heuristiques de recherche déclaratives 75 7.1 Heuristiques pour les formules conjonctives et disjonctives . . . . . . . . 76 7.2 Heuristiques pour les affectations de variables . . . . . . . . . . . . . . . 79 7.3 Heuristiques pour des problèmes de placement . . . . . . . . . . . . . . . 80 42 SOMMAIRE Chapitre 5 Syntaxe et sémantique Sommaire 5.1 5.2 5.1 Syntaxe de Cream . . . . . . . . . . . . . . . . . . . . . . . . . . Sémantique déclarative des modèles Cream . . . . . . . . . . 43 48 Syntaxe de Cream Un modèle Cream consiste essentiellement en : – un ensemble de déclarations dans lesquelles on distingue les définitions de constructeurs et de fonctions d’une part, des règles proprement dites pour la définition des prédicats d’autre part ; – un ensemble d’heuristiques de réordonnancement des formules ; – et un ensemble de formules buts, ou requêtes, pris en conjonction. Nous décrivons ici la syntaxe de Cream de façon bottom-up, c’est-à-dire en décrivant d’abord les expressions de base du langage (données par le non-terminal expr), puis les heuristiques (heuristic) et enfin les déclarations (declaration) et les modèles (model). Les structures de données élémentaires de Cream sont constituées de : – constantes entières, munies des opérateurs arithmétiques et de comparaison usuels ; – variables à domaine fini, avec indexicaux et la contrainte d’égalité en plus des opérateurs partagés avec les constantes entières ; – listes, construites par énumération, intervalles entre deux entiers ou concaténation et parcourues avec les quantificateurs et les combinateurs ; – chaînes de caractères, qui partagent les opérateurs sur les listes ; – enregistrements, dont les champs étiquetés sont utilisés pour la projection. La figure 5.1 présente la syntaxe des modèles Cream qui comprend les expressions de base, les expressions d’heuristiques, et les déclarations parmi lesquelles se trouvent les déclarations de règles. Les entiers sont représentés par le non-terminal integer qui parcourt le domaine fini D ⊆ Z, où D = Jmin_integer, max_integerK. Une variable var est un mot commençant par une majuscule ou le tiret bas. Une chaîne de caractère string est un mot entre guillemets. 43 Syntaxe de Cream 44 name call expr :: = :: = :: = | | | | | | | | | | | | | | | | | | | | | | ident | name : ident name | name(expr,...,expr) integer | var | string | error min_integer | max_integer expr op expr où op ∈ {+, -, *, /} op(expr, expr) où op ∈ {min, max, exp, log} expr rel expr où rel ∈ {=, #, =<, <, >, >=} expr logop expr où logop ∈ {and, or, implies, equiv} not expr forall(var in expr, expr) exists(var in expr, expr) domain(expr, expr, expr) domain_min(expr) | domain_max(expr) | domain_size(expr) variables(expr) | variable_name(expr) | variable_def(expr) [expr,...,expr] | [expr .. expr] | expr ++ expr length(expr) | nth(expr, expr) | pos(expr, expr) {ident: expr,..., ident: expr} | expr:ident foldl(var from expr, var in expr, expr) foldr(var from expr, var in expr, expr) map(var in expr, expr) let(var := expr, expr) minimize(expr,expr) | maximize(expr,expr) search(heuristic,expr) | constraint(expr) dynamic(expr) | static(expr) call head heuristic :: = :: = | | | ident | ident(var,...,var) conjunctive(op(expr) for head disjunctive(op(expr) for head où op ∈ {least, greatest} nil | heuristic and heuristic call declaration :: = | | | | | import(name). head = expr. head –-> expr. domain ident = { ident,...,ident }. heuristic head = heuristic ? expr. model :: = declaration ... declaration Figure 5.1: Syntaxe des modèles Cream. Un identifiant ident est un mot qui commence par une minuscule. name est un identifiant, potentiellement préfixé par d’autres identifiants, destiné à représenter des noms de modules. Une tête head, ou partie gauche de déclaration, est formée d’un ident et de variables distinctes en arguments lorsque son arité est non nulle. CHAPITRE 5. SYNTAXE ET SÉMANTIQUE 45 Opérateurs logiques Les formules sont considérées comme des expressions entières dans {0, 1}, elles sont dites réifiées. Cette coercition usuelle entre booléens et entiers apporte beaucoup d’expressivité [Van89]. La réification permet d’augmenter le niveau de discours et de parler de la valeur de vérité des contraintes. Réifier une formule ou une contrainte c, c’est poser une équivalence entre une variable B à valeur dans {0, 1} et la contrainte c, soit B ⇔ c. La sémantique est la suivante : si C est satisfaite, alors B = 1 est déduit et si C est insatisfaite, alors B = 0 est déduit ; inversement, si B = 1 alors C est posée et si B = 0 alors ¬C est posée. Variables à domaine fini Le prédicat domain/3 pose le domaine de la liste des variables libres contenues dans l’expression en premier argument. Les fonctions domain_min/1, domain_max/1, domain_size/1 donnent respectivement la borne inférieure, la borne supérieure et la cardinalité du domaine. La fonction variables/1 renvoie la liste des variables libres contenues dans une expression. La fonction variable_name/1 renvoie le nom d’une variable libre, déterminé par l’indexation (cf. section 5.2) et sa place dans le terme qui la contient. La fonction variable_def/1) renvoie la tête de déclaration instanciée qui a produit une variable libre donnée. Listes et enregistrements La construction de liste se fait par énumération, e.g. [1, 3, 4, 5, 6, 8] ; par intervalle, e.g. [1, 3 ..6, 8] ; ou par concaténation, e.g. [1]++[3 ..6]++[8]. Le non-terminal string représente des mots quelconques entre guillemets, e.g. "annotation". Comme les listes, les chaînes peuvent être concaténées, e.g. "annotation" ++"_" ++"1" donne "annotation_1". Un enregistrement est un ensemble de couples champ : valeur. L’évaluation de l’expression {column :1, row :_} :column donne la valeur du champ column, soit 1. Par ailleurs, tout enregistrement porte par défaut le champ uid de valeur un entier naturel unique. Combinateurs et liaison let Le combinateur fold(A from i, X in l, e) combine, selon l’expression e, l’accumulateur courant A, initialement i, et l’élément courant X de la liste l ; et récursivement le nouvel accumulateur avec l’élément suivant de la liste, jusqu’à ce qu’elle soit épuisée. Un foldl, fold left, ou réduction par la gauche, commence avec l’élément gauche de la liste, ou tête de la liste. Un foldr, fold right, commence avec l’élément droit, c’est-à-dire le dernier élément de la liste. Syntaxe de Cream 46 Exemple 9. Considérons la fonction reverse(l) qui inverse la liste l. Elle se définit comme foldl(A from [], X in l, [X] ++ A), où A et X sont des variables fraîches. Les itérations successives de reverse([1, 2, 3]) donnent : 1. [1]++[] 2. [2]++([1]++[]) 3. [3]++([2]++([1]++[])) soit la liste [3, 2, 1]. Ces combinateurs remplacent la récursivité dans le langage car il semble plus simple d’apprendre ce schéma de récursion contrôlé, partagé avec la programmation fonctionnelle, plutôt que la récursion explicite. De plus, une fois le schéma appris, il est univoque et facile à lire et donc à partager, ce qui n’est pas forcément le cas de code faisant usage de récursion explicite. Certaines des constructions qui précèdent sont des sucres syntaxiques : – exists(X in l, e) ≡ foldl(A from 0, X in l, A or e) – forall(X in l, e) ≡ foldl(A from 1, X in l, A and e) – map(X in l, e) ≡ foldl(A from [], X in l, A ++ [e]) – reverse(l) ≡ foldl(A from [], X in l, [X] ++ A) – foldr(A from i, X in l, e) ≡ foldl(A from i, X in reverse(l), e) – la construction let est étendue récursivement aux liaisons multiples. Pour tout n, un let de n + 1 liaisons let(X0 :=e0 ,...,Xn :=en ,e) est défini en termes du let simple et du let de n liaisons : let(X0 :=e0 ,let(X1 :=e1 ,...,Xn :=en ,e)). Arbres de recherche et heuristiques Le prédicat search/2 permet de spécifier une stratégie de branchement en interprétant les disjonctions et les quantifications existentielles d’une formule logique comme des points de choix plutôt que comme des contraintes réifiées. Le premier argument est la place de l’heuristique de recherche qui s’applique sur l’arbre du modèle décrit par la formule en deuxième argument. À l’extérieur de ce prédicat, ou sous le prédicat constraint/1, la formule en deuxième argument est une contrainte réifiée, donc à valeur dans {0, 1}. Une heuristique de type conjunctive et une heuristique disjunctive induisent respectivement un réordonnancement des termes des conjonctions et des disjonctions de l’arbre de modèle associé. L’arbre de modèle est la représentation arborescente d’une expression qui inclut notamment les appels de déclarations. Une heuristique op(e) for p(X) définit un critère d’ordre op(e) qui s’applique sur les termes unifiables avec la tête de règle p(X). À chaque terme ainsi filtré est associée une évaluation du critère e fonction de l’instanciation, et l’ensemble des termes est trié selon un ordre croissant ou décroissant donné par op. Une conjonction d’heuristiques de même type agrège les critères en un vecteur et définit un ordre lexicographique. Les termes d’une conjonction ou d’une disjonction sont réordonnés selon le premier critère du vecteur, puis le second en cas d’égalité, et ainsi de suite. CHAPITRE 5. SYNTAXE ET SÉMANTIQUE 47 Exemple 10. Considérons un modèle Cream du problème des n-reines (cf. Sec. 2.2). Le but du modèle Cream correspondant pose les contraintes et demande la recherche de solution : ? let(N = 4, B = board(N), queens_constraints(B, N) and queens_labeling(variables(B), N)). La stratégie de branchement est définie par la formule d’affectations de variables (cf. Sec. 4.3) exprimée dans la déclaration queens_labeling/2 : queens_labeling(Vars, N) --> search(h(N), forall(Var in Vars, queens_labeling_var(Var, N))). queens_labeling_var(Var) --> exists(Val in [1 .. N], queens_labeling_val(Var, Val)). queens_labeling_val(Var, Val) --> Var = Val. Sur cette formule du modèle, l’heuristique disjonctive middle out (cf. Sec. 4.3) est appliquée. L’heuristique h(N) filtre les règles de la forme queens_labeling_val(Var, Val) dans la sous formule disjonctive et la réordonne en plaçant avant les autres les règles qui minimisent abs(N/2 -Val) : heuristic h(N) = disjunctive(least(abs(N/2 - Val)) for queens_labeling_val(Var, Val)). Le code intermédiaire produit par −−hstci−→ est le code non-déterministe suivant qui aura pour effet d’essayer, pour chaque variable, les valeurs du milieu vers les bornes du domaine : search(Q_1_1 Q_2_1 Q_3_1 Q_4_1 = = = = 2 2 2 2 or or or or Q_1_1 Q_2_1 Q_3_1 Q_4_1 = = = = 3 3 3 3 or or or or Q_1_1 Q_2_1 Q_3_1 Q_4_1 = = = = 1 1 1 1 or or or or Q_1_1 Q_2_1 Q_3_1 Q_4_1 = = = = 4 and 4 and 4 and 4) Mode statique et mode dynamique Par défaut, ou dans la portée du prédicat static/1, une expression est développée statiquement par la compilation (cf. section 6.1). Dans la portée du prédicat dynamic/1, elle est évaluée dynamiquement (cf. section 6.2). Sémantique déclarative des modèles Cream 48 Modèles et déclarations Un modèle Cream consiste en une suite de déclarations. Une déclaration est soit un import de module, une déclaration d’objet (constructeur et fonctions), une déclaration de règle (prédicats), une déclaration de domaine énuméré, une spécification d’heuristique ou simplement un but (requête). Les déclarations récursives et les déclarations multiples d’un même symbole de tête sont interdites. Définition 16 (Variables libres). On définit l’ensemble fv(e) des variables libres d’une expression e par récurrence : fv(X) fv(e op e′ ) fv(e rel e′ ) fv(e logop e′ ) fv(let(X := e′ , e)) fv(foldl(A from i, X in l, e)) fv(f (X1 ,...,Xn ) = e.) fv(p(X1 ,...,Xn ) –-> e.) = = = = = = = = {X} fv(e) ∪ fv(e′ ) fv(e) ∪ fv(e′ ) fv(e) ∪ fv(e′ ) fv(e) \ {X} fv(e) \ {A, X} fv(e) \ {X1 , . . . , Xn } fv(e) \ {X1 , . . . , Xn } Les variables libres sont interdites dans les déclarations de règles. Dans les déclarations d’objets et de buts, elles dénotent des variables à domaine fini, les inconnues d’un problème, et sont indexées sur la tête de déclaration. Intuitivement, on entend par indexation d’une variable V sur une tête de déclaration f (X1 ,...,Xn ) le nommage de V en fonction de l’appel f (a1 ,...,an ), cf. Sec. 5.2. Exemple 11. Par exemple, la déclaration suivante introduit une variable à domaine fini dans le champ row pour chaque valeur distincte de I : queen(I) = {column :I, row :_}. La projection d’enregistrement queen(3):row permet d’obtenir cette variable qui sera nommée Queen(3,1) car la variable _ est introduite par l’appel queen(3) et c’est la première à apparaître dans l’ordre syntaxique du corps de la déclaration. Afin d’éviter la collision de noms entre têtes de déclarations, le langage inclut un système de module simple qui préfixe les identifiants avec un nom de module, de façon similaire à [HF06]. 5.2 Sémantique déclarative des modèles Cream Nous décrivons ici la sémantique des succès de Cream qui caractérise les solutions d’un modèle M. Une solution est une affectation de toutes les variables libres de M qui satisfait toutes les contraintes de M. Les variables libres vivent dans les déclarations d’objets et de buts. À chaque nouvelle instance d’un objet, un ensemble distinct de variables libres nommées selon une indexation sur la tête de déclaration instanciée. CHAPITRE 5. SYNTAXE ET SÉMANTIQUE 49 Soit O(M) l’ensemble des déclarations d’objets de M, R(M) l’ensemble des déclarations de règles, et G(M) l’ensemble des buts ^ de M. Les buts sont considérés en g. conjonction : le but associé à M est g(M) = g ∈ G(M) Exemple 12. Soit le modèle Cream du problème des n-reines (cf. Sec. 2.2) suivant, avec à gauche les déclarations d’objets et le but et à droite les déclarations de règles : q(I) = {row : _, column : I}. board(N) = map(I in [1 .. N], q(I)). ? let(N := 4, B := board(N), queens_constraints(B, N). safe(L) --> forall(Q in L, forall(R in L, let(I := Q:column, J := R:column, I < J implies Q:row # R:row and Q:row # J - I + R:row and Q:row # I - J + R:row))). queens_constraints(B, N) --> domain(B, 1, N) and safe(B). Soit Q_1_1, Q_2_1, Q_3_1,Q_4_1 les variables issues respectivement des appels q(1), et représentant les lignes des différentes reines sur l’échiquier. q(2), q(3), q(4) L’ensemble des solutions au problème des 4-reines, c’est-à-dire des affectations de variables qui satisfont toutes les contraintes, est le suivant : {(Q_1_1 = 2, Q_2_1 = 4, Q_3_1 = 1, Q_4_1 = 3), (Q_1_1 = 3, Q_2_1 = 1, Q_3_1 = 4, Q_4_1 = 2)} Indexation L’indexation d’une variable libre V sur la tête de la déclaration f (X1 , . . . , Xn ) qui la contient permet de produire un nom unique par θ en fonction de l’appel f (a1 , . . . , an ) des termes Cream TCream , de sorte que la variable indexée soit la même pour deux appels identiques. Si plusieurs variables vivent dans le corps d’une déclaration, elles sont différenciées par leur ordre d’apparition syntaxique. θ : TCream → (V → V) f (X) 7→ (Y 7→ Z) où Z est fraîche vis-à-vis de X ∪ {Y }, TCream représente les termes Cream et V les variables. Afin d’assurer que l’indexation est réalisable, les arguments d’un appel à une déclaration d’objet sont restreints à être de la forme suivante : indexable :: = integer | [indexable,...,indexable] | {ident: expr,...,ident: expr}uid index :: = constant(integer) | [index,...,index] | uid(uid) Sont indexables les entiers, les enregistrements et les listes d’indexables. Sémantique déclarative des modèles Cream 50 Chaque valeur indexable v définit un index id(v). id :     indexable i ∈ integer [i1 ,...,in ] {ident: expr,...,ident: expr}uid → 7→ 7 → 7→ index constant(i) [id(i1 ),...,id(in )] uid(uid) Affectation Soit fv(e) l’ensemble de variables libres dans une expression e. Une affectation pour M est un couple ν = (ν G , ν O ), où : – ν G est la famille d’affectations fv(g(M)) → D – ν O est une famille d’affectations qui associe à tous les objets o = f (X1 ,...,Xn ) = e. ∈ O(M) et n-uplets (i1 , . . . , in ) ∈ index n , une affectation νfO(i1 ,...,in ) : fv(o) → D Tout affectation ν : var → D est étendue homomorphiquement sur la structure des termes Cream à la fonction ν̃ : expr → expr. Sémantique des succès Soit → la définition de la sémantique des succès de Cream. Nous décrivons ici comment → réduit les expressions du langage noyau de Cream selon une affectation de toutes les variables d’un modèle M par ν G et ν O . La réduction d’un but est une valeur booléenne donnée par la réduction de son corps modulo l’affectation des variables libres. La réduction des appels à déclarations de règles et d’objets est le résultat de la réduction de leur corps, modulo la substitution des paramètres formels et l’affectation des variables libres. Les affectations des variables libres dans les déclarations d’objets et dans les buts sont opérées par ν̃ O , paramétrée par l’indexation de la tête instanciée, et par ν̃ G , respectivement. e → ν̃ G (e) si ? e. ∈ G(M) f (e1 ,...,en ) → ν̃fO(id(e1 ),...,id(en )) (e)[X1 := e1 , . . . , Xn := en ] si f (X1 ,...,Xn ) = e. ∈ O(M) et (e1 , . . . , en ) ∈ indexable n p(e1 ,...,en ) → e[X1 := e1 , . . . , Xn := en ] si p(X1 ,...,Xn ) –-> e. ∈ R(M) et V (e) ⊆ {X1 , . . . , Xn } CHAPITRE 5. SYNTAXE ET SÉMANTIQUE 51 La réduction d’une expression arithmétique est le résultat de son évaluation. La réduction d’une comparaison ou d’une formule logique est la réification du résultat de son évaluation ; le vrai (⊤) est interprété par 1 et le faux (⊥) par 0. Soit δ l’opérateur de réification : δ(⊤) = 1 et δ(⊥) = 0. n ∈ N op n′ ∈ N → n op n′ n ∈ N rel n′ ∈ N → δ(n rel n′ ) n ∈ {0, 1} logop n′ ∈ {0, 1} → δ(n = 1 logop n′ = 1) not n ∈ {0, 1} → δ(n = 0) domain([n1 ,...,nk ], l ∈ N, u ∈ N) → ∀i∈{1,...,k} , δ(δ(ni ≥ l) = 1 ∧ δ(ni ≤ u) = 1) [n ∈ N .. n′ ∈ N] →  [n, [] n + 1,...,n′ ] si n ≤ n′ sinon [e1 ,...,en ] ++ [e′1 ,...,e′n ] → [e1 ,...,en ,e′1 ,...,e′n ] length([e1 , ..., en ]) → n nth(i ∈ {1, . . . , n}, [e1 , ..., en ]) → ei {f1 : e1 ,...,fn : en }:fi → ei let(X := v, e) → e[X := v] foldl(A from i,X in [e1 ,...,en ],e) → i e e1 e · · · e en où u e v = e[A := u, X := v] Une expression g participe à la caractérisation de l’ensemble des solutions d’un modèle M. Quand g est une formule, l’interpréter comme un arbre de recherche n’altère pas l’ensemble des solutions de M. Quand g est une expression quelconque, la développer complètement statiquement ou la compiler en du code procédural dynamique n’altère pas l’ensemble des solutions de M. minimize(g, c) maximize(g, c) search(h, g) constraint(g) static(g) dynamic(g)           →g          Une solution d’un modèle M est une affectation ν = (ν G , ν O ) pour laquelle le but de M est réduit vers 1 par la réduction → associée à la sémantique succès. Définition 17. Soit M un modèle Cream. La sémantique succès Ss (m) de M est l’ensemble des solutions de M : ∗ Ss (M) = {(ν G , ν O ) | ν G (q(M)) → 1} Sémantique déclarative des modèles Cream 52 Exemple 13. Reprenons le modèle Cream du problème des n-reines de l’exemple 12 et vérifions que la réduction du modèle par →, selon une affectation solution au problème, donne bien 1. Soit νqueens4 une affectation qui correspond à une solution au problème des 4-reines. Les seules variables libres du modèle sont introduites par la déclaration q(I). Considérons O alors uniquement les affectations de la famille νqueens qui concernent q(I) : 4 O ν̃q(constant(1)) O ν̃q(constant(2)) O ν̃q(constant(3)) O ν̃q(constant(4)) : : : : Q_1_1 Q_2_1 Q_3_1 Q_4_1 7→ 7→ 7→ 7→ 2 4 1 3 La réduction par → pour cette affectation donne : ∗ board(4) → [{row:2, column:1}, {row:4, column:2}, domain(board(4), 1, 4) → 1 ∗ → 1 ∗ → δ(1 = 1 ∧ 1 = 1) {row:1, column:3}, {row:3, column:4}] safe(board(4)) queens_constraints(board(4), 4) ∗ ∗ Et ainsi let(N :=4, B =board(N), queens_constraints(B, N)) → 1. G O L’affectation νqueens4 = (νqueens , νqueens ) est bien une solution au modèle des 4-reines. 4 4 Chapitre 6 Compilation Sommaire 6.1 Compilation vers CSP par expansion des règles . . . . . . 6.1.1 Transformation en code déterministe . . . . . . . . . . . . . 6.1.2 Transformation en code non déterministe . . . . . . . . . . 6.1.3 Confluence, terminaison, complexité et correction . . . . . . 6.2 Compilation vers CLP par génération de code procédural 6.2.1 Transformation en code déterministe . . . . . . . . . . . . . 6.2.2 Transformation en code non déterministe . . . . . . . . . . 6.2.3 Complexité . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54 54 59 61 65 65 66 70 Un modèle Cream se compile en un programme d’optimisation ou de satisfaction de contraintes sur domaines finis, avec contraintes réifiées et contraintes globales. Le schéma de compilation fondamental, dit statique, est formalisé par un système de réécriture de termes qui procède par expansion des règles des modèles. Ce schéma produit des CSPs, du code efficace « plat », mais il arrive que malgré l’évaluation partielle, une instance de problème soit de trop grande taille pour être complètement développée. De plus, il impose l’instanciation des structures sur lesquelles les itérateurs déroulent les expressions du modèle. Pour prendre en charge ces problèmes de grande taille ou dynamiques, un schéma de compilation par génération de code procédural est également proposé. Ce second schéma produit des programmes CLP incluant des définitions (récursives) du langage cible. La confluence, la correction vis-à-vis de la sémantique, et des bornes de complexité sur la taille des programmes générés sont prouvées pour ces transformations. La compilation d’un modèle consiste en deux phases : une phase d’expansion du but qui, des termes Cream TCream , produit un terme du langage intermédiaire TCreamCore ; et une phase de génération de code qui, des termes TCreamCore produit soit un but principal SICStus-Prolog [C+ 07] dans les termes TSICStus-Prolog , soit un but Choco-Java dans les termes TChoco-Java . Potentiellement, le terme du langage intermédiaire peut contenir des appels et être alors accompagné d’un ensemble de déclarations qui sont transformées par exemple en définitions de clauses SICStus-Prolog. TCream −−hstci−→ TCreamCore −−hstc ; cgi−→ TSICStus-Prolog + TJava-Choco 53 Compilation vers CSP par expansion des règles 54 Plus précisément, deux transformations peuvent intervenir dans la compilation : – la transformation par expansion de règles −−hstci−→, qui développe complètement les déclarations, produit un programme de contraintes « plat » qui se comprend comme un problème de satisfaction de contraintes classique (section 6.1). – la seconde transformation par génération de code procédural −−hdyni−→, qui se retient de développer les déclarations mais produit plutôt des appels et des traductions de celles-ci, c’est-à-dire un programme logique avec contraintes (section 6.2). Nous ne décrivons pas la génération de code −−hstc ; cgi−→ qui consiste à traduire le code intermédiaire de syntaxe fonctionnelle en les idiomes SICStus-Prolog ou Java-Choco et qui n’introduit pas de difficulté particulière. Avant d’appliquer les transformations, les sucres syntaxiques sont traduits en code intermédiaire. Une erreur de compilation (error) est produite pour toute expression qui ne peut être réécrite. Les contraintes globales prédéfinies sont traduites par des règles de réécriture spécifiques et ne sont pas décrites ici. 6.1 Compilation vers CSP par expansion des règles Le schéma de compilation associé au mode statique [FM09], dit par expansion de règles, est défini par deux transformations qui produisent du code intermédiaire : −−hstci−→ développe ou réduit complètement un but en code déterministe qui pose les contraintes stc et passe le relais pour les sous-expressions en mode recherche statique à −−h srch i−→ qui développe et réordonne les formules concernées en code non déterministe. Les règles de compilation du réordonnancement des formules ne sont pas présentées pour ce schéma, pour une présentation voir la section 6.2. Dans ce schéma de compilation, les expressions closes sont simplifiées implicitement par un mécanisme d’évaluation partielle, de façon similaire à Zinc [dlBMRW06] et à Essence [FHJ+ 08]. Mécanisme qui permet de limiter la taille du code généré et un surcoût potentiel à la compilation dû aux structures de données manipulées. Le code intermédiaire suit la syntaxe des modèles Cream, et se trouve enrichie notamment d’un opérateur de réification. 6.1.1 Transformation en code déterministe Commençons la revue des règles de réécriture de la compilation par les expressions de base du langage pour finir avec les déclarations. Opérateurs arithmétiques L’évaluation partielle s’applique notamment sur la transformation des expressions arithmétiques. La réification plonge les booléens produits par les comparaisons dans les entiers. arithmétique e1 −−hstci−→ e′1 e2 −−hstci−→ e′2 e1 op e2 −−hstci−→ e′1 op e′2 comparaisons e1 −−hstci−→ e′1 e2 −−hstci−→ e′2 e1 rel e2 −−hstci−→ reify(e′1 rel e′2 ) 6.1.1 - Transformation en code déterministe 55 Opérateurs logiques L’évaluation partielle et la réification interviennent aussi dans la transformation des expressions logiques. L’élimination des négations dans les formules en les descendant vers les contraintes fait partie de la transformation mais n’est pas présentée. opérateurs binaires e1 −−hstci−→ e′1 e2 −−hstci−→ e′2 e1 logop e2 −−hstci−→ reify(e′1 = 1 logop e′2 = 1) négation e −−hstci−→ e′ not e −−hstci−→ reify(e′ = 0) Variables à domaine fini Les variables à domaine fini sont des structures de données qui portent une place pour une valeur entière et une place pour un nom formé de la tête de déclaration instanciée qui l’a produite et la position de la variable dans le corps de déclaration. La fonction prédéfinie variables(e) donne l’ensemble des variables libres de e après réduction, donc des variables à domaine fini. ensemble des variables e −−hstci−→ e′ fv(e′ ) = {V1 , . . . , Vn } variables(e) −−hstci−→ [V1 ,...,Vn ] variable_name(e) dénote un entier représentant le nom de la variable V en laquelle se réduit e. La fonction η : V → N associe à chaque variable libre portant son nom un entier unique. variable_def(e) dénote la tête de déclaration instanciée qui a produit la variable V en laquelle se réduit e. La fonction δ : V → TCream extrait du nom d’une variable libre la tête de déclaration qui l’a produite. nom d’une variable source d’une variable e −−hstci−→ V variable_name(e) −−hstci−→ η(V ) e −−hstci−→ V variable_def(e) −−hstci−→ δ(V ) Le prédicat domain/3 pose le domaine de toutes les variables contenues dans le terme résultat de la réduction d’une expression e quelconque. Compilation vers CSP par expansion des règles 56 pose de domaine variables(e) −−hstci−→ [V1 , ..., Vn ] ( e1 −−hstci−→ l e2 −−hstci−→ u l, u ∈ N domain(e, e1 , e2 ) −−hstci−→ domain([V1 , ..., Vn ], l, u) l ≤ u bornes et cardinalité   p ∈ {domain_min, e −−hstci−→ V domain_max,  p(e) −−hstci−→ p(V )  domain_size} Listes Les listes peuvent être construites par énumération, par intervalle et par concaténation. Pour une construction par intervalle, les bornes doivent être instanciées statiquement (au moment de la compilation), et l’intervalle est alors réduit en la liste d’entiers appartenant à l’intervalle au sens large. enumération e1 −−hstci−→ e′1 ... en −−hstci−→ e′n [e1 ,...,en ] −−hstci−→ [e′1 ,...,e′n ] intervalle e1 −−hstci−→ l ... en −−hstci−→ u [e1 .. e2 ] −−hstci−→ [l, l + 1,..., u] ( l, u ∈ N l≤u concaténation l1 −−hstci−→ [d1 , . . . , dn ] l2 −−hstci−→ [e1 , . . . , em ] l1 ++ l2 −−hstci−→ [d1 ,...,dn , e1 ,...,en ] Le prédicat prédéfini pos(e, l), dual de nth(e, l), renvoit l’indice de la première occurence de e′ dans [e1 ,...,en ]. longueur élément à l’index l −−hstci−→ [e1 ,...,en ] length(l) −−hstci−→ n e −−hstci−→ i l −−hstci−→ [e1 ,...,en ] i∈N nth(e, l) −−hstci−→ ei index d’un élément e −−hstci−→ e′ l −−hstci−→ [e1 ,...,en ] k = min {i | ei = e′ } i∈{1,...,n} pos(e, l) −−hstci−→ k 6.1.1 - Transformation en code déterministe 57 Enregistrements A l’instar des opérations sur les listes, une projection nécessite que l’enregistrement concerné soit instancié statiquement. construction e1 −−hstci−→ e′1 ... en −−hstci−→ e′n {f1 : e1 ,...,fn : en } −−hstci−→ {f1 : e′1 ,...,fn : e′n } projection ei −−hstci−→ e′i fi ∈ {f1 , . . . , fn } {f1 : e1 ,...,fn : en }:fi −−hstci−→ e′i Combinateurs et liaison Les combinateurs, tous réécrits en foldl, réduisent complètement les listes qu’ils prennent en argument selon l’expression de leur dernier argument e, à la condition que les listes soient instanciées statiquement. réduction par la gauche i −−hstci−→ i0 l −−hstci−→ [e1 , . . . , en ] i0 e e1 −−hstci−→ i1 i1 e e2 −−hstci−→ i2 ... in−1 e en −−hstci−→ in foldl(A from i, X in l, e) −−hstci−→ in où u e v = e[A := u, X := v] Les substitutions induites par la liaison de variables en général, et les constructions let en particulier, sont effectué modulo alpha-conversion. liaison v −−hstci−→ v ′ let(X := v, e) −−hstci−→ e[X := v ′ ] Exemple 14. À l’issu du renommage, let(X:=1, exists(X in [5,3,X], X+X=2)) donne l’expression let(X:=1, foldl(A from 0, Y in [5,3,X], A or Y+Y=2)), où Y et A sont des variables fraîches, est évaluée via réification à 1, c’est-à-dire à vrai. Mode statique et mode dynamique Il est possible d’évaluer une expression dynamiquement plutôt que statiquement avec le prédicat dynamic/1 qui marque l’application du schéma de compilation dit par génération de code procédural (section 6.2) aux expressions dans sa portée. e −−hdyni−→ e′ dynamic(e) −−hstci−→ e′ e −−hstci−→ e′ static(e) −−hstci−→ e′ Compilation vers CSP par expansion des règles 58 Mode contrainte et mode recherche Par défaut, une formule Cream f définit une contrainte réifiée. Dans un prédicat search(f ), f définit une stratégie de branchement. recherche stc nil ⊢ f −−h srch i−→ f ′ search(f ) −−hstci−→ f ′ contrainte f −−hstci−→ f ′ constraint(f ) −−hstci−→ f ′ Un prédicat minimize(f , c) minimise la valeur de la variable à domaine fini V dénoté par c selon la méthode de recherche arborescente par séparation et évaluation. f est une formule implicitement interprétée comme une stratégie de branchement qui contraint V à une affectation. Opérationnellement, l’arbre induit par f est itérativement évalué en resserrant progressivement la borne supérieure de c jusqu’à la preuve d’optimalité. optimisation stc search(e) −−h srch i−→ e′ c−−hstci−→V ( p(e, c) −−hstci−→ p(e′ , V ) p ∈ {minimize, maximize} Déclarations et appels On considère un modèle Cream M. Soit D(M) = R(M) ∪ O(M), E(M) l’ensemble des déclarations de domaines énuméré. Une variable libre Cream dénote une variable à domaine fini ; c’est une structure de donnée qui porte une place pour sa valeur et un nom. Soit V l’ensemble des symboles de variable des termes Cream. La fonction θ (cf. Sec. 5.2) génère une fonction de renommage qui donne un nom unique à chacune des variables libres pour chaque déclaration d’objet f (X1 ,...,Xn ) = e selon l’appel f (x). La compilation commence par l’expansion de la conjonction des buts de M. but g1 and . . . and gn −−hstci−→ g ′ θ(goal)    ? g1 . ∈ G(M) ...   ? g . ∈ G(M) n Le but peut contenir des appels à des déclarations de règles ou d’objets. Les déclarations de règles ne doivent pas contenir de variables libres. 6.1.2 - Transformation en code non déterministe 59 règle a1 −−hstci−→ a′1 ... an −−hstci−→ a′n ( ′ ′ e[X1 := a1 , . . . , Xn := an ] −−hstci−→ e′ r = p(X1 ,...,Xn ) –-> e. ∈ R(M) ′ p(a1 ,...,an ) −−hstci−→ e fv(r) = ∅ Les déclarations d’objets renomment les variables libres en fonction de l’instanciation de la tête f (a1 ,...,an ) et de leur ordre d’apparition dans e par θ, après substitution des paramètres formels. déclaration a1 −−hstci−→ a′1 ... an −−hstci−→ a′n ′ ′ e[X1 := a1 , . . . , Xn := an ] −−hstci−→ e′ f (X1 ,...,Xn ) = e. ∈ O(M) f (a1 ,...,an ) −−hstci−→ e′ θ(f (a′1 , . . . , a′n )) Exemple 15. La déclaration box(L)={size:L, origin:[_,_,_]} introduit trois variables à domaine fini dans le champs origin, différentes pour chaque valeur distincte de L. La projection d’enregistrement box([2,3,2]):origin donne la liste des trois variables suivante : [Box_1(box([2,3,2]),1) , Box_2(box([2,3,2]),2) , Box_3(box([2,3,2]),3) ]. Dans une déclaration de domaine énuméré, chaque nom ei est associé à l’entier naturel i, unique dans la déclaration. domaine énuméré d −−hstci−→ [1, 2, . . . , n] valeur e −−hstci−→ e′ e −−hstci−→ k ( domain d = {e1 , . . . , en } ∈ E(M) fv({e1 , . . . , en }) = ∅    domain d = {e1 , . . . , en } ∈ E(M) e = e′ , k ∈ {1, . . . , n} k   fv({e , . . . , e }) = ∅ 1 n Exemple 16. Étant données les déclarations domain rvb ={rouge, vert, bleu} et jaune ={comp :[rouge, vert]}, on a nth(1, jaune:comp) −−hstci−→ 1. Les symboles déclarés dans les déclarations de domaine énuméré vivent dans le même monde que les symboles de déclarations de règles et de déclarations. Ainsi, sachant que les déclarations multiples sont interdites, si la déclaration précédente est présente dans un modèle, aucune règle ou déclaration ne peut être de la forme f = e, f ∈ {rouge, vert, bleu}. 6.1.2 Transformation en code non déterministe stc Par la transformation −−h srch i−→, l’opérateur de conjonction and devient un opérateur de séquence ; et l’opérateur de disjonction or devient un opérateur de choix non 60 Compilation vers CSP par expansion des règles stc déterministe. Et −−h srch i−→ repose sur −−hstci−→ pour toutes les autres expressions. La formule f est développée et ladite altération des opérateurs est appliquée. stc nil ⊢ f −−h srch i−→ f ′ 6.1.3 - Confluence, terminaison, complexité et correction 6.1.3 61 Confluence, terminaison, complexité et correction Confluence de la compilation statique L’interdiction de déclarations multiples et la restriction aux têtes linéaires, c’est-àdire qui ne contiennent en arguments que des variables distinctes, permettent de montrer la confluence de la compilation [FM09]. Autrement dit, quelque soit l’ordre d’application des règles de réécriture, quelque soit la stratégie de réécriture, elles généreront le même programme de contraintes pour un même modèle d’entrée. Proposition 2. Pour tout modèle Cream, le système de réécriture de termes associé à la compilation −−hstci−→ est confluent. Preuve 1. Montrons que le système de réécriture de termes −−hstci−→ est orthogonal, i.e. linéaire à gauche et non recouvrant, ce qui implique la confluence du système [Ros73]. D’abord, les têtes de règles de réécriture de −−hstci−→ associées aux déclarations sont formées à partir d’un symbole et de variables distinctes comme arguments, d’où la linéarité à gauche de ces règles. Par ailleurs, les déclarations multiples sont interdites, et le renommage des variable par indexation sur la tête de déclaration est déterministe, d’où le non-recouvrement de ces règles. Le système de réécriture des déclarations est donc orthogonal. Ensuite, par définition, les autres règles −−hstci−→ associées aux prédicats prédéfinis et aux primitives ne se recouvrent pas. Elles réécrivent toutes un symbole différent et sont linéaires à gauche. Le système de réécriture des prédicats prédéfinis est orthogonal. Donc le système de réécriture de termes −−hstci−→ est orthogonal.  Précisons que cette preuve ne fait pas l’hypothèse de la terminaison 1 . La propriété de confluence des règles de compilation −−hstci−→ serait donc valide si les déclarations récursives étaient autorisées. Contrairement à Zinc, Cream exclut cette possibilité pour assurer la terminaison de la compilation statique, une propriété de sécurité confortable du point de vue d’un non-programmeur. Terminaison de la compilation statique Supposons, sans perte de généralité, que les modèles Cream ne contiennent qu’un seul but défini par une règle nommée solve. Définition 18. Étant donné un modèle M, soit ρ(s) le rang de déclaration d’un symbole s défini inductivement par : – ρ(s) = 0 si s n’est pas le symbole de tête d’une déclaration ou d’une règle de M, – ρ(s) = n + 1 si s est le symbole de tête d’une déclaration ou d’une règle de M, et n est le plus grand rang de déclaration des symboles de la partie droite de la déclaration. Le rang de déclaration de M est le plus grand rang de déclaration des symboles de M. 1. Quand l’hypothèse de terminaison est faite, la condition de non-recouvrement, ou plus généralement la confluence des paires critiques [Ter03, HF07], suffit à prouver la confluence sans la condition de linéarité. 62 Compilation vers CSP par expansion des règles Proposition 3. Pour tout modèle Cream, le système de réécriture de termes −−hstci−→ est noethérien. Preuve 2. Montrons que −−hstci−→ termine en montrant que la réécriture des termes prédéfinis et la réécriture des déclarations de règles et d’objets terminent. Par définition, les règles de −−hstci−→ qui concernent les prédicats et fonctions prédéfinis et les primitives n’accroissent pas les rangs de déclaration et terminent. Considérons les règles de réécriture de −−hstci−→ associées aux règles et aux déclarations. Un système de réécriture → termine s’il existe un ordre de simplification ≻RPO tel que pour toute règle r → l, r ≻RPO l [Der79]. Montrons donc que pour toute réduction t −−hstci−→ t′ on a t ≻RPO t′ . Soit ≻ l’ordre sur les symboles de déclaration : s ≻ t ⇐⇒ ρ(s) > ρ(t). Soit ≻RPO l’ordre de simplification sur les termes induit par ≻. Soit la réduction t −−hstci−→ t′ et la déclaration f (X1 ,...,Xn ) = u. ∈ O(M), tels que t = f (a1 , . . . , an ) et t′ = u[a1 , . . . , an ]. Soit g le symbole d’un sous-terme s de u et si les sous-termes de s. Par définition, f ≻ g et ≻RPO est monotone vis-à-vis du contexte, et stable vis-à-vis de la substitution [Hof92], donc ∀i ∈ {1, . . . , n} : t ≻RPO si . Alors, pour tout symbole de sous-terme g de u, f ≻ g =⇒ f (X1 , . . . , Xn ) ≻RPO u, donc t ≻RPO t′ . Donc le système de réécriture −−hstci−→ termine.  Complexité de la compilation statique Une borne de complexité sur la taille des programmes générés peut être obtenue : Définition 19. Étant donné un modèle Cream M, le rang de combinateur α(s) d’un symbole s est défini inductivement par : – α(s) = 0 si s n’est pas le symbole de tête d’une déclaration ou d’une règle dans M, – α(s) = max{n + α(s′ ) | s(x1 , . . . , xn ) –> r ∈ D(m), r contient une imbrication de n combinateurs sur une expression contenant le symbole s′ }. Le rang de combinateur de M est le plus grand rang de combinateur des symboles de M. Proposition 4. Pour tout modèle Cream M, la taille du programme généré par −−hstci−→ est en O(la · br ), où l est la plus grande longueur de liste dans M (ou au moins 1), a est le rang de combinateur de M, b est la plus grande taille de partie droite des règles et déclarations de M, et r est le rang de déclaration de M. Preuve 3. La preuve se fait par induction sur a. Dans le cas de base, a = 0, il n’existe pas de combinateur dans M, et la taille du programme généré est borné linéairement par r duplications de corps ou partie droite de règle, i.e. est en O(br ). Dans le cas inductif, a > 0, considérons d’abord la taille du programme généré sans réécrire des occurences superficielles (ou ultrapériphériques) des combinateurs. Par induction, la taille est en O(la−1 · br ). Maintenant, ce programme généré peut être dupliqué au plus l fois par les combinateurs superficiels, d’où la taille en O(la · br ) selon cette stratégie. Par la propriété de confluence 2, le programme généré est indépendant de la stratégie de réécriture. La taille du programme généré par −−hstci−→ est donc en O(la · br ) quelque soit la stratégie.  6.1.3 - Confluence, terminaison, complexité et correction 63 Exemple 17. Soient le modèle Cream du problème des n-reines (à gauche) et le code généré (à droite) par la compilation −−hstci−→ : q(I) = {row = _, column = I}. board(N) = map(I in [1 .. N], q(I)). domain([Q_1_1,Q_2_1,Q_3_1,Q_4_1], 1, 4) and safe(L) --> all_different(L) and forall(Q in L, forall(R in L, let(I:=Q:column, J:=R:column, I < J implies Q:row # J - I + R:row and Q:row # I - J + R:row))). queens_constraints(B, N) --> domain(B, 1, N) and safe(B). heuristic h(N) = disjunctive(least(abs(N/2 - Val)) for queens_labeling_val(Var, Val)). ? let(N := 4, B := board(N), queens_constraints(B, N) and queens_labeling(B, N)). all_different([Q_1_1,Q_2_1,Q_3_1,Q_4_1]) and Q_1_1 # 1+Q_2_1 and Q_1_1 # -1+Q_2_1 and Q_1_1 # 2+Q_3_1 and Q_1_1 # -2+Q_3_1 and Q_1_1 # 3+Q_4_1 and Q_1_1 # -3+Q_4_1 and Q_2_1 # 1+Q_3_1 and Q_2_1 # -1+Q_3_1 and Q_2_1 # 2+Q_4_1 and Q_2_1 # -2+Q_4_1 and Q_3_1 # 1+Q_4_1 and Q_3_1 # -1+Q_4_1 and search(Q_1_1 Q_1_1 Q_2_1 Q_2_1 Q_3_1 Q_3_1 Q_4_1 Q_4_1 = = = = = = = = 2 1 2 1 2 1 2 1 or or or or or or or or Q_1_1 Q_1_1 Q_2_1 Q_2_1 Q_3_1 Q_3_1 Q_4_1 Q_4_1 = = = = = = = = 3 or 4 and 3 or 4 and 3 or 4 and 3 or 4) La compilation par −−hstci−→ de l’instance des 4-reines produit théoriquement un code quadratique en la taille de l’instance par la double imbrication de quantificateurs universels. Cependant, la taille du code effectivement produit est inférieure au produit cartésien selon la liste des 4 reines avec elle-même grâce à l’évaluation partielle effectuée sur l’implication, la partie gauche de l’implication étant complètement instanciée à la compilation. Correction des transformations vis-à-vis de la sémantique déclarative des modèles Cream La proposition suivante montre que la compilation par −−hstci−→ préserve la sémantique déclarative des modèles Cream. Proposition 5. Étant donné un modèle Cream M, soit M′ tel que M −−hstci−→ M′ le résultat de la compilation de M, alors Ss (M) = Ss (M′ ) Preuve 4. Pour toute affectation (ν G , ν O ) pour M, nous vérifions inductivement sur ∗ stc les dérivations de −−hstci−→ et −−h srch i−→ que ν G (M) → ν G (M′ ). La plupart des dérivations de −−hstci−→ sont indépendantes de toute affectation et vérifient donc la propriété par définition et correspondent à une réduction de la sémantique. En ce qui concerne les dérivations de −−hstci−→ liées aux appels de définitions, elles sont restreintes aux appels dont les arguments sont indexables et utilisent ν O pour l’indexation. De la même façon, les buts, qui sont des déclarations d’arité nulle, utilisent ν G pour l’indexation des variables libres. 64 Compilation vers CSP par expansion des règles stc La transformation qui formalise le mode recherche −−h srch i−→ se fonde sur −−hstci−→ et n’altère pas l’ensemble des solutions par ailleurs. 6.2.1 - Transformation en code déterministe 6.2 65 Compilation vers CLP par génération de code procédural Le schéma de compilation associé au mode dynamique, dit par génération de code procédural [MMF09], se distingue du schéma statique par la définition de deux transformations qui produisent du code intermédiaire structuré : −−hdyni−→ produit du code déterministe qui pose les appels (aux déclarations) de contraintes et les appels aux parties recherche. Ces sous-expressions en mode recherche dynamique sont transformées par dyn i−→ en code non déterministe qui s’occupe de la génération du code de réordon−−h srch nancement et du parcours de l’arbre de recherche. Ce schéma dynamique est nécessaire lorsque la forme du modèle dépend d’indexicaux, c’est-à-dire d’expressions qui contiennent des variables à domaine fini. Par exemple, supposons qu’un itérateur dépende d’une liste construite par intervalle dont une des bornes est un indexical. Alors, le résultat de l’application de l’itérateur est inconnu au moment de la compilation et le schéma par expansion ne peut s’appliquer. Le code intermédiaire suit la syntaxe des modèles Cream, mais sans l’interdiction de récursion qui est levée, et avec des directives sur les stratégies de branchement (redyn formulés par −−h srch i−→). Un tel code intermédiaire n’introduit pas de difficulté de traduction dans le langage cible à la génération de code. 6.2.1 Transformation en code déterministe V ⊢ · −−hdyni−→ · transforme inductivement sur la structure des expressions Cream. V représente l’ensemble des variables de l’environnement de la sous-expression considérée ; V est utilisé pour passer le contexte aux définitions auxiliaires introduites par la transformation. Chaque déclaration d’objet p(X) = e ∈ O(M) et chaque déclaration de règle p(X) –-> e ∈ R(M) est transformée en le code intermédiaire pd (X) = e′ où fv(e) ⊢ e −−hdyni−→ e′ . Les appels reposent alors sur la définition : V ⊢ p(X) −−hdyni−→ pd (X). Dans le langage source, la récursion est interdite et les combinateurs sur listes finies sont préférés. Cependant, ces itérateurs sont traduits dans les langages cibles par des définitions récursives travaillant sur les transformations des listes. Pour chaque foldl, une définition récursive itérant sur une liste est générée. V ⊢ l −−hdyni−→ l′ V ⊢ i −−hdyni−→ i′ V ⊢ foldl(A from i, X in l, e) −−hdyni−→ q(l′ , i′ , V ) où q est un nouveau symbole de prédicat défini comme suit, et toutes les variables sont fraîches vis-à-vis de V : q([], I, V ) = I. q([H | T ], I, V ) = q(T , e′ , V ). V ⊢ e[A := I, X := H] −−hdyni−→ e′ Compilation vers CLP par génération de code procédural 66 Les autres cas de −−hdyni−→ sont définis homomorphiquement aux sous-expressions, en considérant les questions de portée et de captures de noms : par exemple, V ⊢ d −−hdyni−→ d′ V · X ⊢ e[V := X] −−hdyni−→ e′ V ⊢ let(V := d in e) −−hdyni−→ let(X := d′ in e′ ) où X est une variable fraîche. Les directives de recherche reposent sur la transformation de recherche (définie dans la section 6.2.2). V ⊢ h −−hdyni−→ conjunctive(o1∧ ) ... and conjunctive(on∧ ) and disjunctive(o1∨ ) ... and disjunctive(om ∨) 1 n 1 m ′ dyn ([o∧ , . . . , o∧ ], [o∨ , . . . , o∨ ]); V ⊢ e −−h srch i−→ e V ⊢ search(h, e) −−hdyni−→ e′ 6.2.2 Transformation en code non déterministe Dans le contexte dynamique, ni la stratégie de branchement, ni les critères des heuristiques ne peuvent être supposés connus précisément au moment de la compilation. La dyn transformation −−h srch i−→ génère du code qui réordonne au moment de l’exécution plutôt que de réordonner statiquement au moment de la compilation et les opérateurs de disjonction or deviennent des points de choix. La compilation des heuristiques de recherche repose sur la notion de couche O : pour O ∈ {∧, ∨}, on appelle couche O d’un arbre ∧/∨ un sous-graphe maximal de l’arbre qui ne contient que noeuds O. Le symbole ∧ correspond à la conjonction and et le symbole ∨ à la disjonction or. La définition des couches O est généralisée aux expressions de la syntaxe Cream en leur rendant transparents les let, les appels de déclarations, la partie droite de implies et les arbres décrits en intention par foldl. Les noeuds enfants d’une couche sont les noeuds hors de la couche et enfants d’un noeud dans la couche. La couche O racine est la couche O qui contient le noeud racine s’il n’est pas le dual de O, ou la couche vide sinon. Par convention, le noeud racine est le seul enfant de la couche vide. Le réordonnancement défini par les heuristiques est appliqué entre tous les noeuds enfants de chaque couche O : les critères définis pour un opérateur O conjonctif ou disjonctif associent un vecteur de coûts à chaque enfant, et les enfants sont réordonnés selon leurs coûts, lexicographiquement. dyn La compilation par −−h srch i−→ produit, pour un couple donné de critères (o∧ , o∨ ), du code qui réordonne la racine d’une couche O de l’arbre et récursivement pour les enfants. Deux vecteurs de coût c∧ and c∨ , de même dimension que o∧ et o∨ respectivement, sont entretenus. dyn (o∧ , o∨ ); V ⊢ · −−h srch i−→ · ≡ ∞ dyn (c∞ ∧ , c∨ ); V ⊢ · −−h srch(∧) i−→ · 6.2.2 - Transformation en code non déterministe 67 La transformation est initialisée arbitrairement avec des vecteurs de coûts maxima c∞ ∧ et ∞ c∨ , dont chaque composante est égale à max_integer, puisqu’aucun critère ne s’applique en dehors de la formule induisant l’arbre de recherche. La couche racine, potentiellement vide, peut toujours être considérée comme une couche ∧. dyn dyn · −−h srch(O) i−→ · repose sur la transformation auxiliaire (c∧ , c∨ ); V ⊢ · −−h list(O) i−→ · qui produit du code qui génère une liste d’associations : pour chaque noeud fils d’une couche O, le vecteur de coûts du noeud est associé à l’appel de définition pour explorer le fils récursivement. dyn (c∧ , c∨ ); V ⊢ e −−h list(O) i−→ e′ dyn (c∧ , c∨ ); V ⊢ e −−h srch(O) i−→ iter_predicatesO (e′ ) où iter_predicatesO (l) est une fonction interne qui sélectionne itérativement l’élément de meilleur coût dans la liste l, exécute la définition associée, et considère ensuite les autres éléments récursivement, selon O, soit en conjonction soit en disjonction. Déclarations et appels Pour chaque déclaration p(X) –-> e. ∈ R(M), la compilation produit deux définitions en code intermédiaire, une pour chaque type de couche, i.e. conjonctive et disjonctive. déclaration pour couche ∧ dyn (u(C∧ , o∧ , p(X)), C∨ ); fv(e) ⊢ e −−h srch(∧) i−→ e′ dyn (o∧ , o∨ ); V ⊢ p(X) –-> e −−h srch(∧) i−→ p∧ (C∧ , C∨ , X) = e′ p(X) –-> e. ∈ R(M) déclaration pour couche ∨ dyn (C∧ , u(C∨ , o∨ , p(X))); fv(e) ⊢ e −−h srch(∨) i−→ e′ dyn (o∧ , o∨ ); V ⊢ p(X) –-> e −−h srch(∨) i−→ p∨ (C∧ , C∨ , X) = e′ p(X) –-> e. ∈ R(M) où les vecteurs de variables C∧ = v(dim(o∧ )) et C∨ = v(dim(o∨ )), avec v(n) = [V1 , . . . , Vn ], permettent de passer les coûts d’une déclaration du code intermédiaire à une sous-déclaration. La fonction u(c, o, p(X)) calcule le le nouveau vecteur de coûts c′ , en mettant à jour les composantes qui concernent le prédicat p(X) : − → −−−−−−−−−−→ → u(− ci , ei for pi (Yi ), p(X)) = c′i où : c′i =  σ(e i) ci si σ(pi (Yi )) = p(X) sinon Compilation vers CLP par génération de code procédural 68 Les appels au prédicat p(X) se font sur une de ces deux définitions, selon le type de la couche courante O. dyn (c∧ , c∨ ); V ⊢ p(X) −−h list(O) i−→ pO (c∧ , c∨ , X) p(X) –-> e. ∈ R(M) Conjonction et disjonction dyn dyn Les transformations de conjonctions et disjonctions −−h list(∧) i−→ et −−h list(∨) i−→ sont définies de manière duale. Considérons ici uniquement le cas de les transformations de conjonctions. dyn −−h list(∧) i−→ produit du code qui concatène les listes produites par les transformations des opérandes de la conjonction a and b. dyn (c∧ , c∨ ); V ⊢ a −−h list(∧) i−→ a′ dyn (c∧ , c∨ ); V ⊢ b −−h list(∧) i−→ b′ dyn (c∧ , c∨ ); V ⊢ a and b −−h list(∧) i−→ append(a′ , b′ ) Lors d’un changement d’opérateur au sein d’une couche, en l’occurence une disjonction a or b dans une couche ∧, un changement de couche s’impose. La disjonction est un nouvel élément à ordonner pour la couche courante, un noeud enfant de la couche ∧, et le prédicat q est introduit pour se charger de la construction de la sous couche ∨. La construction syntaxique delay(p(X)) est introduite pour dénoter symboliquement le terme p(X) plutôt que l’application de la déclaration p(X). dyn (c∧ , c∨ ); V ⊢ a or b −−h list(∧) i−→ [{ costs = c∧ , predicate = delay(q(c∧ , c∨ , V )) }] où q applique la transformation récursivement à la sous-couche ∨ (toutes les variables sont fraîches vis-à-vis de V ) : q(C∧ , C∨ , V ) = e. dyn (C∧ , C∨ ); V ⊢ a or b −−h srch(∨) i−→ e Implication Le sous-arbre de la partie gauche b d’une implication a implies b est exécuté si la partie droite a est vraie. V ⊢ a −−hdyni−→ a′ dyn (c∧ , c∨ ); V ⊢ b −−h list(O) i−→ b′ dyn (c∧ , c∨ ); V ⊢ a implies b −−h list(O) i−→ filter(cO , a′ , b′ ) où    d filter(c, g, d) = [{ costs = c,   predicate = delay(true) }] si g est vraie sinon 6.2.2 - Transformation en code non déterministe 69 Contraintes et sous directives de recherche Une contrainte ou une sous directive de recherche e est un noeud enfant d’une couche. La transformation produit donc pour chacune une liste singleton associant leur coût avec un nouveau prédicat q. dyn (c∧ , c∨ ); V ⊢ e −−h list(O) i−→ [{ costs = cO , predicate = delay(q(V )) }] où q applique récursivement la transformation (toutes les variables sont libres vis-àvis de V ) : q(V ) = e′ . V ⊢ e −−hdyni−→ e′ Liaison let V ⊢ v −−hdyni−→ v ′ dyn (c∧ , c∨ ); V · Y ⊢ e[X := Y ] −−h list(O) i−→ e′ dyn (c∧ , c∨ ); V ⊢ let(X := v in e) −−h list(O) i−→ let(Y := v ′ , e′ ) où Y est une variable fraîche. Combinateurs Les combinateurs sont traduits par des définitions récursives. De plus, il est possible que l’appel récursif se fasse dans une autre couche que celle de l’appel du combinateur. C’est pourquoi la compilation des combinateurs fait usage d’un symbole spécial rec pour appréhender la récursion de manière compatible avec les changements de couche. V ⊢ reverse(l) −−hdyni−→ l′ dyn (c∧ , c∨ ); V ⊢ foldl(A from i, X in l, e) −−h list(O) i−→ qO (l′ , c∧ , c∨ , V ) où qO est un nouveau symbole de prédicat défini comme suit (toutes les variables sont fraîches vis-à-vis de V ) : qO ([],C∧ ,C∨ ,V ) = i′ . qO ([H | T ],C∧ ,C∨ ,V ) = e′ . dyn (C∧ , C∨ ); V ⊢ i −−h list(O) i−→ i′ (C∧ , C∨ ); V · H ⊢ dyn −−h list(O) i−→ e′ e[A := rec(q, T , V ), X := H] et rec est traduit en l’appel récursif à qO : dyn (c∧ , c∨ ); V · H ⊢ rec(q, T , V ) −−h list(O) i−→ qO (T , c∧ , c∨ , V ) Compilation vers CLP par génération de code procédural 70 6.2.3 Complexité Propriété 1. Pour tout modèle Cream M, la taille du programme généré par −−hdyni−→ est linéaire en la taille de M. Il existe O(|D(m)| · s) définitions du code intermédiaire (pd , p∨ et p∧ ), où |D(m)| est le nombre de définitions de M, et s est le nombre de prédicats search. Chaque définition du code intermédiaire, et donc du code généré, est de taille linéaire en la taille de la déclaration Cream originelle ; définitions auxiliaires dédiées aux foldl et aux sous-couches comprises. dyn Preuve 5. Les transformations −−hdyni−→ et −−h list(·) i−→ sont des transformations inductives pour lesquelles chaque étape compose linéairement le résultat des sous transformations, soit par définitions auxiliaires, soit par expressions « en place ». Par conséquent, il existe un facteur constant entre la taille des définitions générées et la taille d’une déclaration Cream originelle. Plus précisément, pour chaque déclaration Cream p(X), sont générées une définition pd , et deux définitions p∨ et p∧ par sous-directive de recherche search.  Ce résultat de complexité est à mettre en rapport avec la transformation du mode statique −−hstci−→ dans laquelle le développement complet des déclarations mène à une taille du code généré exponentielle en la taille du modèle dans le pire des cas. Exemple 18. Considérons la transformation de la version dynamique du modèle Cream du problème des n-reines. Pour une description des heuristiques du modèle employées, voir la section 7.1. Par la suite, le modèle est représenté à gauche et sa transformation en code intermédiaire par −−hdyni−→ à droite. But et déclarations d’objets Le but du modèle est compilée par −−hdyni−→ en le code intermédiaire suivant où chaque appel de déclaration du but du modèle est associé un appel de définition du code intermédiaire : ? dynamic(let(N := 4, B := board(N), queens_constraints(B, N) and queens_labeling(variables(B)))). goal = let(N := 4, B := board(N), queens_constraints(B, N) and queens_labeling(rcp_variables(B))). La déclaration q/1 est transformée en une déclaration du code intermédiaire dans laquelle la variable libre porte l’information de sa provenance (premier argument de rcp_var/2) et sa position syntaxique dans le corps de la déclaration (deuxième argument de rcp_var/2). q(I) = {row:_, column:I}. q(I) = rcp_rec([row(rcp_var(q(I),1)), column(I)]). La déclaration de l’échiquier board/1 est transformée en une définition principale et une définition récursive auxiliaire board_foldl_1/3 pour le map : board(N) = map(I in [1 .. N], q(I)). board_foldl_1([], I_board_foldl_1, _) = I_board_foldl_1. board_foldl_1([I_1 | Tail_1], I_board_foldl_1, []) = board_foldl_1(Tail_1, append(I_board_foldl_1, q(I_1)), []). board(N) = board_foldl_1(rcp_range(1, N), [], []). 6.2.3 - Complexité 71 Déclarations de règles relatives aux contraintes La contrainte all_different/1 est une contrainte globale intégrée à Cream et les contraintes relatives aux diagonales sont posées par la traduction de l’imbrication des quantifications universelles safe_foldl_2/3. safe(L) --> all_different(L) and forall(Q in L, forall(R in L, let(I := Q:column, J := R:column, I < J implies Q:row # J - I + R:row and Q:row # I - J + R:row))). queens_constraints(B, N) --> domain(B, 1, N) and safe(B). safe(L) = all_different(rcp_variables(L)) and safe_foldl_2(L, 1, []). queens_constraints(B, N) = domain(rcp_variables(B), 1, N) and safe(B). La transformation du quantificateur le plus extérieur (dont la variable liée est donne la définition récursive safe_foldl_2/3 qui itère sur les éléments de la liste représentant l’échiquier. À chaque itération, safe_foldl_2/3 combine par conjonction (avec comme élément initial 1) les contraintes posées par la définition safe_foldl_3/3 : Q) safe_foldl_2([], I_safe_foldl_2, _) = I_safe_foldl_2. safe_foldl_2([Q_2 | Tail_2], I_safe_foldl_2, []) = safe_foldl_2(Tail_2, (I_safe_foldl_2 and safe_foldl_3(L, 1, Q_2)), []). La définition récursive safe_foldl_3/3 est la traduction du quantificateur universel le plus profond et itère aussi sur la liste représentant l’échiquier. Elle pose effectivement les contraintes de non-capture entre une reine particulière, représentée par la variable Q_2 de l’environnement, et l’ensemble des reines contenues dans la liste dont l’élément courant est la variable R_3 . safe_foldl_3([], I_safe_foldl_3, _) = I_safe_foldl_3. safe_foldl_3([R_3 | Tail_3], I_safe_foldl_3, [Q_2]) = safe_foldl_3(Tail_3, (I_safe_foldl_3 and let(I := rcp_att(Q_2, column), J := rcp_att(R_3, column), I < J implies rcp_att(Q_2, row) # J - I + rcp_att(R_3, row) and rcp_att(Q_2, row) # I - J + rcp_att(R_3, row))), [Q_2]). Compilation vers CLP par génération de code procédural 72 Déclarations des heuristiques Dans le modèle Cream, une heuristique conjonctive (ff) et une heuristique disjonctive (mo) paramètrent le prédicat search comme présenté ci-dessous. heuristic ff = conjunctive(least(domain_size(Var)) for queens_labeling_var(Var)). heuristic mo = disjunctive(least(abs((domain_max(Var) - domain_min(Var))/2 - Val)) for queens_labeling_val(Var, Val)). queens_labeling(Vars) --> search(ff and mo, forall(Var in Vars, queens_labeling_var(Var))). Quantification universelle et filtrage de la règle queens_labeling_var/1 dyn La compilation du quantificateur universel forall par −−h list(∧) i−→ produit un appel à la procédure récursive srch_foldl_1_and/1. Cette procédure est considérée par convention dans une couche ∧ et les coûts correspondant aux critères des heuristiques sont initialisés à max_integer (le plus grand coût possible). queens_labeling(Vars) = iter_predicates_and(srch_foldl_1_and(Vars, [max_integer], [max_integer], [])). srch_foldl_1_and/1 génère une liste de procédure iter_predicates_and/1 posera couples (costs, predicate) à partir de laquelle la itérativement le but associé au couple de plus petit coût. À noter que la procédure transporte les vecteurs de coûts relatifs aux heuristiques conjonctives et disjonctives par les variables CostsC et CostsD, respectivement. srch_foldl_1_and([], CostsC, _, _) = [rcp_rec([costs(CostsC), predicate(delay(true))])]. srch_foldl_1_and([Var | Tail], CostsC, CostsD, []) = append(queens_labeling_var_and(CostsC, CostsD, [Var]), srch_foldl_1_and(Tail, CostsC, CostsD, [])). Chaque élément de la liste produite est le résultat d’un appel à la définition du code intermédiaire queens_labeling_var_and/3, générée par la compilation de la règle du modèle queens_labeling_var/1, qui définit un sous arbre des affectations possibles d’une variable du problème. queens_labeling_var(Var) --> exists(Val in [domain_min(Var) .. domain_max(Var)], queens_labeling_val(Var, Val)). La règle queens_labeling_var/1 est membre d’une couche ∧ et filtrée par l’heuristique Le coût relatif à l’heuristique conjonctive est donc mis à jour pour la définition du code généré et devient domain_size(Var). ff. queens_labeling_var_and(_, CostsD, [Var]) = srch_foldl_2_and(rcp_range([domain_min(Var)..domain_max(Var)]), domain_size(Var), CostsD, [Var]). 6.2.3 - Complexité 73 Quantification existentielle et filtrage de la règle queens_labeling_val/2 dyn La compilation du quantificateur existentiel exists par −−h list(∧) i−→ produit un appel à la procédure récursive srch_foldl_2_and/3 dans laquelle s’opère le changement de couche ∧ en couche ∨. srch_foldl_2_and([], CostsC, CostsD, _) = [rcp_rec([costs(CostsC), predicate(delay(true))])]. srch_foldl_2_and([Val | Tail], CostsC, CostsD, [Var]) = [rcp_rec([costs(CostsC), predicate(delay(srch_subterm_1(Tail,CostsC,CostsD,[Var,Val])))])]. Le changement de couche est matérialisé par l’appel à la définition srch_subterm_1/4. srch_subterm_1(Tail, CostsC, CostsD, [Var, Val]) = iter_predicates_or(append(queens_labeling_val_or(CostsC, CostsD, [Var, Val]), srch_foldl_2_or(Tail, CostsC, CostsD, Var))). Cette définition fait elle-même appel à la procédure récursive srch_foldl_2_or qui génère la liste des appels affectations possibles et dont chaque élément est le produit d’un appel à queens_labeling_val_or/3. Dans le cas présent, la procédure iter_predicates_or/2 appliquera récursivement le choix non déterministe du prédicat de ladite liste associé au plus petit coût. srch_foldl_2_or([], _, CostsD, _) = [rcp_rec([costs(CostsD), predicate(delay(true))])]. srch_foldl_2_or([Var | Tail], Costs_I, Costs_J, [Var, Val]) = append(queens_labeling_var_or(CostsC, CostsD, [Var, Val]), srch_foldl_1_or(Tail, CostsC, CostsD, [Var, Val])). Le prédicat queens_labeling_val_or/3 est le résultat de la compilation de dyn queens_labeling_val/2 par −−h list(∨) i−→ et correspond à la sélection d’une valeur pour Var. queens_labeling_val(Var, Val) --> Var = Val. queens_labeling_val_or/3 renvoit un couple dont le coût est celui relatif à l’heuristique disjonctive (abs(Var-Val)/2) et le prédicat associé est l’affectation elle-même (Var=Val). queens_labeling_val_or(_, _, [Var, Val]) = rcp_rec([costs([abs(Var - Val)/2]), predicate(delay(Var = Val))]). Cet exemple a illustré le schéma de compilation par génération de code procédural et la linéarité de cette transformation. Pour chaque règle et quantificateur sont produites deux définitions du code intermédiaire et à chaque changement de couche est produite une définition du code intermédiaire. 74 Compilation vers CLP par génération de code procédural Chapitre 7 Heuristiques de recherche déclaratives Sommaire 7.1 Heuristiques pour les formules conjonctives et disjonctives 76 7.2 Heuristiques pour les affectations de variables . . . . . . . . 79 7.3 Heuristiques pour des problèmes de placement . . . . . . . 80 Ajouter à un langage de modélisation les moyens d’exprimer des heuristiques de recherche est indispensable pour résoudre un problème combinatoire efficacement voire effectivement. La PPC offre un langage riche pour formuler les problèmes combinatoires, mais dans la plupart des cas concrets, une fois modélisé, le problème n’est pas résolu par une stratégie de recherche standard. Alors, sans les moyens de définir et expérimenter rapidement une stratégie, les ingénieurs non-experts du domaine ne vont pas plus loin avec la PPC. Les langages de modélisation OPL et Comet sont les seuls de l’état de l’art à proposer une composante recherche. Dans ces langages, les stratégies de branchement sont exprimées par des imbrications d’itérateurs forall pour le séquencement et tryall pour le choix non déterministe de contraintes. Ils offrent en outre des structures de contrôle comme les conditionnelles ou les boucles et donc une composante de programmation de la recherche. Les heuristiques sont définies comme des ordres lexicographiques sur les listes qui paramètrent ces deux itérateurs. Une originalité de Cream est de proposer une composante recherche purement déclarative. Les stratégies de branchement sont exprimés par des formules logiques quelconques dans la portée du prédicat search. Dans ces formules, la conjonction est interprétée comme l’opérateur de séquence et la disjonction comme l’opérateur de choix non déterministe. Les quantificateurs universels forall et existentiels exists jouent donc des rôles similaires aux itérateurs forall et tryall d’OPL et Comet. Il est possible en Cream de définir simplement des stratégies de branchement plus complexes que des entrelacements de quantifications universelles et existentielles avec les combinateurs génériques fold. En Cream, les heuristiques ne sont pas définies à chaque niveau d’itérateur sur les listes qui les paramètrent mais par filtrage ou pattern-matching sur les têtes de règles présentes dans les sous-formules conjonctives et disjonctives du modèle. 75 Heuristiques pour les formules conjonctives et disjonctives 76 L’avantage de cette approche est de permettre au modélisateur de définir indépendemment et de manière déclarative d’une part la stratégie de branchement et d’autre part les heuristiques qui le réordonne à un haut niveau de discours. Les conséquences pratiques sont la rapidité de développement et de modification des stratégies de recherche et leur lisibilité même pour les non-programmeurs. Dans un langage de programmation logique avec contraintes comme SICStus-Prolog [C 07], la notion d’heuristique de sélection de variables et de valeurs recouvre à la fois la notion d’ordonnancement et la notion de stratégie de branchement. L’ensemble des heuristiques standards au sens de SICStus-Prolog est représenté par les options du prédicat labeling/2. La table suivante montre pour chaque option comment s’exprime l’équivalent Cream. + Option de labeling/2 SICStus-Prolog min max ff ffc enum bisect step up down Équivalent Cream conjunctive(least(domain_min(X)) for labeling_var(X) conjunctive(greatest(domain_max(X)) for labeling_var(X) cf. Sec. 7.2 conjunctive(least(domain_size(X)) for labeling_var(X) and conjunctive(greatest(degree(X)) for labeling_var(X) arbre d’affectations type Fig. 4.6, cf. Sec. 7.2 dichotomic_split/1, cf. Chap. 9 sur le modèle de interval_split/1, cf. Chap. 9 disjunctive(least(V) for labeling_val(_, V) disjunctive(greatest(V) for labeling_val(_, V) Table 7.1: Heuristiques de sélection de variables et de sélection de valeurs standards SICStus-Prolog et équivalents Cream. Dans la table 7.1, on considère une définition Cream du labeling sur la base du modèle présenté dans la section 7.2. On suppose que le langage est enrichi de la fonction prédéfinie degree/1 qui prend une variable à domaine fini X en argument et dénote le nombre de contraintes actives du modèle dans lesquelles X apparaît. Dans ce chapitre, nous présentons des stratégies de recherches Cream pour trois problèmes. Un premier problème simple d’ordonnancement de tâches disjonctives illustre la capacité de Cream à spécifier des heuristiques de façon déclarative sur des formules logiques. Ensuite, le problème des n-reines montre que les heuristiques traditionnelles de sélection de variables et de valeurs sont respectivement des cas particuliers d’heuristiques conjonctives et disjonctives. Enfin nous renvoyons à un problème de placement issu de l’industrie pour montrer l’efficacité de l’approche sur des problèmes réels. 7.1 Heuristiques pour les formules conjonctives et disjonctives Considérons le modèle Cream du problème du pont (Bridge Problem [Van99], p. 209) consistant à trouver un ordonnancement des tâches qui minimise la durée de construction CHAPITRE 7. HEURISTIQUES DE RECHERCHE DÉCLARATIVES 77 d’un pont à 5 segments. Le projet implique 46 tâches et un ensemble de contraintes sur ces tâches. En plus des contraintes habituelles de précédence, le problème fait intervenir des contraintes de ressources. La plupart des tâches requièrent une ressource (une grue par exemple) et les tâches qui demandent la même ressource ne peuvent se chevaucher dans le temps. Les déclarations d’objets Les tâches sont définies comme des couples (date de début, durée). La fonction end/1 calcule la somme des composantes et dénote donc la date de fin d’une tâche. m1 = {start : _, duration : 16}. m2 = {start : _, duration : 8}. (...) end(Task) = Task:start + Task:duration. La borne supérieure de la durée effective de construction du pont et le domaine des tâches sont définis. (...) maxDuration = sum(map(T in tasks, T:duration)). tasks_domain --> domain(tasks, 0, maxDuration). Le but Le but du modèle Cream consiste à poser les contraintes de précédence, de distance et de disjonction et à demander la minimisation de la date de fin des travaux par une recherche branch and bound (cf. Sec. 4.1). ? tasks_domain and precedences and distances and disjunctives and minimize(disjunctives, stop:start). Heuristiques et déclarations règles relatives à la recherche L’arbre de recherche exploré à chaque itération de l’optimisation est induit par la formule disjunctives, dans la portée du prédicat de minimize, qui met en disjonction les tâches qui requièrent la même ressource. La formule disjunctives est définie par les règles suivantes : precedes(T1, T2) --> end(T1) =< T2:start. disjuncts(T1, T2) --> precedes(T1, T2) or precedes(T2, T1). disjunctives --> forall(Task in resource, forall(T1 in Task, forall(T2 in Task, T1:uid < T2:uid implies disjuncts(T1, T2)))). Supposons que l’on veuille que la recherche essaye les paires de tâches disjonctives dans l’ordre décroissant des durées. L’heuristique et le but s’écrivent alors ainsi : 78 Heuristiques pour les formules conjonctives et disjonctives heuristic h = conjunctive(greatest(T1:duration + T2:duration) for disjuncts(T1, T2)). ? tasks_domain and precedences and distances and minimize(search(h, disjunctives), stop:start). L’heuristique h définit un ordre sur les termes en conjonction issus de disjunctives et qui s’unifient avec disjuncts(T1, T2), c’est-à-dire les contraintes disjonctives. Donc, dans la conjonction produite par disjunctives, les sous-formules (contraintes) issues de la déclaration de règle disjuncts(T1, T2) seront essayées lors de la recherche dans l’ordre décroissant (greatest) des sommes de durées T1:duration+T2:duration. Il est aussi possible d’exprimer un ordre sur les termes de chaque disjonction, par exemple celui qui préfère accomplir la tâche de plus grande durée avant l’autre. Cela consiste à ajouter l’heuristique disjonctive suivante portant sur les termes introduits par la règle precedes(T1, T2), et h devient alors : heuristic h = conjunctive(greatest(T1:duration + T2:duration) for disjuncts(T1, T2)) and disjunctive(greatest(T1:duration) for precedes(T1, T2)). Le résultat (partiel) de compilation vers SICStus-Prolog donne le code suivant où le point-virgule dénote l’opérateur de choix non déterministe, et la virgule l’opérateur de séquence : (...) minimize((((M6+20#=<M1 ; M1+16#=<M6), (P1+20#=<P2 ; P2+13#=<P1), (M6+20#=<M2 ; M2+8#=<M6), (M6+20#=<M3 ; M3+8#=<M6), (M6+20#=<M4 ; M4+8#=<M6), (M6+20#=<M5 ; M5+8#=<M6), (V1+15#=<V2 ; V2+10#=<V1), (T1+12#=<T2 ; T2+12#=<T1), (...) (B4+1#=<B6 ; B6+1#=<B4), (B5+1#=<B6 ; B6+1#=<B5) ), labeling([up], [Stop])), Stop). On peut constater que les contraintes disjonctives sont posées dans un ordre non croissant de somme des durées des tâches (20+16 ≥ 20+13 . . . ). Pour chaque contrainte disjonctive, la tâche de plus grande durée est essayée d’abord (20 ≥ 16, 20 ≥ 13, ...). L’heuristique conjonctive laisse des cas d’égalité que l’on peut par exemple départager en ajoutant une heuristique conjonctive dynamique qui préfère essayer les paires qui peuvent débuter au plus tôt. Les deux heuristiques conjonctives forment alors un vecteur de critères : heuristic h = conjunctive(greatest(T1:duration + T2:duration) for disjuncts(T1, T2)) and conjunctive(least(domain_min(T1:start)+domain_min(T2:start)) for disjuncts(T1, T2)). CHAPITRE 7. HEURISTIQUES DE RECHERCHE DÉCLARATIVES 7.2 79 Heuristiques pour les affectations de variables En Cream, les heuristiques pour le choix de variables et de valeurs dans un labeling (cf. Sec. 4.3) sont exprimées naturellement comme des heuristiques sur une formule qui représente l’arbre de recherche des affectations. Pour illustrer ces propos, considérons la version dynamique du modèle Cream du problème des n-reines (cf. Sec. 2.2) pour lequel la recherche est guidée par des heuristiques de choix de variables et de valeurs. Le code généré par la compilation de ce modèle dynamique Cream est présenté dans la section 6.2. Le but Le but du modèle consiste à poser les contraintes et à demander la recherche par affectations. Le prédicat dynamique impose au compilateur d’appliquer le schéma par génération de procédures. ? dynamic(let(N := 4, B := board(N), queens_constraints(B, N) and queens_labeling(variables(B)))). Heuristiques et déclarations de règles relatives à la recherche D’un point de vue logique, l’arbre des affectations correspond à la formule qui énonce que pour toute variable V du problème, il existe une affectation de V à une valeur dans son domaine (qui satisfait toutes les contraintes). La partie recherche s’exprime avec les déclarations de règles suivantes : queens_labeling(Vars) --> search(ff, forall(Var in Vars, queens_labeling_var(Var))). queens_labeling_var(Var) --> exists(Val in [domain_min(Var) .. domain_max(Var)], queens_labeling_val(Var, Val)). queens_labeling_val(Var, Val) --> Var = Val. où queens_labeling_var(Var) définit le sous-arbre des affectations possibles d’une variable et queens_labeling_val(Var, Val) son affectation à une valeur particulière. Une bonne heuristique conjonctive pour le problème des n-reines est une instance du principe first-fail (cf. Sec. 4.3) : on préfère les variables dont la taille de domaine est la plus petite. L’heuristique dynamique Cream (des indexicaux sont en jeu) correspondante filtre les termes des sous-formules conjonctives qui sont introduits par la règle de tête queens_labeling_var(Var). Le critère d’ordre de l’heuristique a pour effet d’appliquer les appels à queens_labeling_var(Var) dans l’ordre croissant (least) des tailles de domaine de la variable Var. heuristic ff = conjunctive(least(domain_size(Var)) for queens_labeling_var(Var)). 80 Heuristiques pour des problèmes de placement On peut combiner avec cette heuristique conjonctive l’heuristique disjonctive middle out, introduite dans un contexte statique à la fin de la section 5.1, qui ordonne les valeurs d’un domaine du milieu vers les bornes. Dans le contexte dynamique de cet exemple, l’heuristique peut se baser sur les indexicaux et profiter de la propagation. L’heuristique disjonctive et la recherche Cream correspondantes s’écrivent de la manière suivante : heuristic mo = disjunctive(least(abs((domain_max(Var) - domain_min(Var))/2 - Val)) for queens_labeling_val(Var, Val)). queens_labeling(Vars) --> search(ff and mo, forall(Var in Vars, queens_labeling_var(Var))). 7.3 Heuristiques pour des problèmes de placement Nous renvoyons au chapitre 9 pour d’autres exemples de spécifications d’heuristiques de placement dans Cream. Ces heuristiques ont permis de résoudre effectivement des problèmes de placement réels issus de l’industrie avant le terme du projet européen NetWMS, ce qui ne fût pas le cas des autres partenaires académiques qui travaillaient avec SICStus-Prolog et Choco-Java. D’autre part, les ingénieurs experts en placement ont pu lire ces heuristiques et y reconnaître certains principes utilisés en colisage. Troisième partie Évaluation Sommaire 8 Bibliothèque pour la modélisation de connaissances en placement 83 8.1 Formes et objets k-dimensionnels . . . . . . . . . . . . . . . . . . . . . . 85 8.2 Relations et règles de placement génériques . . . . . . . . . . . . . . . . . 87 8.3 Règles de placement spécifiques . . . . . . . . . . . . . . . . . . . . . . . 90 9 Modèles et résolution de problèmes de placement 95 9.1 Problème de placement optimal de carrés dans un rectangle . . . . . . . 96 9.2 Problème de chargement de palette . . . . . . . . . . . . . . . . . . . . . 101 9.3 Problème de chargement de container . . . . . . . . . . . . . . . . . . . . 105 82 SOMMAIRE Chapitre 8 Bibliothèque pour la modélisation de connaissances en placement Sommaire 8.1 Formes et objets k-dimensionnels . . . . . . . . . 8.1.1 Assemblages de formes k-dimensionnelles . . . . 8.1.2 Alternatives d’assemblages de formes . . . . . . . 8.2 Relations et règles de placement génériques . . 8.2.1 Relations d’Allen . . . . . . . . . . . . . . . . . . 8.2.2 Relations du Region Connection Calculus (RCC) 8.2.3 Règles de placement . . . . . . . . . . . . . . . . 8.3 Règles de placement spécifiques . . . . . . . . . . 8.3.1 Règles relatives au poids . . . . . . . . . . . . . . 8.3.2 Règles relatives aux longueurs et aux surfaces . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 85 86 87 87 88 89 90 90 91 Nous présentons ici la bibliothèque de modélisation des connaissances en placement PKML (pour Packing Knowledge Modelling Library) [FM09]. C’est à notre connaissance la première bibliothèque d’un langage de modélisation qui offre les définitions nécessaires à la modélisation des problèmes de placement de dimensions quelconques. PKML illustre l’expressivité de Cream et a permis de résoudre des problèmes de placement réels issus du projet européen Net-WMS qui traite des problèmes de placement de taille réelle en logistique et dans l’industrie automobile (cf. Sec. 9.2 et Sec. 9.3). La bibliothèque a participé à la résolution effective de ces problèmes que les autres partenaires du projet travaillant avec SICStus-Prolog ou Choco-Java n’ont pas eu le temps de résoudre en prenant en compte toutes les contraintes en jeu. PKML repose sur le formalisme de règles, le mécanisme de réification implicite et le système de modules de Cream. La bibliothèque est construite comme une hiérarchie de modules. Le module de base introduit les règles qui définissent les relations d’intervalles d’Allen [All91]. Sur ce premier module sont construites les règles qui définissent les relations du Region Connection Calculus [RCC92] entre orthotopes (ou boîtes) de dimensions quelconques. Le module de plus haut niveau s’attache à définir, sur la base des deux premiers, les problèmes de Bin Packing purs et un ensemble de règles spécifiques qui concernent la répartition du poids et la stabilité de placements en 3 dimensions. De plus, la bibliothèque fait usage 83 84 de la contrainte géométrique geost [BCP+ 07] intégrée à Cream. Contraintes géométriques au-dessus de geost Un objectif du projet européen a été réalisé avec le développement de la contrainte géométrique geost. Cette contrainte globale très efficace est dédiée aux problèmes de placement en dimensions quelconques. Elle est implantée en SICStus-Prolog et en Choco-Java et intégrée dans Cream sous le nom de non_overlapping/2. Elle prend en charge essentiellement les contraintes de non-enchevêtrement d’orthotopes de dimensions quelconques et ne fournit pas de support pour d’autres contraintes qui doivent être définies en Cream en l’occurrence. Par exemple, le problème de chargement de containers (cf. Sec. 9.3) consiste en un problème de placement Bin Packing en 3 dimensions enrichi de plusieurs contraintes spécifiques de répartition de poids et de stabilité : container_loading_constraints(Items, Bin, BinSize, Dims) --> domains(Items, BinSize, Dims) and containmentAE(Items, [Bin], Dims) and non_overlapping(Items, Dims) and gravity(Items) and blocked(Items) and stack_height(Items) and stack_area(Items) and stack_alignment(Items) and stack_support_area(Items, 100) and stack_weight_sum(Items) and weight_balancing(Items, Bin, 1, 10). Par ailleurs, nous avons participé à l’évolution de geost avec règles [CBM08]. Dans cette version, geost est paramétrée par un langage de règles qui constitue un sousensemble strict des règles PKML restreintes aux contraintes linéaires. Il a été montré que ce langage plus simple que PKML est compilable efficacement vers les indexicaux k-dimensionnels du noyau de geost. Extensibilité du langage et réification La réification est un mécanisme très utile pour composer arbitrairement des contraintes, mais dans un langage comme SICStus-Prolog par exemple, il est nécessaire de manipuler les variables associées à chaque contrainte réifiée. Cream offre une réification implicite qui permet de composer arbitrairement des règles sans avoir à manipuler explicitement ces variables réifiées. Considérons par exemple la définition d’une contrainte disjonctive de précédence entre tâches. En SICStus-Prolog (à gauche), la réification de la contrainte est explicitée par la variable R alors qu’elle est implicite en Cream (à droite). precedes(T1, T2, R) :end(T1, ET1), origin(T2, OT2), (ET1 #< OT2) #<=> R. precedes(T1, T2) --> end(T1) < origin(T2). 8.1.1 - Assemblages de formes k-dimensionnelles 8.1 85 Formes et objets k-dimensionnels 8.1.1 Assemblages de formes k-dimensionnelles Les formes définies dans PKML sont des assemblages d’orthotopes et vivent dans Zk . Un orthotope est une généralisation du rectangle que l’on peut définir comme un produit cartésien d’intervalles. Un point dans cet espace est représenté par une liste de ses k coordonnées entières [i1,...,ik]. Ces coordonnées peuvent être des variables ou des valeurs constantes entières. Une boîte ou box est un orthotope dans Zk , représenté par un enregistrement contenant un attribut taille ou size indiquant la liste des longueurs de la boîte dans chaque dimension. Une boîte déportée ou shifted box est représentée par un enregistrement contenant un attribut box et un attribut offset donnant les coordonnées locales (à une forme) de la boîte dans chaque dimension. Dans PKML, une forme ou shape est un assemblage rigide de boîtes déportées. Une forme est alors représentée par un enregistrement contenant un attribut sboxes pour la liste des boîtes déportées constituant la forme. Par exemple : % Soit b1 et b2 deux boîtes. b1 = {size = [3, 1]}. b2 = {size = [1, 3]}. % Soit sb1, sb2, sb1 = {box = b2, sb2 = {box = b1, sb3 = {box = b2, sb4 = {box = b1, sb3 et offset offset offset offset sb4 quatre boîtes déportées. = [1, 2]}. = [0, 0]}. = [3, 0]}. = [0, 0]}. % Soit s1, s2 deux formes. s1 = {sboxes = [sb1, sb2]}. s2 = {sboxes = [sb3, sb4]}. sb1 b2 b1 sb2 s1 s2 sb3 sb4 Les déclarations suivantes définissent respectivement : la construction d’une boîte et son volume ; et la construction d’une boîte déportée et sa taille dans une dimension donnée. Formes et objets k-dimensionnels 86 make_box(L) = {size = L}. box_volume(B) = prod(B:size). make_sbox(B, O) = {box = B, offset = O}. sbox_size(SB, D) = nth(D, SB:box:size). Le constructeur d’une forme quelconque (un assemblage de boîtes) et d’une forme composée d’une seule boîte, l’origine locale, la fin et la taille de la forme dans une dimension donnée, et le volume de la forme (en supposant que les boîtes de l’assemblage de s’enchevêtrent pas) : make_shape(SBs) = {sboxes = SBs}. make_shape_box(B) = make_shape([make_sbox(B, map(_ in B:size, 0))]). shape_origin(S, D) = min(map(SB shape_end(S, D) = max(map(SB in shape_size(S, D) = shape_end(S, shape_volume(S) = sum(map(SB in 8.1.2 in S:sboxes, sbox_offset(SB, D))). S:sboxes, sbox_end(SB, D))). D) - shape_origin(S, D). S:sboxes, box_volume(SB:box))). Alternatives d’assemblages de formes Un objet PKML ou object, désignant un container ou un carton, est un enregistrement portant un attribut shapes indiquant un ensemble de formes alternatives, un point origin et un attribut shape_index représentant la forme courante de l’objet. Des attributs optionnels tels que le poids pourraient aussi apparaître dans l’enregistrement. On appelle polymorphe un objet qui peut prendre plus d’une forme. Les formes alternatives d’un objet peuvent représenter des rotations discrètes d’une forme, comme par exemple les formes s1 et s2 de l’objet o1 dans la figure ci-dessous, ou des formes complètement différentes dans un problème de configuration. Nous ne distinguons pas entre les traits d’un objet à placer et ceux d’un contenant, puisque le contenant d’un niveau peut devenir un objet à placer au niveau supérieur (cf. sections 9.2 et 9.3). % Soit o1 l’objet polymorphe de forme s1 ou s2 o1 = {shapes = [s1, s2], shape_index = S, origin = [_, _]}. ? let(Origin := o1:origin, o1:shape_index = 1 and nth(1, Origin) = 1 and nth(2, Origin) = 1). ? let(Origin := o1:origin, o1:shape_index = 2 and nth(1, Origin) = 1 and nth(2, Origin) = 1). d2 d2 s1 o1 o1 s2 1 1 d1 1 d1 1 8.2.1 - Relations d’Allen 87 Il est à noter que si les tailles des boîtes composant une forme sont connues, les déclarations de taille et de volume ont pour valeur des entiers fixés ; alors que si les tailles sont inconnues, c’est-à-dire des variables, les expressions ont pour valeur des termes non clos. Ces termes sont nécessaires en PKML pour définir, à l’aide de variables réifiées, la fin, le bout dans une dimension donnée ou le volume d’un objet polymorphe, comme suit : make_object(SL, OL, S) = {shapes=SL, shape_index=S, origin=OL}. make_object_shape(S, L) = {shapes=[S], shape_index=1, origin=L}. origin(O, D) = nth(D, O:origin). end(O, D) = origin(O, D) + sum(map(S in [1 .. length(O:shapes)], (O:shape_index = S) * shape_end(nth(S, O:shapes), D))). size(O, D) = sum(map(S in [1 .. length(O:shapes)], (O:shape_index = S) * shape_size(nth(S, O:shapes), D))). area(O, D1, D2) = size(O, D1) * size(O, D2). volume(O) = sum(map(S in [1 .. length(O:shapes)], (O:shape_index = S) * shape_volume(nth(S, O:shapes)))). 8.2 Relations et règles de placement génériques La bibliothèque PKML repose sur les relations d’intervalles d’Allen [All91] en une dimension, et les relations topologiques du Region Connection Calculus (RCC) [RCC92] en dimensions supérieures. Dans les deux cas, les relations présentent deux bonnes propriétés : elles sont complètes et mutuellement exclusives. Toute paire de polytope entretient une des relations mais pas deux à la fois. Toutes ces relations sont prédéfinies entre orthotopes (entiers) dans dans deux bibliothèques [FM08]. Des relations supplémentaires sont ajoutées pour des raisons de commodité ou d’efficacité, 8.2.1 Relations d’Allen Les relations les plus simples définies dans PKML sont des relations entre orthotopes uni-dimensionnels, donc des intervalles. Il existe 13 relations d’Allen (6 relations avec leur symétrique plus la relation d’égalité) dont nous décrivons un sous-ensemble ici. precedes/3, preceded_by/3 : un intervalle a précède un intervalle b dans une dimension d lorsque a se termine strictement avant le début de b. precedes(A, B, D) --> end(A, D) < origin(B, D). preceded_by(A, B, D) --> end(B, D) < origin(A, D). a b d b d a Relations et règles de placement génériques 88 meets/3, met_by/3 : un intervalle a rencontre un intervalle b dans la dimension d lorsque a se termine avant le début b et le touche. meets(A, B, D) --> end(A, D) = origin(B, D). met_by(A, B, D) --> end(B, D) = origin(A, D). a b d b a d overlaps/3, overlapped_by/3 : un intervalle a chevauche un intervalle b lorsque a commence strictement avant b et se termine strictement après le début de b et strictement avant la fin de b. overlaps(A, origin(A, end(A, D) origin(B, B, D) --> D) < origin(B, D) and < end(B, D) and D) < end(A, D). overlapped_by(A, B, D) --> origin(B, D) < origin(A, D) and origin(A, D) < end(B, D) and end(A, D) > end(B, D). a b d b a d De façon générale, deux intervalles a et b se chevauchent lorsque leur intersection est non vide. Par définition, cette notion de chevauchement n’est pas définie par une relation d’Allen unique. Puisqu’elle est nécessaire dans tout problème de placement, bibliothèque introduit la relation supplémentaire overlaps_sym/3 : overlaps_sym(A, B, D) --> end(A, D) > origin(B, D) and end(B, D) > origin(A, D). 8.2.2 Relations du Region Connection Calculus (RCC) Les relation de RCC sont définies sur les relations uni-dimensionnelles d’Allen. disjoint/3 : deux orthotopes sont disjoints s’il existe au moins une dimension dans laquelle la projection de l’un précède la projection de l’autre. d2 disjoint(O1, O2, Dims) --> exists(D in Dims, precedes(O1, O2, D) or preceded_by(O1, O2, D)). o1 o2 d1 8.2.3 - Règles de placement 89 overlap/3 : deux orthotopes se chevauchent ou s’enchevêtrent si leurs projections se chevauchent dans toutes les dimensions. d2 overlap(O1, O2, Dims) --> forall(D in Dims, overlaps_sym(O1, O2, D)). o1 o2 8.2.3 d1 Règles de placement Ces relations génériques entre orthotopes sont utilisées dans PKML pour définir des règles de placement génériques pour les problèmes Bin Packing comme suit : non_overlapping_bin(Items, Dims) --> forall(O1 in Items, forall(O2 in Items, O1:uid < O2:uid implies not overlap(O1, O2, Dims))). containmentAE(Items, Bins, Dims) --> forall(I in Items, exists(B in Bins, rcc::contains_meets(B, I, Dims))). Les règles définissent respectivement le non-chevauchement d’un ensemble d’objets sur un nombre de dimensions donné, et l’inclusion de tous les objets dans le contenant. bin_packing_binary(Items, Bins, Dims) --> containmentAE(Items, Bins, Dims) and non_overlapping_bin(Items, Dims) and labeling(Items). La règle qui définit le problème Bin Packing demande, pour un ensemble d’objets et de contenants définis sur un nombre de dimension donnés, s’il existe une solution à la conjonction des deux contraintes précédentes. Dans un souci d’efficacité, la contrainte géométrique geost est préférée en pratique à la version binaire de la contrainte de non-chevauchement que l’on vient de présenter. Le bibliothèque définit aussi des relations dans l’espace naturel à trois dimensions. on_top/2 : une boîte o1 est sur une boîte o2 si les deux boîtes se chevauchent sur le plan du « sol » (sur le plan décrit pas les axes des deux premières dimensions) et o1 est touchée par o2 dans la dimension « verticale » (la troisième dimension). on_top(O1, O2) --> overlap(O1, O2, [1, 2]) and met_by(O1, O2, 3). Règles de placement spécifiques 90 above/2 : une boîte o1 est au-dessus d’une boîte o2 si leurs projections au « sol » se chevauchent et si la projection de o1 dans la dimension « verticale» est précédée par o2 ou si o1 est touchée par o2 dans cette même dimension. above(O1, O2) --> overlap(O1, O2, [1, 2]) and (preceded_by(O1, O2, 3) or met_by(O1, O2, 3)). 8.3 Règles de placement spécifiques Les règles métier de placement existent et sont définies dans PKML pour prendre compte des règles de bon sens ou les spécifications industrielles qui dépassent la portée des problèmes Bin Packing purs [CD85]. 8.3.1 Règles relatives au poids Par exemple, les règles suivantes expriment des contraintes de poids pour un placement admissible en logistique. Les orthotopes considérés sont munis de deux propriétés supplémentaires : un poids weight et un facteur piling, dont le produit avec weight exprime le poids qu’un objet peut supporter (sa gerbabilité). stack_weight/1 : Quelque soit la paire d’objets (o1 , o2 ) ∈ Items × Items, si o1 est au-dessus de o2 alors o1 est plus léger que o2 . stack_weight(Items) --> forall(O1 in Items, forall(O2 in Items, above(O1, O2) implies lighter(O1, O2))). 20Kg 40Kg 50Kg 50Kg 40Kg 20Kg 8.3.2 - Règles relatives aux longueurs et aux surfaces 91 stack_weight_sum/1 : Pour tout o1 ∈ Items, la somme des poids des objets de l’ensemble {o2 | above(o2 , o1 ), o2 ∈ Items} est inférieure à la capacité de support de o1 . stack_weight_sum(Items) --> forall(O1 in Items, sum(map(O2 in Items, above(O2, O1) * O2:weight)) =< O1:piling * O1:weight). 0 ∗ 20 Kg 1 ∗ 20 Kg 1 ∗ 50 Kg 0 ∗ 20 Kg 1 ∗ 40 Kg 1 ∗ 50 Kg weight_balancing/4 : Soit L = {o | stand_in_first_half(o,bin,d), o ∈ Items} l’ensemble des objets de Items dont les projections dans la première moitié (ou partie « gauche ») de la projection dans la dimension d du contenant bin. Soit R = {o | stand_in_second_half(o,bin,d), o ∈ Items} l’ensemble des objets inclus dans la seconde moitié (ou partie « droite »). Soit wL et wR les sommes des poids des objets dans la partie gauche et dans la partie droite, respectivement. La règle énonce que le rapport de poids max(wL , wR )/min(wL , wR ) ne doit pas excéder un certain entier naturel ratio. stand_in_first_half(Item, Bin, D) --> end(Item, D) =< size(Bin, D) / 2. stand_in_second_half(Item, Bin, D) --> origin(Item, D) >= size(Bin, D) / 2. weight_balancing(Items, Bin, D, Ratio) --> let(WL = sum(map(O in Items, O:weight * stand_in_first_half(O, Bin, D))), WR = sum(map(O in Items, O:weight * stand_in_second_half(O, Bin, D))), 100 * max(WL, WR) =< (100 + Ratio) * min(WL, WR)). wL 8.3.2 ratio max(wL , wR ) ≤ 1+ min(wL , wR ) 100 wR Règles relatives aux longueurs et aux surfaces Les règles suivantes expriment des contraintes de tailles et de surfaces d’objet appartenant à même une pile. 92 Règles de placement spécifiques stack_area/1 : Quelque soit la paire d’objets (o1 , o2 ) ∈ Items × Items, si o1 repose sur o2 alors la base de o1 (sa surface dans le plan décrit par les axes des deux premières dimensions) est supérieure à la base de o2 . stack_area(Items) --> forall(O1 in Items, forall(O2 in Items, on_top(O1, O2) implies larger_area(O1, O2, 1, 2))). stack_height/1 : Quelque soit la paire d’objets (o1 , o2 ) ∈ Items × Items, si o1 repose sur o2 alors la hauteur o1 (sa taille dans la troisième dimension) est supérieure à la hauteur de o2 . stack_height(Items) --> forall(O1 in Items, forall(O2 in Items, on_top(O1, O2) implies larger_size(O1, O2, 3))). stack_oversize/2 : Quelque soit la paire d’objets (o1 , o2 ) ∈ Items × Items, si leurs projections au sol (plan décrit par les axes des deux premières dimensions) se chevauchent alors la différence (en valeur absolue) entre leurs origines et la différence entre leurs fins dans les deux premières dimensions ne doivent pas être strictement supérieures à l’entier naturel length. stack_oversize(Items, Length) --> forall(O1 in Items, forall(O2 in Items, overlap(O1, O2, [1,2]) implies forall(D in [1, 2], oversize(O1, O2, D) =< Length))). 8.3.2 - Règles relatives aux longueurs et aux surfaces 93 stack_support_area/2 : Pour tout objet o1 , ou bien il repose au sol (sa coordonnée dans la troisième dimension vaut 0) ; ou bien sa base est recouverte au moins à la hauteur de l’entier naturel percent par l’ensemble des objets sur lesquels o1 repose. stack_support_area(Items, Percent) --> forall(O1 in Items, origin(O1, 3) = 0 or sum(map(O2 in Items, on_top(O1, O2) * overlap_area(O1, O2, 1, 2))) >= (area(O1, 1, 2) * Percent) / 100). stack_alignment/1 : Quelque soit la paire d’objets (o1 , o2 ) ∈ Items × Items, si les projections au sol de o1 et o2 se chevauchent, alors o1 et o2 sont alignés à l’origine ou alignés à la fin dans la première dimension (par convention la plus longue). stack_alignment(Items) --> forall(01 in Items, forall(02 in Items, overlap(01, 02, [1, 2]) implies origin(01, 1) = origin(02, 1) or end(01, 1) = end(02, 1))). Dans le contexte de la bibliothèque PKML, la proposition 4 a pour conséquence : Proposition 6. Les modèles PKML contenant des listes d’au plus l éléments génèrent des programmes de contraintes de taille O(l4 ) en présence à la fois de d’alternatives de formes et d’assemblages de boîtes, O(l3 ) en présence d’une des deux caractéristiques uniquement, et O(l2 ) en présence de boîtes simples. 94 Règles de placement spécifiques Chapitre 9 Modèles et résolution de problèmes de placement Sommaire 9.1 Problème de placement optimal de carrés dans un rectangle 96 9.1.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96 9.1.2 Modèle et évaluation . . . . . . . . . . . . . . . . . . . . . . . 96 9.2 Problème de chargement de palette . . . . . . . . . . . . . . 101 9.2.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101 9.2.2 Modèle et évaluation . . . . . . . . . . . . . . . . . . . . . . . 101 9.3 Problème de chargement de container . . . . . . . . . . . . . 105 9.3.1 Définition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 9.3.2 Modèle et évaluation . . . . . . . . . . . . . . . . . . . . . . . 106 Ce chapitre présente trois problèmes de placement académique et issus de l’industrie de difficultés croissantes. Ces problèmes illustrent l’expressivité et l’efficacité du langage ainsi que sa capacité à exprimer de façon déclarative les stratégies de branchement et les heuristiques d’ordonnancement. Ce trait original de Cream nous a permis de construire et d’expérimenter rapidement des heuristiques menant à la résolution effective de problèmes de placement réels issus de l’industrie (cf. Sec. 9.3). Cela avant le terme du projet européen NetWMS, ce qui ne fût pas le cas des autres partenaires académiques qui travaillaient avec SICStus-Prolog et Choco-Java. Dans les modèles qui suivent, la syntaxe des fold est légèrement différente que celle présentée jusque là. En particulier, les combinateurs de listes sont de la forme plus standard foldl(X in l, p, i, e) où – X est une variable liée dans e prenant à chaque itération comme valeur l’élément suivant de l ; – p est un symbole de prédicat binaire défini par ailleurs et qui attend en premier argument (fold left) l’accumulateur et en second argument l’expression e[X] ; – et i est l’élément initial pour l’accumulateur. 95 Problème de placement optimal de carrés dans un rectangle 96 9.1 Problème de placement optimal de carrés dans un rectangle 9.1.1 Définition Le problème de placement optimal de carrés dans un rectangle (Optimal Rectangle Packing) [Kor04] est un problème académique qui consiste à trouver le plus petit rectangle (qui ne soit pas un carré) contenant n carrés de tailles si = i, 1 ≤ i ≤ n. 9.1.2 Modèle et évaluation Le modèle Cream est basé sur la modélisation et la procédure de recherche de Simonis et O’Sullivan (S&O) [SO08], une approche par décomposition, et implanté en SICStus-Prolog. Les résultats du programme SICStus-Prolog de S&O ont été confrontés au benchmark de Korf [Kor04] et ont améliorés les meilleurs temps de résolution connus de facteurs compris entre 100 et 300. Nous comparons à la fin de cette section les performances du programme de S&O avec celles du programme généré à partir du modèle Cream. L’idée de la stratégie de branchement est d’énumérer les rectangles candidats par ordre d’aires ascendant et, pour chaque rectangle, tenter d’y placer les carrés. Cette stratégie est dynamique au sens où les problèmes de placement des n carrés successifs dépendent de l’instanciation donnée par l’énumération des rectangles candidats. Par construction, au premier placement réussit correspond le plus petit rectangle englobant. L’efficacité du modèle réside dans la décomposition du problème et dans l’association des contraintes globales de non-enchevêtrement bi-dimensionnel (disjoint2/3) et de cumul unidimensionnel (cumulative/4) avec une recherche basée sur le découpage par intervalles ; découpage des domaines de toutes les coordonnées dans la première dimension d’abord puis découpage dans la seconde. Imports et données import(’Cream/lib/pkml/pkml’). import(’Cream/lib/search/domain_splitting’). % Constructeur du rectangle. % Il porte sa propre aire pour faciliter l’énumération. make_object_shape_area(S, OL, A) = {shapes:[S], shape_index:1, origin:OL, area:A}. bin = make_object_shape_area(make_shape_size([_, _]), [1, 1], _). % Constructeur de formes carrées. % Les carrés sont ordonnés du plus grand au plus petit. square(S) = make_shape_size([S, S]). squares(N) = map(Size in reverse([2 .. N]), square(Size)). % Constructeur des carré à placer. % Les objets à placer sont des carrés munis de coordonnées. item(S) = make_object_shape(S, [_, _]). items(N) = map(S in squares(N), item(S)). 9.1.2 - Modèle et évaluation 97 % Taille des objets dans chaque dimension. w(O) = size(O, 1). h(O) = size(O, 2). Le but Le but du modèle montre la décomposition du problème : énumération de rectangles englobants (solve_bin_subproblem/6) et résolution du problème de placement des N carrés (solve_items_subproblem/3). % % % % % % % ? N : taille de l’instance Items : liste des carrés à placer Width : largeur du rectangle (longueur dans la première dimension) Height : hauteur du rectangle (longueur dans la seconde dimension) Area : aire du rectangle LB : borne inférieur de la solution optimale UB : borne supérieure de la solution optimale let(N := 25, Items := items(N), Width := size(bin, 1), Height := size(bin, 2), Area := bin:area, LB := sum(map(I in Items, area(I, [1, 2]))) + 1, UB := LB + 200, solve_bin_subproblem(Width, Height, Area, LB, UB, N) and solve_items_subproblem(Items, Width, Height)). (1) Énumération des rectangles Il s’agit de poser les domaines des variables représentant largeur (W) et hauteur (H) des rectangles candidats, puis de poser les contraintes sur ces variables. Ces contraintes mettent en relation largeur, hauteur et aire (A) ; imposent que la hauteur soit supérieure à la largeur ; et considèrent les plus grands carrés qui ne peuvent pas être empilés les uns sur les autres et qui doivent donc tenir dans la dimension longue. bin_domain(W, H, A, L, U, N) --> domain([W, H], N, L) and domain([A], L, U). bin_constraints(W, H, A, N) --> let(K := (W + 1)/2, A = W * H and H >= W and (W >= 2 * N - 1 or H >= (N * N + N - (K - 1) * (K - 1) - (K - 1)) / 2)). La stratégie de branchement énumère d’abord les aires puis la largeur dans l’ordre croissant. bin_search --> variable_ordering([is(^:area), is(w(^)), is(h(^))]) and labeling(bin). 98 Problème de placement optimal de carrés dans un rectangle (2) Placement des carrés De la même façon, le sous-problème de placement des carrés pose les domaines des coordonnées, puis les contraintes et enfin définit la stratégie de branchement. Ici, (lower_quadrant/3) brise une symétrie du problème en imposant que le plus grand carré soit nécessairement placé dans le quart inférieur gauche du rectangle candidat. items_domain(Items, W, H) --> forall(It in Items, let(X := x(It), Y := y(It), S := w(It), domain(X, 1, W - S + 1) and domain(Y, 1, H - S + 1) )) and lower_quadrant(Items, W, H). % Bris de symétrie : le premier carré est placé nécessairement % dans le quart inférieur gauche du rectangle englobant. lower_quadrant(Items, W, H) --> let(FstIt := nth(1, Items), X := x(FstIt), Y := y(FstIt), S := w(FstIt), domain(X, 1, (W - S + 2) / 2) and domain(Y, 1, (H + 1) / 2)). % Contraintes globales portant sur les carrés. % disjoint2/3 : non-envechêtrement bi-dimensionnel % cumulative/4 : plafonnement du cumul des carrés dans chaque dimension items_constraints(Items, W, H) --> let(Xs := map(It in Items, x(It)), Ys := map(It in Items, y(It)), Ss := map(It in Items, w(It)), disjoint2(Xs, Ys, Ss) and cumulative(Xs, Ss, Ss, H) and cumulative(Ys, Ss, Ss, W)). La stratégie de branchement procède par découpage d’intervalles dont les primitives interval_split/3 et dichotomic_split/1 sont définies dans la bibliothèque. L’application et la combinaison des ces primitives se présentent ainsi : 1. pour tous les carrés, découper le domaine des coordonnées dans la première dimension, sauf les 6 plus petits ; puis, pour tous les carrés, fixer les coordonnées dans la même dimension ; 2. pour tous les carrés, découper le domaine des coordonnées dans la seconde dimension ; puis, pour tous les carrés, fixer les coordonnées dans la même dimension. Le découpage s’effectue par intervalles de longueur le tiers du carré considéré et selon des origines ascendantes. Les coordonnées sont ensuite fixées par une recherche dichotomique standard sur leur domaine. 9.1.2 - Modèle et évaluation 99 % XSs : liste de couples (Xi , Si ) % YSs : liste de couples (Yi , Si ) avec % Xi coordonnée du carré i dans la première dimension, % Yi coordonnée du carré i dans la seconde dimension, % Si taille du carré i. items_search(Items, W, H) --> let(XSs := map(It in Items, {coord : x(It), siz : w(It)}), YSs := map(It in Items, {coord : y(It), siz : h(It)}), Min := 1, MaxX := W + 1, MaxY := H + 1, dynamic( search( forall(XS in XSs, XS:siz > 6 implies interval_split(XS:coord, Min, MaxX, max(1, (XS:siz*3)/10))) and forall(XS in XSs, dichotomic_split(XS:coord)) and forall(YS in YSs, interval_split(YS:coord, Min, MaxY, max(1, (YS:siz*3)/10))) and forall(YS in YSs, dichotomic_split(YS:coord))))). Le découpage d’un domaine, initialement [min, max], par intervalles de longueur l comprend n = (max − min)/l coupes. Comme l’illustre la figure 9.1, à chaque étape c ∈ {1, . . . , n} le domaine est restreint à l’intervalle [min + (c − 1) ∗ l, min + c ∗ l]. interval_predicate(List, Ctn) --> let(Var := nth(1, List), Len := nth(2, List), Cut := domain_min(X) + Len, Var =< Cut or (Var > Cut and Ctn)). interval_split(X, Min, Max, L) --> foldr(C in [1 .. (Max - Min) / L], interval_predicate, true, [X, L]). min max I1 l I2 I3 Figure 9.1: Découpage d’un domaine [min, max] par intervalles Ik de longueur l. 100 Problème de placement optimal de carrés dans un rectangle Solution Figure 9.2: Une solution de Optimal Rectangle Packing, n = 22. La table 9.1 compare les temps de résolution du modèle original avec le code généré Cream. On constate que le code généré est plus lent à résoudre d’un facteur 2 pour la plupart des instances mais qui tend à diminuer avec la taille des instances. n Compilation 18 19 20 21 22 23 24 25 0.650 0.700 0.780 0.810 0.870 0.930 0.980 1.060 Résolution Cream Réf. 17 9 17 8 30 17 100 63 430 297 2700 1939 3900 2887 27020 20713 Table 9.1: Temps d’exécution pour Optimal Rectangle Packing, en secondes (Linux / Intel Core2 Quad CPU, 2.83GHz). Conclusion Nous avons présenté un modèle Cream pour le problème de placement optimal de carrés dans un rectangle construit sur la base de la proposition de S&O. Cream permet de formuler concisément ce problème de nature dynamique et de le résoudre avec un surcoût limité vis-à-vis du programme de référence. C’est un modèle qui ne pourrait être formulé dans les langages de modélisation qui adoptent une compilation statique comme Zinc ou Essence. La stratégie de branchement est exprimée dans le modèle Cream grâce à deux déclarations de règles définissant des recherches par découpage d’intervalles. Le temps de compilation du modèle Cream 9.2.2 - Modèle et évaluation 101 ne dépasse pas 1 seconde et les temps d’exécution pour les différentes instances du problème tendent à un surcoût raisonnable d’environ 25% par rapport au programme SICStus-Prolog de S&O. 9.2 Problème de chargement de palette 9.2.1 Définition Le problème de chargement de pallettes (Pallet Loading) est un problème industriel de placement tri-dimensionnel qui consiste à placer un ensemble de cartons sur une pallette. Les cartons sont des parrallèlépidèdes rectangles ou cuboïdes, ainsi que l’espace de placement dont la base est une palette. Les cartons peuvent subir une rotation d’un quart de tour autour de l’axe orthogonal à la palette. Par rapport au problème de placement précédent, trois niveaux de difficulté supplémentaires sont introduits : – c’est un problème en 3 dimensions, en fait ce premier problème se réduit en un problème de placement 2+1D, c’est-à-dire par couches d’objets, chaque couche étant solution d’un problème en 2 dimensions ; – les objets peuvent subir des rotations, ce ne sont pas des carrés ; – une nouvelle contrainte sur la surface de la base des boîtes composant chaque couche est ajoutée. Les palettes sont remplies de préférence avec un ensemble d’objets homogène en terme de référence, idéalement avec une voire deux références différentes. Les solutions doivent respecter les contraintes suivantes : – Si plusieurs références de cartons sont à placer sur une palette, les objets de plus grande base doivent être placés sur les objets de base plus petite. Chaque palette est destinée à être placée à son tour dans un container (cf. section suivante : Problème Container Loading). 9.2.2 Modèle et évaluation Imports et données Une instance du problème Pallet Loading implique typiquement 3 modules qui définissent : – les données propres à l’instance (définitions du contenant et des contenus), – la stratégie de branchement utilisée pour la résolution, – et le modèle commun à toutes les instance de Pallet Loading avec placement « par couches ». import(’Cream/examples/packing/pallet_loading_1.data’). import(’Cream/examples/packing/pallet_loading_1.strategy’). import(’Cream/examples/packing/pallet_loading_layer.model’). Problème de chargement de palette 102 Le module concernant les données du problème définit : – la pallette : ses dimensions réelles et réduites ; – pour chaque référence de carton : les dimensions réelles et réduites ainsi que la cardinalité. % Les dimensions du problème dimensions = [1, 2, 3]. % Réduction des longueurs par dimension % selon le plus grand commun diviseur canonical_size(RefSize) = map(D in dimensions, nth(D, RefSize) / gcd(D)). % Définition de la palette pf82ref_size = [2240, 2240, 992]. pf82_size = canonical_size(pf82ref_size). bin_data = {ref_size = pf82ref_size, size = pf82_size}. % Définitions des cartons c11ref_size = [600, 400, 200]. c11_size = canonical_size(c11ref_size). items_c11 = {ref_size = c11ref_size, size = c11_size, card = 80}. items_data = [items_c11]. Le modèle importe la bibliothèque PKML afin de profiter des définitions de constructeurs, fonctions et autres règles impliquées dans les problèmes de placement. Afin de profiter d’une éventuelle factorisation des longueurs dans une instance de problème, les longueurs d’une référence de carton i dans la dimension d sont divisées par le plus grand commun diviseur de toutes les longueurs potentiellement impliquées 1 dans d. La taille réduite d’une référence i dans la dimension d est donc cdi = rid /pgcd(Rd ), où Rd est l’ensemble des longueurs impliquées dans la dimension d. import(’Cream/lib/pkml/pkml’). gcd(I) = _. % Dans chaque dimension d, les longueurs des objets du problème % sont divisées par leur plus grand commun diviseur pgcd(d). reduced_sizes(BinRefSize, ItemsRefSize, Dimensions) --> let(BinWidthRef := nth(1, BinRefSize), BinHeightRef := nth(2, BinRefSize), BinLengthRef := nth(3, BinRefSize), ItemsWidthRef := map(IRS in ItemsRefSize, nth(1, IRS)), ItemsHeightRef := map(IRS in ItemsRefSize, nth(2, IRS)), ItemsLengthRef := map(IRS in ItemsRefSize, nth(3, IRS)), gcd([BinWidthRef] ++ ItemsWidthRef ++ ItemsHeightRef, gcd(1)) and gcd([BinHeightRef] ++ ItemsWidthRef ++ ItemsHeightRef, gcd(2)) and gcd([BinLengthRef] ++ ItemsLengthRef, gcd(3))). 1. potentiellement car les objets sont polymorphiques (une rotation autorisée). 9.2.2 - Modèle et évaluation 103 Le but Le but demande simplement de placer les cartons sur la pallette après avoir si possible réduit les longueurs. ? dynamic( let(Dimensions := dimensions, BinData := bin_data, BinRefSize := BinData:ref_size, ItemsData := items_data, ItemsRefSize := map(ItemsData in ItemsData, ItemsData:ref_size), reduced_sizes(BinRefSize, ItemsRefSize, Dimensions) and pallet_loading(BinData, ItemsData, Dimensions))). Les contraintes Placer les cartons sur la pallette, c’est les faire tenir dans l’espace de la pallette (containmentAE/3) sans qu’ils ne s’enchevêtrent (non_overlapping/2). Cette contrainte est définie par la contrainte globale geost. Les contraintes containmentAE/3 et domains/3 ne peuvent être confondues car les objets sont polymorphes. Les domaines doivent être décidés statiquement, donc domain/3 ne peut que considérer qu’une sous-approximation des longueurs dans les dimensions qui sont permutées par la rotation ; et containmentAE/3 s’assure grâce à des contraintes réifiées, une fois l’orientation décidée, que les objets tiennent bien dans la pallette. De plus, puisque les cartons d’une même références forment une classe d’équivalence pour le placement (ils sont rigoureusement identiques et sont placés, pour chaque coordonnées, des plus petites valeurs de domaines aux plus grandes), la contrainte globale lexicographic/1 permet de briser la symétrie de permutation des cartons d’une même référence. Enfin, on demande (stack_area/1) que les cartons de plus grande base soient empilés sur les cartons de plus petite base. % Bris de symétrie : pour tout ensemble d’objets de même référence K, % les vecteurs coordonnées {(xi , yi , zi ) | i ∈ {1, . . . , card(K)}} respectent % l’ordre lexicographique. lexicographical(ListOfItemsGroups, Dims) --> forall(Items in ListOfItemsGroups, lexicographic(map(I in Items, map(D in Dims, nth(D, I:origin))))). pallet_loading_constraints(Items, ItemsReferences, Bin, BinSize, Dims) --> domains(Items, BinSize, Dims) and containmentAE(Items, [Bin], Dims) and non_overlapping(Items, Dims) and lexicographical(ItemsReferences, [3, 1, 2])and stack_area(Items). Problème de chargement de palette 104 La stratégie de recherche Les cartons sont placés par couches de même référence load_layers/5 et l’heuristique demande de placer les références dans l’ordre croissant de leur base. h_layer = conjunctive(least(nth(1, ItemsSize) * nth(2, ItemsSize)) for load_layers(_, _, _, ItemsSize, _)). La stratégie de branchement est basée sur un placement par couches : calculer le nombre de couches par référence et placer d’abord les références dont la base est la plus petite. pallet_loading_search(ItemsData, ItemsReferences, Bin, BinSize, Dims) --> layers_card(ItemsData, BinSize) and search(h_layer, forall(ID in [1 .. length(ItemsData)], let(ItemData := nth(ID, ItemsData), Items := nth(ID, ItemsReferences), ItemsCard := ItemData:card, ItemsSize := ItemData:size, LayersCard := layers_card(ID), ItemsPerLayerCard := ItemsCard / LayersCard, load_layers(LayersCard, ItemsPerLayerCard, Items, ItemsSize, BinSize)))). La stratégie de branchement détermine d’abord pour tous les cartons leur coordonnée sur l’axe vertical (orthogonal à la pallette) avec la plus petite valeur disponible. import(’Cream/lib/search/domain_splitting’). search_strategy(Items, Dimensions, UpperBounds, IntervalLengths) --> forall(I in Items, origin(I, 3) = domain_min(origin(I, 3))) and interval_dichotomic_split_interleaved(Items, Dimensions, UpperBounds, IntervalLengths). Ensuite, il s’agit de résoudre un problème de placement bi-dimensionnel par une recherche par découpage d’intervalles semblable à celle utilisée pout Optimal Rectangle Packing (cf. section 9.1) interval_dichotomic_split_interleaved(Items, Dimensions, UpperBoundsList, IntervalLengthList) --> forall(I in Items, forall(D in Dimensions, let(UpperBound := nth(D, UpperBoundsList), IntervalLength := nth(D, IntervalLengthList), OriginID := origin(I, D), interval_split(OriginID, 0, UpperBound, IntervalLength) and dichotomic_split(OriginID)))). 9.3.1 - Définition 105 Solutions Les figures 9.3 et 9.4, issues de CLPGUI [FSC04], montrent respectivement une solution d’une instance de Pallet Loading impliquant 80 objets d’une même référence (i.e., de même caractéristiques), et une solution d’une instance impliquant 40 objets et deux références. Figure 9.3: Une solution de Pallet Loading ; 80 objets polymorphes, 1 référence. Figure 9.4: Une solution de Pallet Loading ; 40 objets polymorphes, 2 références. Conclusion Le problème 2+1D de chargement de palettes monoréférences a pu être résolu car le jeu de boîtes à placer permet une division en couches, mais ce n’est pas le cas de tous les problèmes de chargement de pallettes [ACC+ 10]. En effet, certaines instances présentent une grande hétérogénéité en terme de références d’objets à placer ce qui empêche de trouver une structure exploitable par une heuristique et nécessite d’explorer la combinatoire de toutes les rotations. De plus, la contrainte globale geost effectue une propagation trop faible dans le contexte de problèmes avec objets polymorphiques. En particulier geost n’inclut pas de raisonnement sur les volumes des objets à placer. 9.3 9.3.1 Problème de chargement de container Définition Le problème de chargement de container (Container Loading) est un problème industriel de placement tri-dimensionnel, non réductible à un problème 2+1D, qui consiste à placer un ensemble de palettes dans un container qui pourra être destiné au transport routier, ferroviaire ou maritime. Les palettes peuvent subir une rotation d’un quart de tour autour de l’axe orthogonal à la base du container. De plus, les palettes sont munies d’un attribut de poids et de facteur de gerbabilité dont le produit indique la capacité d’un objet à supporter du poids. Problème de chargement de container 106 Enfin, les solutions doivent respecter 7 contraintes spécifiques portant sur la stabilité et la répartition du poids localement à une pile et globalement dans le container. 9.3.2 Modèle et évaluation Imports et données Une instance de Container Loading se découpe en trois modules : les données, la stratégie de recherche et le modèle à proprement parler. import(’Cream/examples/packing/container_loading_1.data’). import(’Cream/examples/packing/container_loading_1.strategy’). import(’Cream/examples/packing/container_loading.model’). Les données brutes sont transformées pour produire des enregistrements du type qui suit. Chaque référence d’objet a une certaine taille (réduite si possible), un facteur de gerbabilité, un poids et une cardinalité. % taille originale one_ref_size = [224, 170, 121]. % taille réduite one_size = canonical_size(one_ref_size). % propriétés de la référence "one" items_one = {ref_size = one_ref_size, size = one_size, piling = 1, weight = 220, card = 4}. (...) % ensemble des références de l’instance items_data = [items_one, items_two, ..., items_n]. Le but Avant de résoudre le problème Container Loading on essaye de réduire les tailles de l’instance si possible (reduced_sizes/3), puis on pose les contraintes et on lance la recherche (container_loading/3). % % % ? Dims : nombre de dimensions du problème BinData : données du contenant ItemsData : données des références à placer dynamic( let(Dims := dimensions, BinData := bin_data, BinRefSize := BinData:ref_size, ItemsData := items_data, ItemsRefSize := map(ItemsData in ItemsData, ItemsData:ref_size), reduced_sizes(BinRefSize, ItemsRefSize, Dims) and container_loading(BinData, ItemsData, Dims))). 9.3.2 - Modèle et évaluation 107 Les contraintes Après les domaines des variables, les contraintes génériques correspondant au problème pur de Bin Packing sont posées. Elles comprennent la contrainte globale geost. À ces contraintes génériques sont ajoutées un ensemble de contraintes spécifiques au problème container loading dont une grande partie a été décrite dans la section 8.3. Elles assurent que dans toute solution : – les objets sont stables et ne glissent pas, c’est-à-dire que chaque objet ait un support dans toutes les dimensions (gravity(Items) et blocked(Items)) ; – les objets de plus petite hauteur sont placés sous les autres (stack_height(Items)) ; – les objets de plus petite base sont placés sous les autres (stack_area(Items)) ; – les contours des piles sont «lisses» (stack_alignment(Items)) ; – les objets sont supportés sur 100% de la surface de leur base (stack_support_area(Items, 100)) ; – les objets n’ont pas à supporter plus que leur gerbabilité (stack_weight_sum(Items)) – et enfin que le poids des objets soit bien réparti globalement dans la longueur de contenant (weight_balancing(Items, Bin, 1, 10)) . import(’Cream/lib/pkml/pkml’). container_loading_constraints(Items, Bin, BinSize, Dims) --> domains(Items, BinSize, Dims) and containmentAE(Items, [Bin], Dims) and non_overlapping(Items, Dims) and gravity(Items) and blocked(Items) and stack_height(Items) and stack_area(Items) and stack_alignment(Items) and stack_support_area(Items, 100) and stack_weight_sum(Items) and weight_balancing(Items, Bin, 1, 10). La stratégie de recherche Après avoir décider des orientations, la stratégie de branchement se décompose en deux : 1. placer grossièrement tous les objets (place_item_coarsely/2) par découpage de leur domaine ; 2. décider précisément du placement (place_item/1). L’heuristique principale (l’heuristique sur les conjonctions), dissociée de la stratégie de branchement, choisit de placer d’abord les objets les moins lourds et les moins capable de supporter (de gerbabilité minimale). Problème de chargement de container 108 h_cont_load = conjunctive([ least(Item:piling * Item:weight) for place_item_coarsely(Item, _), least(Item:piling * Item:weight) for place_item(Item), least(domain_size(Var)) for dichotomic_split(Var)]). (...) search(h_cont_load, forall(Item in Items, place_item_coarsely(Item, BinSize)) and forall(Item in Items, place_item(Item))). place_item_coarsely(Item, BinSize) --> interval_split(Item, 2, 0, nth(2, BinSize), size(Item, 2) - 1) and interval_split_out_middle(Item, 1, 0, nth(1, BinSize), size(Item, 1) / 2) and interval_split_reverse(Item, 3, 0, nth(3, BinSize), size(Item, 3) - 1). place_item(Item) --> forall(D in dimensions, dichotomic_split(origin(Item, D))). Puisque l’heuristique choisit de placer d’abord les objets les moins lourds et les moins capable de supporter, il s’agit de les placer de haut en bas, grâce à la stratégie suivante appliquée sur la dimension de l’axe vertical. interval_split_reverse/5 : découpe le domaine de la coordonnée de l’objet item dans la dimension dim par intervalles de longueur len, de la borne supérieure max vers la borne inférieure min, et en prenant en compte la taille de l’objet s. interval_reverse_predicate(Rec, Ctn) --> let(Var := Rec:var, Len := Rec:len, Cut := domain_max(Var) - Len, Var >= Cut or (Var < Cut and Ctn)). interval_split_reverse(Item, Dim, Min, Max, Len) --> foldr(C in [1 .. ((Max - size(Item, Dim)) - Min) / Len], interval_reverse_predicate, true, {var = end(Item, Dim), len = Len}). min max I1 s I2 len I3 Figure 9.5: Découpage d’un domaine [min, max] par intervalles Ii de longueur len de la borne supérieure vers la borne inférieure. 9.3.2 - Modèle et évaluation 109 Pour ce qui est de la première dimension, la dimension des plus grandes longueurs, la stratégie consiste à ventiler les objets aux extrémités et finir le placement au centre du contenant, grâce à la règle suivante. interval_split_out_middle/5 : Le découpage d’un domaine, initialement [min, max], d’un objet de taille s des bornes vers le milieu et par intervalles de longueur len comprend n = ((max − s) − min)/(2 ∗ len) coupes. Comme l’illustre la figure 9.6, à chaque étape c ∈ {0, . . . , n − 1} le domaine est restreint à l’intervalle [min + (c − 1) ∗ len, (min + c ∗ len) − s]. interval_split_out_middle(Item, Dim, Min, Max, Len) --> exists(I in [0 .. ((Max - size(Item, Dim))- Min) / (2 * Len))], let(LB := I * Len, UB := (I + 1) * Len, domain(origin(Item, Dim), Min + LB, Min + UB) or domain(end(Item, Dim), Max - UB, Max - LB))). min I1 mid max I2 len I3 s I4 I5 , I6 Figure 9.6: Découpage d’un domaine [min, max] par intervalles Ii de longueur l des bornes du domaine vers milieu mid, en considérant la taille d’objet s. Solution La figure 9.7, montre une solution d’une instance de Container Loading. L’exécution du programme généré SICStus-Prolog, sur une machine Linux dotée de procésseurs Intel Core2 Quad CPU cadencé à 2.83GHz, résout le problème en 8 secondes et 489 backtracks. Conclusion Le problème de placement en 3 dimensions avec rotations et contraintes de stabilité et de poids a été résolu en Cream dans un temps raisonnable grâce à la combinaison des contraintes réifiées à la contrainte globale geost, qui a pris en charge les contraintes 110 Problème de chargement de container Figure 9.7: Une solution de Container Loading ; 19 objets polymorphes, 5 références. Plus un objet est lourd et peut supporter de poids, plus il est rouge foncé. Inversement, moins un objet est lourd et peut supporter de poids, plus il est vert clair. de non-enchevêtrement, et à une heuristique de recherche performante. Il s’avère que l’heuristique présentée adopte une stratégie similaire à celle utilisée par les ingénieurs spécialisés en colisage et qu’elle s’exprime en Cream dans des termes intuitifs pour ces derniers. Parmi les partenaires du projet Net-WMS dont est issu ce problème de chargement de palettes, nous avons été les seuls à résoudre ce problème en prenant en compte toutes les contraintes. Cela a été possible d’une part grâce à la bibliothèque PKML et d’autre part grâce à la rapidité d’expérimentation des heuristiques exprimées par pattern-matching sur les têtes de règles qu’offre Cream. Cependant, la stratégie présentée n’est pas suffisamment robuste pour résoudre toutes les instances de cette classe de problème. Pour ces problèmes avec rotations en effet, deux difficultés se combinent : d’une part l’algorithme de filtrage de la contrainte geost n’est actuellement pas suffisant pour propager correctement les contraintes d’occupation volumique en présence de rotations, et d’autre part la stratégie de placement nécessite d’effectuer les choix d’orientation des objets trop tôt et n’est pas robuste pour cette raison. 111 112 Problème de chargement de container Chapitre 10 Conclusion Cream est un langage de modélisation à base de règles pour la programmation par contraintes. Il a été conçu pour permettre à l’homme de métier profane en PPC et qui n’est pas nécessairement programmeur d’exprimer ses connaissances et des exigences industrielles sur des problèmes d’optimisation combinatoire grâce à la définition de règles. Les règles Cream sont déclaratives et compositionnelles, ce qui autorise l’énoncé d’une connaissance complexe par fragments. Un point original de Cream réside dans le fait que non seulement les arbres mais aussi les heuristiques d’ordonnancement de la recherche peuvent être spécifiés déclarativement. Dans le langage, une stratégie de branchement est définie par une formule logique du modèle. Les heuristiques qui guident le parcours de l’arbre de recherche induit sont déclaratives, c’est-à-dire dissociées de la stratégie de branchement, compositionnelles et exploitent directement la structure du modèle. Elles sont définies comme des ordres de préférence sur les sous-formules conjonctives et disjonctives, par filtrage des têtes des déclarations de règle. Par opposition aux autres langages de modélisation dans lesquels les stratégies de recherche doivent encore être programmées en s’appuyant sur des listes ou des tableaux et des structures de contrôles, comme dans OPL et Comet par exemple. La transformation des modèles Cream en programmes de contraintes sur domaines finis a été décrite formellement pour deux schémas de compilation complémentaires. Le schéma de compilation fondamental, dit statique, qui procède par expansion des règles du modèle comme dans Zinc par exemple, est formalisé par un système de réécriture de termes défini inductivement et muni d’un mécanisme d’évaluation partielle. Ce schéma produit des programmes de contraintes « plats » optimisés. La confluence et la correction de la transformation vis-à-vis d’une sémantique déclarative sont prouvées, ainsi qu’une borne de complexité sur la taille des programmes générés. De plus, la terminaison de la compilation et la terminaison de l’exécution des modèles Cream sont garanties. L’obtention de tels résultats reflète la simplicité des choix de conception de Cream, comme par exemple l’absence de récursion remplacée par des constructeurs et itérateurs sur listes généraux. Cependant, cette borne de complexité montre une explosion potentielle de la taille des contraintes générées. Dans de tels cas, le schéma de compilation dynamique par génération de procédures, similaire à celui d’OPL et de Comet, peut être appliqué. 113 114 Dans Cream, ce schéma produit des programmes de contraintes structurés incluant de définitions (récursives) de clauses telles qu’un programmeur les auraient écrites dans un langage de programmation logique par contraintes. Cette stratégie engendre un facteur constant de surcoût à l’exécution. Enfin, l’expressivité du langage et l’efficacité des programmes générés ont été évaluées avec succès sur un problème académique et sur des problèmes issus de la logistique dans l’industrie automobile. Les modèles considérés reposent sur une bibliothèque dédiée à la modélisation des problèmes de placement multi-dimensionnel. La bibliothèque inclut les définitions nécessaires à l’expression de problèmes de placement purs de dimension quelconque. Elle offre aussi des définitions relatives à des exigences sur le poids, la surface, le volume, la stabilité, et autres règles de placement spécifiques. L’évaluation du modèle du problème académique Optimal Rectangle Packing permet de constater que les stratégies de recherche dynamiques efficaces s’expriment de façon concise et déclarative. L’évaluation du langage est d’autant plus riche qu’elle a compris des problèmes de placement réels non-purs tri-dimensionnels et impliquant des objets polymorphiques. Elle a montré qu’un langage de modélisation à base de règles donne les moyens d’exprimer et de composer les contraintes spécifiques à la logistique dans l’industrie automobile. De plus, les heuristiques déclaratives ont permis d’expérimenter rapidement et sans peine de nombreuses idées de stratégies jusqu’à résoudre effectivement les problèmes. Cependant, il est nécessaire d’améliorer les stratégies pour les problèmes de placement avec rotations. Perspectives Des travaux complémentaires doivent être accomplis sur le système de module du langage dans la perspective de développer des bibliothèques de modèles réutilisables dans une hiérarchie de modèles et pour des applications spécifiques. Un système de type pour Cream est en cours de développement par Thierry Martinez [MMF10] afin d’aider encore à l’écriture des modèles. Pour le moment la compilation ne garantit pas le bon typage des programmes générés. En particulier, une définition d’objet est implicitement entendue comme un constructeur ou une fonction, or l’appel à une telle définition directement sous un opérateur logique n’est pas une expression valide. D’un autre côté, les définitions de règles sont comprises comme des définitions de prédicats et donc de type booléen. Il serait alors confortable qu’un système de type impose cette sémantique. Par ailleurs, l’approche de spécification d’heuristiques d’ordre par filtrage proposée devrait être applicable à d’autre langages de modélisation qui permettent des définitions, tel que ZINC [dlBMRW06] par exemple. Il serait également intéressant de tenter d’adapter l’expression du contrôle par filtrage sur les membres gauches de règles au langage Prolog pour lequel on recourt toujours à l’écriture d’un méta-interpréteur en l’absence d’un langage d’expression du contrôle. De plus, l’aspect déclaratif du langage de définition d’heuristique de Cream en fait un bon candidat pour un apprentissage automatique. Les critères dynamiques dépendraient alors de profils d’exécution. Enfin, une extension naturelle du langage serait d’intégrer la spécification de procédures de recherche dans le langage, actuellement limitées à la recherche en profondeur CHAPITRE 10. CONCLUSION d’abord et par séparation et évaluation. 115 116 Bibliographie [ACC+ 10] Abder Aggoun, Mats Carlsson, Mickaël Collardey, Francesca Di Lucchio, François Fages, Philippe Gravez, and Renaud Deligny. Industrial prototypes. deliverable D9.1, FP6 Strep project Net-WMS, February 2010. [All91] J. Allen. Time and time again : The many ways to represent time. International Journal of Intelligent System, 6(4), 1991. [AS99] Krzysztof R. Apt and Andrea Schaerf. The alma project, or how firstorder logic can help us in imperative programming. In Correct System Design, pages 89–113, 1999. [AW07] Krzysztof R. Apt and Mark Wallace. Constraint Logic Programming using Eclipse. Cambridge University Press, New York, NY, USA, 2007. [BCP+ 07] N. Beldiceanu, M. Carlsson, E. Poder, R. Sadek, and C. Truchet. A generic geometrical constraint kernel in space and time for handling polymorphic k-dimensional objects. In C. Bessière, editor, Proc. CP’2007, volume 4741 of LNCS, pages 180–194. Springer, 2007. Also available as SICS Technical Report T2007 :08, http://www.sics.se/libindex.html. [BPN01] Ph. Baptiste, C. Le Pape, and W. Nuijten. Constraint-Based Scheduling : Applying Constraint Programming to Scheduling Problems. Kluwer Academic Publishers, 2001. [C+ 07] M. Carlsson et al. SICStus Prolog User’s Manual. Swedish Institute of Computer Science, release 4 edition, 2007. ISBN 91-630-3648-7. [CBM08] Mats Carlsson, Nicolas Beldiceanu, and Julien Martin. A geometric constraint over k-dimensional objects and shapes subject to business rules. In Peter J. Stuckey, editor, Proceedings of CP’08, volume 5202 of LNCS, pages 220–234. Springer, 2008. [CD85] H. Carpenter and W. Dowsland. Practical consideration of the pallet loading problem. Journal of the Operations Research Society, 36 :489– 497, 1985. [CJCM08] François Clautiaux, Antoine Jouglet, Jacques Carlier, and Aziz Moukrim. A new constraint programming approach for the orthogonal packing problem. Comput. Oper. Res., 35(3) :944–959, 2008. [CKPR72] A. Colmerauer, Henry Kanoui, Robert Pasero, and Philippe Roussel. Un système de communication en français, rapport préliminaire de fin de contrat iria, groupe intelligence artificielle. Technical report, Technical Report, Faculté des Sciences de Luminy, Université Aix-Marseille II, 1972. 117 118 BIBLIOGRAPHIE [Col84] A. Colmerauer. Equations and inequations on finite and infinite trees. In Proc. of the International conference on fifth generation computer systems FGCS’84, pages 85–99. ICOT, 1984. [Col87] Alain Colmerauer. Opening the prolog iii universe. BYTE, 12(9) :177– 182, 1987. [Col96] A. Colmerauer. Specification of Prolog IV. Technical report, LIM Technical Report, 1996. [DC01] Daniel Diaz and Philipe Codognet. Design and implementation of the GNU Prolog system. Journal of Functional and Logic Programming, 6, October 2001. [Der79] Nachum Dershowitz. Orderings for term-rewriting systems. In SFCS ’79 : Proceedings of the 20th Annual Symposium on Foundations of Computer Science, pages 123–131. IEEE Computer Society, 1979. [dlBMRW06] Maria Garcia de la Banda, Kim Marriott, Reza Rafeh, and Mark Wallace. The modelling language Zinc. In Proceedings of the International Conference on Principles and Practice of Constraint Programming CP’06), pages 700–705. Springer-Verlag, 2006. [DP85] Rina Dechter and Judea Pearl. Generalized best-first search strategies and the optimality of A*. J. ACM, 32(3) :505–536, July 1985. [FAB+ 07] François Fages, Abder Aggoun, Nicolas Beldiceanu, Mats Carlsson, Filipe Carvalho, Philippe Gravez, András Kovács, and Julien Martin. State-ofthe-art of enabling technologies for packing and planning in future wms. deliverable D3.1, FP6 Strep project Net-WMS, 2007. [FHJ+ 08] Alan M. Frisch, Warwick Harvey, Chris Jefferson, Bernadette MartinezHernandez, and Ian Miguel. Essence : A constraint language for specifying combinatorial problems. Constraints, 13 :268–306, 2008. [FM08] François Fages and Julien Martin. From rules to constraint programs with the Rules2CP modelling language. INRIA Research Report RR6495, Institut National de Recherche en Informatique, April 2008. [FM09] François Fages and Julien Martin. From rules to constraint programs with the Rules2CP modelling language. In Recent Advances in Constraints, Revised Selected Papers of the 13th Annual ERCIM International Workshop on Constraint Solving and Constraint Logic Programming, CSCLP’08, volume 5655 of Lecture Notes in Artificial Intelligence, pages 66–83. Springer-Verlag, 2009. [For82] Charles Forgy. Rete : A fast algorithm for the many pattern/many object pattern match problem. Artificial Intelligences, 19(1) :17–37, 1982. [FSC04] François Fages, Sylvain Soliman, and Rémi Coolen. CLPGUI : a generic graphical user interface for constraint logic programming. Journal of Constraints, Special Issue on User-Interaction in Constraint Satisfaction, 9(4) :241–262, October 2004. [GJM06] Ian P. Gent, Christopher Jefferson, and Ian Miguel. Minion : A fast scalable constraint solver. In Proceedings of the 17th European Conference on Artificial Intelligence, ECAI, pages 98–102, 2006. BIBLIOGRAPHIE 119 [HF06] Rémy Haemmerlé and François Fages. Modules for Prolog revisited. In Proceedings of International Conference on Logic Programming ICLP 2006, number 4079 in Lecture Notes in Computer Science, pages 41–55. Springer-Verlag, 2006. [HF07] Rémy Haemmerlé and François Fages. Abstract critical pairs and confluence of arbitrary binary relations. In Proceedings of th 18th International Conference on Rewriting Techniques and Applications, RTA’07, number 4533 in Lecture Notes in Computer Science. Springer-Verlag, 2007. [HG95] William D. Harvey and Matthew L. Ginsberg. Limited discrepancy search. In IJCAI’95 : Proceedings of the 14th international joint conference on Artificial intelligence, pages 607–613, San Francisco, CA, USA, 1995. Morgan Kaufmann Publishers Inc. [Hof92] Dieter Hofbauer. Termination proofs by multiset path orderings imply primitive recursive derivation lengths. Theoretical Computer Science, 105(1) :129–140, 1992. [HPP00] Pascal Van Hentenryck, Laurent Perron, and Jean-Francois Puget. Search and strategies in opl. ACM Transactions on Compututational Logic, 1(2) :285–320, 2000. [IBM] IBM. IBM ILOG JRules. http ://www.ibm.com/developerworks/websphere/zones/brms/. [ILO] ILOG. ILOG Solver. http ://www.ilog.com/products/solver/. [JBo] JBoss. JBoss Drools. http ://www.jboss.org/drools/documentation.html. [JL87] Joxan Jaffar and Jean-Louis Lassez. Constraint logic programming. In Proceedings of the 14th ACM Symposium on Principles of Programming Languages, Munich, Germany, pages 111–119. ACM, January 1987. [JM94] Joxan Jaffar and Michael J. Maher. Constraint logic programming : a survey. Journal of Logic Programming, 19/20 :503–581, May 1994. [Jun98] Ulrich Junker. Constrained-based problem decomposition for a key configuration problem. In CP, pages 265–279, 1998. [Kor85] Richard E. Korf. Depth-first iterative-deepening : an optimal admissible tree search. Artif. Intell., 27(1) :97–109, 1985. [Kor96] Richard E. Korf. Improved limited discrepancy search. In AAAI/IAAI, Vol. 1, pages 286–291, 1996. [Kor04] Richard E. Korf. Optimal rectangle packing : New results. In ICAPS, pages 142–149, 2004. [Kow74] R. Kowalski. Predicate logic as programming language. In IFIP Congress, pages 569–574, 1974. [Lab00] F. Laburthe. CHOCO : Implementing a CP kernel. In Proceedings of Techniques for Implementing Constraint Programming Systems (TRICS), pages 71–85, 2000. [LD60] A. H. Land and A. G Doig. An automatic method of solving discrete programming problems. Econometrica, 28(3) :497–520, 1960. [Mac77] Alan K. Mackworth. Consistency in networks of relations. Artif. Intell., 8(1) :99–118, 1977. BIBLIOGRAPHIE 120 [MF85] Alan K. Mackworth and Eugene C. Freuder. The complexity of some polynomial network consistency algorithms for constraint satisfaction problems. Artif. Intell., 25(1) :65–74, 1985. [MH05] Laurent Michel and Pascal Van Hentenryck. The comet programming language and system. In CP, pages 881–881, 2005. [MMF09] Julien Martin, Thierry Martinez, and François Fages. On the specification of search tree heuristics by pattern-matching in a rule-based modelling language. In Proceedings of the Eighth International Workshop on Constraint Modelling and Reformulation, associated to CP’09, pages 73–86, 2009. [MMF10] Julien Martin, Thierry Martinez, and François Fages. Static expansion vs procedural code generation in rule-based modeling languages. In preparation, 2010. [NSB+ 07] Nicholas Nethercote, Peter J. Stuckey, Ralph Becket, Sebastian Brand, Gregory J. Duck, and Guido Tack. MiniZinc : Towards a standard CP modelling language. In CP, pages 529–543, 2007. [RCC92] D.A. Randell, Z. Cui, and A.G. Cohn. A spatial logic based on regions and connection. In B. Nebel, C. Rich, and W. R. Swartout, editors, Proc. of 2nd International Conference on Knowledge Representation and reasoning KR’92, pages 165–176. Morgan Kaufmann, 1992. [RdlBMW07] Reza Rafeh, Maria Garcia de la Banda, Kim Marriott, and Mark Wallace. From Zinc to design model. In Proceedings of PADL’07, pages 215–229. Springer-Verlag, 2007. [RMdlB+ 08] Reza Rafeh, Kim Marriott, Maria Garcia de la Banda, Nicholas Nethercote, and Mark Wallace. Adding search to zinc. In CP, pages 624–629, 2008. [Ros73] B.K. Rosen. Tree-manipulating systems and Church-Rosser theorems. Journal of the ACM, 20 :160–187, 1973. [SLM] C. Schulte, Lagerkvist, http ://www.gecode.org/. [SO08] Helmut Simonis and Barry O’Sullivan. Using global constraints for rectangle packing. In Proceedings of the first Workshop on Bin Packing and Placement Constraints BPPC’08, associated to CPAIOR’08, May 2008. [Sot09] Ricardo Soto. Langages et transformation de modèles en programmation par contraintes. PhD thesis, Université de Nantes, 2009. [Tar72] Robert Tarjan. Depth-first search and linear graph algorithms. SIAM Journal on Computing, 1(2) :146–160, 1972. [Ter03] Terese. Term Rewriting Systems, volume 55 of Cambridge Tracts in Theoretical Computer Science. Cambridge University Press, 2003. [Van89] Pascal Van Hentenryck. Constraint satisfaction in Logic Programming. MIT Press, 1989. [Van99] Pascal Van Hentenryck. The OPL Optimization programming Language. MIT Press, 1999. and G. M., Tack. Gecode. BIBLIOGRAPHIE 121 [vdKFLS10] Roman van der Krogt, Jacob Feldman, James Little, and David Stynes. An integrated business rules and constraints approach to data centre capacity management. In Proceedings of the 16th Conference on Principles and Practice of Constraint Programming CP 2010, Saint-Andrews, Scotland, 2010. [VG06] Vincent Vidal and Hector Geffner. Branching and pruning : An optimal temporal pocl planner based on constraint programming. Artificial Intelligence, 170(3) :298–335, 2006. [VHDT92] Pascal Van Hentenryck, Yves Deville, and Choh-Man Teng. A generic arcconsistency algorithm and its specializations. Artif. Intell., 57(2-3) :291– 321, 1992. [VKK91] Nageshwara Rao Vempaty, Vipin Kumar, and Richard E. Korf. Depthfirst versus best-first search. In AAAI, pages 434–440, 1991. [Wal96] Mark Wallace. Practical applications of constraint programming. Constraints, 1(1/2) :139–168, 1996. [ZK93] Weixiong Zhang and Richard E. Korf. Depth-first vs. best-first search : New results. In AAAI, pages 769–775, 1993. 122 BIBLIOGRAPHIE BIBLIOGRAPHIE 123