001package org.cpsolver.coursett.constraint;
002
003import java.util.ArrayList;
004import java.util.BitSet;
005import java.util.Comparator;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.List;
009import java.util.Set;
010
011import org.cpsolver.coursett.Constants;
012import org.cpsolver.coursett.criteria.FlexibleConstraintCriterion;
013import org.cpsolver.coursett.model.Lecture;
014import org.cpsolver.coursett.model.Placement;
015import org.cpsolver.coursett.model.TimeLocation;
016import org.cpsolver.coursett.model.TimetableModel;
017import org.cpsolver.ifs.assignment.Assignment;
018import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext;
019import org.cpsolver.ifs.assignment.context.ConstraintWithContext;
020import org.cpsolver.ifs.criteria.Criterion;
021
022
023/**
024 * Flexible constraint. <br>
025 * This constraint expresses relations between several classes. Provides similar
026 * functions as Group constraint. Unlike group constraint, Flexible constraint
027 * is able to parse some of its parameters from its reference field<br>
028 * 
029 * @author  Matej Lukac
030 * @version CourseTT 1.3 (University Course Timetabling)<br>
031 *          Copyright (C) 2013 Matej Lukac<br>
032 * <br>
033 *          This library is free software; you can redistribute it and/or modify
034 *          it under the terms of the GNU Lesser General Public License as
035 *          published by the Free Software Foundation; either version 3 of the
036 *          License, or (at your option) any later version. <br>
037 * <br>
038 *          This library is distributed in the hope that it will be useful, but
039 *          WITHOUT ANY WARRANTY; without even the implied warranty of
040 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
041 *          Lesser General Public License for more details. <br>
042 * <br>
043 *          You should have received a copy of the GNU Lesser General Public
044 *          License along with this library; if not see
045 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
046 */
047public abstract class FlexibleConstraint extends ConstraintWithContext<Lecture, Placement, FlexibleConstraint.FlexibleConstraintContext> {
048
049    protected int iPreference;
050    private boolean iIsRequired;   
051    private String iOwner;
052    
053    // Constraint type
054    protected FlexibleConstraintType iConstraintType = null;
055        
056    // A pattern the constraint was created from
057    protected String iReference = "";   
058    
059    // Determines whether the constraint is checked for every week in the semester    
060    protected List<BitSet> iWeeks = null;
061    protected Integer iDayOfWeekOffset = null;
062    protected Boolean iPreciseDateComputation = null;
063    
064    /**
065     * Flexible constraint types
066     * 
067     */
068    public static enum FlexibleConstraintType {
069        /**
070         * Given classes must be taught in a way there is a break between two blocks of classes. 
071         */
072        MAXBLOCK_BACKTOBACK("_(MaxBlock):([0-9]+):([0-9]+)_", MaxBlockFlexibleConstraint.class, "Block"),
073        /**
074         * There must be a break of a given length in a given time interval.
075         */
076        BREAK("_(Break):([0-9]+):([0-9]+):([0-9]+)_", BreakFlexibleConstraint.class, "Break"),
077        /**
078         * Limit number of breaks between adjacent classes on a day.
079         */
080        MAX_BREAKS("_(MaxBreaks):([0-9]+):([0-9]+)_", MaxBreaksFlexibleConstraint.class, "MaxBreaks"),
081        /**
082         * Limit number of weeks on which an a class can take place.
083         */
084        MAX_WEEKS("_(MaxWeeks):([0-9]+):([0-9]+)_", MaxWeeksFlexibleConstraint.class, "MaxWeeks"),
085        /**
086         * Limit number of days of a week. 
087         */
088        MAX_DAYS("_(MaxDays):([0-9]+)_", MaxDaysFlexibleConstraint.class, "MaxDays"),
089        /**
090         * Minimize free time of an instructor during a day (between the first and the last class).
091         */
092        MAX_HOLES("_(MaxHoles):([0-9]+)_", MaxHolesFlexibleConstraint.class, "MaxHoles"),
093        /**
094         * Limit number of half-days of a week. 
095         */
096        MAX_HALF_DAYS("_(MaxHalfDays):([0-9]+)_", MaxHalfDaysFlexibleConstraint.class, "MaxHalfDays"),
097        /**
098         * Limit number of consecutive days of a week. 
099         */
100        MAX_CONSECUTIVE_DAYS("_(MaxConsDays):([0-9]+)_", MaxConsecutiveDaysFlexibleConstraint.class, "MaxConsDays"),
101        ;
102        
103        private String iPattern;
104        private Class<? extends FlexibleConstraint> iImplementation;
105        private String iName;
106        FlexibleConstraintType(String pattern, Class<? extends FlexibleConstraint> implementation, String name) {
107            iPattern = pattern; iImplementation = implementation; iName = name;
108        }
109        
110        public String getPattern() { return iPattern; }
111        
112        public String getName() { return iName.replaceAll("(?<=[^A-Z])([A-Z])"," $1"); }
113
114        public FlexibleConstraint create(Long id, String owner, String preference, String reference) throws IllegalArgumentException {
115            try {
116                return iImplementation.getConstructor(Long.class, String.class, String.class, String.class).newInstance(id, owner, preference, reference);
117            } catch (IllegalArgumentException e) {
118                throw e;
119            } catch (Exception e) {
120                throw new IllegalArgumentException(e.getMessage(), e);
121            }
122        }
123    }
124    
125    /**
126     * Constructor
127     * @param id unique id
128     * @param owner identifier of distribution preference the constraint was created for
129     * @param preference time preference ("R" for required, "P" for prohibited, "-2",
130     *            "-1", "1", "2" for soft preference)   
131     * @param reference parameters of the constraint in String form            
132     */
133    public FlexibleConstraint(Long id, String owner, String preference, String reference){
134        super();                
135        iId = id;
136        iReference = reference;
137        iPreference = Constants.preference2preferenceLevel(preference);
138        iIsRequired = preference.equals(Constants.sPreferenceRequired);        
139        iOwner = owner;                
140    }
141    
142
143    /** 
144     * Return current number of violations.
145     * @param assignment current assignment
146     * @param conflicts conflicting placements to be unassigned
147     * @param assignments assigned placements 
148     * @return the number of violations of the constraint during days and all weeks of the semester
149     */
150    public abstract double getNrViolations(Assignment<Lecture, Placement> assignment, Set<Placement> conflicts, HashMap<Lecture, Placement> assignments);
151
152    
153    /**
154     * Return weeks of the term.
155     * @return a list of bitsets (one for each week of the term) representing datePatterns or null if semester is whole semester is considered
156     */
157    public List<BitSet> getWeeks(){
158        if (iWeeks == null){
159            TimetableModel model = (TimetableModel) getModel();
160            iWeeks = new ArrayList<BitSet>();
161
162            boolean checkWeeks = model.getProperties().getPropertyBoolean("FlexibleConstraint.CheckWeeks", false);
163            
164            if (checkWeeks) {
165                // get weeks method returns bitsets representing weeks during semester
166                iWeeks = model.getWeeks();
167            } else {
168                // weeks are not considered, all placements are taken into consideration
169                iWeeks.add(null);
170            } 
171        }  
172          
173        return iWeeks;
174    }
175    
176    public int getDayOfWeekOffset() {
177        if (iDayOfWeekOffset == null) {
178            TimetableModel model = (TimetableModel) getModel();
179            iDayOfWeekOffset = model.getProperties().getPropertyInt("DatePattern.DayOfWeekOffset", 0);
180        }
181        return iDayOfWeekOffset;
182    }
183    
184    public boolean isPreciseDateComputation() {
185        if (iPreciseDateComputation == null) {
186            TimetableModel model = (TimetableModel) getModel();
187            iPreciseDateComputation = model.getProperties().getPropertyBoolean("FlexibleConstraint.PreciseDateComputation", false);
188        }
189        return iPreciseDateComputation;
190    }
191    
192    @Override
193    public boolean isConsistent(Placement value1, Placement value2) {
194        HashMap<Lecture, Placement> assignments = new HashMap<Lecture, Placement>();
195        if (value1 != null)
196            assignments.put(value1.variable(), value1);
197        if (value2 != null)
198            assignments.put(value2.variable(), value2);
199        
200        if (getNrViolations(null, null, assignments) != 0) return false;
201
202        return super.isConsistent(value1, value2);
203    }
204    
205    /**
206     * Returns placements of variables assigned to this constraint with assignment which satisfy following conditions:
207     * They must be taught in the day included in dayCode.
208     * They cannot be included in conflicts
209     * Their date pattern intersects with week
210     * 
211     * @param assignment current assignment
212     * @param dayCode representation of days in week combination
213     * @param conflicts placements to be unassigned
214     * @param value placement to be assigned
215     * @param assignments placements of variables
216     * @param week bitset representing a date pattern
217     * @return placements of variables assigned to this constraint with assignment which satisfy conditions above
218     */
219    protected Set<Placement> getRelevantPlacements(Assignment<Lecture, Placement> assignment, int dayCode, Set<Placement> conflicts, Placement value,
220            HashMap<Lecture, Placement> assignments, BitSet week) {
221        Set<Placement> placements = new HashSet<Placement>();
222        
223        for (Lecture lecture : variables()) {
224            // lecture of the value is already assigned
225            if(value != null && lecture.equals(value.variable()))continue;
226
227            // lecture might not have assignment if it is present in assignments
228            if (assignments != null && assignments.containsKey(lecture)) {
229                Placement placement = assignments.get(lecture);
230                if (placement != null && shareWeeksAndDay(placement.getTimeLocation(), week, dayCode))
231                    placements.add(placement);
232            } else if (assignment != null) {
233                Placement placement = assignment.getValue(lecture);
234                if (placement != null && (conflicts == null || !conflicts.contains(placement)) && shareWeeksAndDay(placement.getTimeLocation(), week, dayCode))
235                    placements.add(placement);
236            }
237        }
238
239        if (value == null || (conflicts != null && conflicts.contains(value))) {
240            return placements;
241        } 
242        
243        if (shareWeeksAndDay(value.getTimeLocation(), week, dayCode)) placements.add(value); 
244
245        return placements;
246    }
247    
248    /**
249     * Used to determine the daycode and week of a timelocation
250     * 
251     * @param t timelocation 
252     * @param week date pattern compared to timelocation
253     * @param dayCode days compared to timelocation
254     * @return true if TimeLocation matches the date pattern and days
255     */
256    protected boolean shareWeeksAndDay(TimeLocation t, BitSet week, int dayCode){
257        if (isPreciseDateComputation()) return t.overlaps(dayCode, week, dayCode);
258        
259        boolean matchDay = (t.getDayCode() & dayCode) != 0;
260        boolean matchWeek = (week == null || t.shareWeeks(week));                
261        return matchDay && matchWeek;
262    }
263    
264    /**
265     * Creates a list of blocks from a placements sorted by startSlot
266     * 
267     * @param sorted list of placements sorted by startSlot
268     * @param maxBreakBetweenBTB maximum number of free slots between BTB placements
269     * @return groups of BTB placements as a list of blocks
270     */
271    protected List<Block> mergeToBlocks(List<Placement> sorted, int maxBreakBetweenBTB){
272        List<Block> blocks = new ArrayList<Block>();
273        // add placements to blocks
274        for (int i = 0; i < sorted.size(); i++) {
275            Placement placement = sorted.get(i);
276            boolean added = false;
277            // add placement to a suitable block
278            for (int j = 0; j < blocks.size(); j++) {
279                if (blocks.get(j).addPlacement(placement)) {
280                    added = true;
281                }
282            }
283            // create a new block if a lecture does not fit into any block
284            if (!added) {
285                Block block = new Block(maxBreakBetweenBTB);
286                block.addPlacement(placement);
287                blocks.add(block);
288            }
289        }   
290        return blocks;
291    }
292    
293    @Override
294    public boolean isHard() {
295        return iIsRequired;
296    }    
297    
298    @Override
299    public String getName() {
300        return iOwner + ": " + iConstraintType.getName();
301    }
302    
303    public FlexibleConstraintType getType(){
304        return iConstraintType;
305    }
306    
307    public String getReference() {        
308        return iReference;
309    }
310    
311    public String getOwner() {        
312        return iOwner;
313    }   
314    
315    /**
316     * Prolog reference: "R" for required, "P" for prohibited", "-2",.."2" for
317     * preference
318     * @return prolog preference
319     */
320    public String getPrologPreference() {
321        return Constants.preferenceLevel2preference(iPreference);
322    }
323
324    /**
325     * Return the current preference of the flexible constraint, considering conflicts and new assignments.
326     * Used to compute value for flexible constraint criterion.
327     * @param assignment current assignment
328     * @param conflicts conflicting assignment
329     * @param assignments proposed assignments
330     * @return the current preference of the flexible constraint
331     */
332    public double getCurrentPreference(Assignment<Lecture, Placement> assignment, Set<Placement> conflicts, HashMap<Lecture, Placement> assignments){
333        if (isHard()) return 0;
334        double pref = getNrViolations(assignment, conflicts, assignments);
335        if(pref == 0){
336            return - Math.abs(iPreference);
337        }
338        return Math.abs(iPreference) * pref;
339    }
340
341    /**
342     * A block is a list of placements sorted by startSlot, which are BTB.
343     * maxSlotsBetweenBackToBack determines the number of free slots between two BTB placements
344     *
345     */
346    public class Block {
347        
348        // start slot of the block
349        private int startSlotCurrentBlock = -1;        
350        // end slot of the block
351        private int endSlotCurrentBlock = -1;        
352        // max number of slots between classes to be considered Back-To-Back; 4 slots default      
353        private int maxSlotsBetweenBackToBack = 4;
354        // the list of placements of this block
355        private List<Placement> placements = new ArrayList<Placement>();
356        
357        public Block(int maxSlotsBetweenBTB){
358            this.maxSlotsBetweenBackToBack = maxSlotsBetweenBTB;            
359        }              
360        
361        /**
362         * Adds placement to the block and updates block's attributes.
363         * 
364         * @param placement placement to be added to the block
365         * @return true if the placement was successfully added to the block
366         */
367        public boolean addPlacement(Placement placement){   
368            if (placement == null){
369                return false;
370            }
371            
372            TimeLocation t = placement.getTimeLocation();
373            
374            if (t == null){
375                return false;
376            }
377            
378            // if placements is empty, the block only contains currently added placement -> set start and end
379            if (placements.isEmpty()){
380                placements.add(placement);
381                startSlotCurrentBlock = t.getStartSlot();
382                endSlotCurrentBlock = t.getStartSlot() + t.getLength();
383                return true;
384            }
385            
386            // if startSlotCurrentBlock equals placement's start slot, add placement and adjust endSlotCurrentBlock
387            if (t.getStartSlot() == startSlotCurrentBlock){
388                placements.add(placement);
389                int tEnd = t.getStartSlot() + t.getLength();
390                if (tEnd > endSlotCurrentBlock){
391                    endSlotCurrentBlock = tEnd;
392                }
393                return true;
394            }      
395            
396            // if placement starts among endSlotCurrentBlock + modifier and startSlotCurrentBlock, add the placement
397            if (endSlotCurrentBlock + maxSlotsBetweenBackToBack >= t.getStartSlot() && t.getStartSlot() >= startSlotCurrentBlock ){
398                placements.add(placement);
399                int tEnd = t.getStartSlot() + t.getLength();
400                if (tEnd > endSlotCurrentBlock){
401                    endSlotCurrentBlock = tEnd;
402                }
403                return true;
404            }
405            
406            return false;
407        }
408
409        public boolean haveSameStartTime() {
410            if (!placements.isEmpty()) {
411                int startSlot = placements.get(0).getTimeLocation().getStartSlot();
412                for (Placement p : getPlacements()) {
413                    if (p.getTimeLocation().getStartSlot() != startSlot)
414                        return false;
415                }
416            }
417            return true;
418        }
419        
420        public int getStartSlotCurrentBlock(){
421            return startSlotCurrentBlock;
422        }
423        
424        public int getEndSlotCurrentBlock(){
425            return endSlotCurrentBlock;
426        }
427        
428        public int getNbrPlacements(){
429            return placements.size();
430        }        
431       
432        public List<Placement> getPlacements(){
433            return placements;
434        }
435        
436        public int getLengthInSlots(){
437            return endSlotCurrentBlock - startSlotCurrentBlock;
438        }
439        
440        @Override
441        public String toString(){
442            return "[" + startSlotCurrentBlock + ", " + endSlotCurrentBlock + "]" + " PlacementsNbr: "+ getNbrPlacements();
443        }          
444    }
445    
446    /**
447     * Placement comparator: earlier placement first, shorter placement first if both start at the same time.
448     */
449    protected static class PlacementTimeComparator implements Comparator<Placement> {
450        @Override
451        public int compare(Placement p1, Placement p2) {
452            TimeLocation t1 = p1.getTimeLocation(), t2 = p2.getTimeLocation();
453            // compare by start time (earlier first)
454            if (t1.getStartSlot() < t2.getStartSlot())
455                return -1;
456            if (t1.getStartSlot() > t2.getStartSlot())
457                return 1;
458            // same start -> compare by length (shorter first)
459            if (t1.getLength() < t2.getLength())
460                return -1;
461            if (t1.getLength() > t2.getLength())
462                return 1;
463            // fallback
464            return 0;
465        }
466    }
467    
468    @Override
469    public String toString() {
470        return getName();
471    }
472    
473    @Override
474    public FlexibleConstraintContext createAssignmentContext(Assignment<Lecture, Placement> assignment) {
475        return new FlexibleConstraintContext(assignment);
476    }
477    
478    public class FlexibleConstraintContext implements AssignmentConstraintContext<Lecture, Placement> {
479        protected double iLastPreference = 0;
480        
481        FlexibleConstraintContext() {}
482        
483        FlexibleConstraintContext(Assignment<Lecture, Placement> assignment) {
484            updateCriterion(assignment);
485        }
486
487        @Override
488        public void assigned(Assignment<Lecture, Placement> assignment, Placement value) {
489            updateCriterion(assignment);
490        }
491
492        @Override
493        public void unassigned(Assignment<Lecture, Placement> assignment, Placement value) {
494            updateCriterion(assignment);
495        }
496
497        /**
498         * Update value of FlexibleConstraintCriterion and number of violated FlexibleConstraints
499         */
500        protected void updateCriterion(Assignment<Lecture, Placement> assignment) {
501            if (!isHard()) {
502                Criterion<Lecture, Placement> criterion = getModel().getCriterion(FlexibleConstraintCriterion.class);
503                if (criterion != null) {
504                    criterion.inc(assignment, -iLastPreference);                
505                    iLastPreference = getCurrentPreference(assignment, null, null);
506                    criterion.inc(assignment, iLastPreference);  
507                }
508            }
509        }
510        
511        public double getPreference() {
512            return iLastPreference;
513        }
514    }
515}