PageRenderTime 58ms CodeModel.GetById 14ms app.highlight 38ms RepoModel.GetById 1ms app.codeStats 0ms

/farmR/src/java/src/jfm/model/CroppingComponent.java

https://code.google.com/p/javawfm/
Java | 598 lines | 443 code | 45 blank | 110 comment | 81 complexity | 04b16a49dd8091072b55130e503ef162 MD5 | raw file
  1
  2package jfm.model;
  3
  4
  5import java.text.DecimalFormat;
  6import java.util.*;
  7
  8import jfm.lp.*;
  9import jfm.model.Crop.CropCopy;
 10import jfm.model.Types.*;
 11
 12
 13/** Contains all crops and constructs matrix variables and constraints relating to crop and operation areas.
 14 * This is the key model component class for the model. It contains all crop objects, definitions for 
 15 * all disease and rotation types, the overall area of cropping (farm area) and the limit 
 16 * constraints which may be controlled by the user. Exposes its underlying crops via the 
 17 * \c getCrop function to allow them to be externally manipulated.
 18 * 
 19 * @author Ira Cooke */
 20public final class CroppingComponent extends ModelComponent {	
 21	// ---- Cropping Data ---- //
 22	private final Map<DiseaseType,Disease> diseases=new HashMap<DiseaseType,Disease>(); 
 23//	private final Map<CropType,Rotation> rotations=new HashMap<CropType,Rotation>();
 24	private final List<Limit> limits= new ArrayList<Limit>(); // Limits on Areas or other quantities for crops
 25	private final Map<CropType,Crop> crops=new HashMap<CropType,Crop>(); // List of base crops 
 26	private final Set<CropYear> cropYearsSet=new LinkedHashSet<CropYear>();// Handy aliase for each of the crop years across all types 
 27	private static String constraintTag="Cropping";
 28//	private  double cropArea; // Total crop area avail on this farm 
 29	
 30	/** Gets a specified disease object.  
 31	 * @return The specified disease 
 32	 * @param type The desired disease type 
 33	 * */
 34	public Disease getDisease(DiseaseType type){
 35		if ( diseases.containsKey(type)){
 36			return diseases.get(type);
 37		} else {
 38			throw new Error("Attempt to get undefined disease of type "+type);
 39		}
 40	}
 41	/** Attempt to replace a disease type with a new Disease object. 
 42	 * @param type The disease type to be replaced 
 43	 * @param newDisease The new disease object to be associated with type 
 44	 * @return true if the operation was successful
 45	 * */
 46	public boolean replaceDisease(DiseaseType type,Disease newDisease){
 47		if ( diseases.containsKey(type)){
 48			diseases.put(type, newDisease);
 49			return true;
 50		} else {
 51			return false;
 52		}
 53	}
 54	
 55	/** Returns the crop copy corresponding to a particular year of a particular CropType.
 56	 * If the requested crop type is not present an Error is thrown.  
 57	 * @param baseType The base type of the requested crop. */
 58	public Crop getCrop(CropType baseType){
 59		if ( !crops.containsKey(baseType)){
 60			throw new Error("Request for undefined crop type "+baseType);
 61		}
 62		return crops.get(baseType);
 63	}
 64	
 65	
 66	
 67	public void setFormulaVariables(){
 68//		System.out.println("Setting fvars for cropping "+crops.values().size());
 69		for ( Crop cp: crops.values()){
 70//			System.out.println("Setting vars for "+cp.type);
 71			cp.setFormulaVariables(getParent().location());
 72		}
 73	}
 74	
 75	/** @return A map from crop types to copies of the crops in this object. 
 76	 * Returned crops are deregistered from the parent object */
 77	public Map<CropType,Crop> copyCrops(){
 78		Map<CropType,Crop> cps = new LinkedHashMap<CropType,Crop>();
 79		for ( Crop cp: crops.values()){
 80			Crop cpcpy=cp.copy();
 81			cpcpy.deRegisterCopies();
 82			cps.put(cp.type, cpcpy);
 83		}
 84		return cps;
 85	}
 86	
 87	/** @return An unmodifiable view of all the crops . */
 88	public Map<CropType,Crop> getCrops(){
 89		Map<CropType,Crop> cps = Collections.unmodifiableMap(crops);
 90		return cps;
 91	}
 92	/** Returns an immutable view of the set of all base CropTypes for this Cropping object. */
 93	public Set<CropType> baseCropTypes(){
 94		return Collections.unmodifiableSet(crops.keySet());
 95	}
 96	/** Returns an immutable view of the set of all CropYears for this Cropping object. */
 97	public Set<CropYear> cropYears(){
 98		return Collections.unmodifiableSet((cropYearsSet));
 99	}
100	
101//	public List<Limit> getLimits(){return Collections.unmodifiableList(limits);};
102	public void clearCrops(){
103		crops.clear();
104		cropYearsSet.clear();
105		requireMatrixRebuild();
106	}
107	
108	/** Clear all the Limits. */
109	public void clearLimits(){
110		limits.clear();
111		requireMatrixRebuild();
112	}
113	/** Add a Limit to this Cropping object. 
114	 * This function does not copy its argument when adding 
115	 * @param lim A new limit to be added */
116	public void addLimit(Limit lim){
117		limits.add(lim);
118		requireMatrixRebuild();
119	}
120	
121	// CONSTRUCTOR //
122	public CroppingComponent(){
123		super(ModelComponent.MCType.CROPPING);
124		requireObjective(ObjectiveType.PROFIT);
125		addConstraintBuilder(new OperationAreaConstraint());
126		addConstraintBuilder(new OperationSequencingConstraint());
127		addConstraintBuilder(new NonSequentialOperationConstraint());
128		addConstraintBuilder(new DiseaseConstraint());
129		addConstraintBuilder(new OperationMinAreaConstraint());
130		addConstraintBuilder(new AreaLimitConstraint());
131	}
132
133	
134	/** Add a new type of Crop and its associated year copies.
135	 * This function does not copy its argument when adding. 
136	 * Throws an Error if the type of crop to be added is already present.
137	 * @param newCrop A Crop object of the type to be added */
138	public void addCrop(Crop newCrop){
139		CropType type=newCrop.type;
140		if ( crops.containsKey(type)){
141			throw new Error("Can't add crop of type "+type+" because there is already a crop with that type");
142		}
143		for ( int i=0;i<newCrop.getYearCopies().size();i++){
144			cropYearsSet.add(new CropYear(type,i));
145		}
146		crops.put(type,newCrop);
147//		addRotation(newCrop.rotation());
148		requireMatrixRebuild();
149	}
150	
151	/** Add a new Disease corresponding to a particular base DiseaseType.
152	 * This function does not copy its argument when adding.
153	 * Throws an Error is the base type of the new disease is already present.
154	 * @param newDisease A Disease object to be added */
155	public void addDisease(Disease newDisease){
156		DiseaseType baseType=newDisease.base;
157		if ( diseases.containsKey(baseType)){
158			throw new Error("Can't add disease of type "+baseType+" because a disease with that type already exists ");
159		}
160		diseases.put(baseType, newDisease);
161		requireMatrixRebuild();
162	}
163	
164	/** Add a new Rotation penalty specification corresponding to a DiseaseType in the crops being rotated to.
165	 * This function does not copy its argument when adding.
166	 * @param newRotation The Rotation object specifying the penalties */
167/*	private void addRotation(Rotation newRotation){
168		CropType toType = newRotation.to;
169		if ( rotations.containsKey(toType)){
170			throw new Error("Can't add rotation of disease type "+toType+" because a rotation penalty for that disease already exists ");
171		}
172	//	System.out.println("Adding: "+toType+": "+newRotation.getPenalty(toType)[0]+" "+newRotation.getPenalty(toType)[1]);
173		rotations.put(toType, newRotation);
174		requireMatrixRebuild();
175	}*/
176	
177	
178	/** Create a working copy of all the objects contained within this Cropping object. 
179	 * Performs a deep copy on all mutable objects such as crops, limits and rotations, and a shallow copy 
180	 * of immutable objects such as diseases and rotations. Changes in the copied object will not 
181	 * be reflected in the original.
182	 * @return A working copy of the current object */
183	public CroppingComponent copy(){
184		CroppingComponent cpcpy = new CroppingComponent();		
185		for ( Crop cb:crops.values()){
186			cpcpy.addCrop(cb.copy());
187		}
188		for(Disease dp:diseases.values()){
189			cpcpy.addDisease(dp);
190		}
191		/*
192		for(Rotation rp:rotations.values()){
193			cpcpy.addRotation(rp.copy());
194		}*/
195		for(Limit lm:limits){
196			cpcpy.addLimit(lm.copy());
197		}
198		return cpcpy;
199	}
200	
201	
202	/** Get a list of all the CropCopy objects which have a non-zero solved area */
203	List<CropCopy> cropsUsed(){
204		List<CropCopy> thelist=new ArrayList<CropCopy>();
205		for ( CropYear cy:cropYearsSet){
206			CropCopy cp=crops.get(cy.base).getCopy(cy.copyYear);
207			if ( cp.getSolvedArea() > 0 ){
208		//		System.out.println(cp.name()+cp.getSolvedArea());
209				thelist.add(cp);
210			}
211		}
212		return thelist;
213	}
214	
215	double[] getRotationPenalty(CropType to,DiseaseType from){
216		if ( crops.containsKey(to)){
217			Rotation torot=crops.get(to).rotation();
218			return torot.getPenalty(from);
219		} else {
220			double[] nocosts={0,0};
221			return nocosts;
222		}
223	}
224	
225	Set<CropType> cropsWithDisease(DiseaseType dis){
226		Set<CropType> cps=new HashSet<CropType>();
227		if ( dis==DiseaseType.NONE){
228			return cps; // If disease is None then we just return the empty set
229		}
230		for(CropType ctype: crops.keySet()){
231			if ( crops.get(ctype).diseaseType==dis){
232				cps.add(ctype);
233			}
234		}
235		return cps;
236	}
237	
238	/** Checks to make sure that all the required diseases and rotations have been defined 
239	 * for the crops present */
240	public boolean diseasesAndRotationsComplete(){
241		for (Crop cp:crops.values()){
242			if ( !diseases.containsKey(cp.diseaseType) && cp.diseaseType!=DiseaseType.NONE){
243				return false;
244			}
245		}
246		return true;
247	}
248	protected void initializeStructure(){
249		Location location=getParent().location();
250		for(CropType ct:crops.keySet()){
251			List<CropCopy> clist=crops.get(ct).getYearCopies();
252			for(int i=0;i<clist.size();i++){
253				CropCopy cp=clist.get(i);
254				cp.registerParent(this);		
255				MatrixVariable newVariable=new MatrixVariable(cp.grossMargin(),
256						0.0,1.0,
257						LPX.LPX_LO,LPX.LPX_CV,matrix.numCols(),ObjectiveType.PROFIT);
258				newVariable.setTag(ct.xmlname+"_"+cp.copyYear());
259				matrix.addVariable(newVariable);	// The gross margin variable for this crop
260				cp.registerVariable(newVariable,0);
261				// Now the operation variables for this crop
262				for(Operation op:cp.operations()){
263					op.registerParent(this);
264					List<Integer> pallow = new ArrayList<Integer>(op.unfoldedAllowedSet());
265					Collections.sort(pallow); // We need to ensure that columns for each set of periods 
266					for(Integer p:pallow){
267						newVariable=new MatrixVariable(-op.cost(p,cp,location,getParent().fuelPrice()),0,0,
268								LPX.LPX_LO,LPX.LPX_CV,matrix.numCols(),ObjectiveType.PROFIT);
269								matrix.addVariable(newVariable);
270					newVariable.setTag(ct.xmlname+"_"+cp.copyYear()+"_"+op.type.shortName+"_"+p);
271					op.registerVariable(newVariable,p);
272					}
273				}
274			}	
275		}
276	}
277	
278	
279	protected void updateStructure(){
280		for(Crop cp:crops.values()){
281			for ( CropCopy cy:cp.getYearCopies()){
282				cy.updateStructure(this);
283			}
284		}
285		structureUpdateDone();
286	}
287
288	
289	private final void buildLimitAreaConstraint(Limit lim){
290
291		int row=matrix.numRows();
292		MatrixRow rowpointer =new MatrixRow(lim.min,lim.max,lim.type,
293				row,constraintTag,"AreaLimit");
294		matrix.addRow(rowpointer);		
295		row++;
296		Set<CropType> limCrops=new HashSet<CropType>(lim.getCropSubjects());	
297		Set<CropType> allCrops=crops.keySet();
298		limCrops.retainAll(allCrops); // Ensures only crops that actually exist get this constraint applied;
299		if ( limCrops.size() != lim.getCropSubjects().size()){
300			throw new Error("One or more limit crops in the list \n"+lim.getCropSubjects()+
301					"\n were not specified in the cropping model \n"+allCrops);
302		}
303		for(CropType ct:limCrops){
304			List<CropCopy> cplist=crops.get(ct).getYearCopies();
305			for( CropCopy cp:cplist){
306				rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),1));
307			}
308		}
309	}	
310		
311	/** Constrain the area of each crop to equal the area of the first operation for that crop. 
312	 * 
313	 * @author Ira Cooke 
314	 * 
315	 * */ 
316	public final class OperationAreaConstraint extends ConstraintBuilder {
317		public OperationAreaConstraint(){
318			super(ConstraintBuilder.CBType.OPAREA,ModelComponent.MCType.CROPPING);
319		}
320		protected void build(){
321			int row=matrix.numRows();
322			for(CropType ct:crops.keySet()){
323				for(CropCopy cp:crops.get(ct).getYearCopies()){
324					MatrixRow rowpointer =new MatrixRow(0,0,LPX.LPX_FX,
325							row,constraintTag,type().tag+"_"+ct);
326					matrix.addRow(rowpointer);		
327					row++;
328					rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),-1));
329					Operation op=cp.operations().get(0); // The first operation is the only one that is relevant here 
330					for(Integer p:op.unfoldedAllowedSet()){
331						rowpointer.addElement(new MatrixElement(op.getDependentColumn(p),1));
332//					}
333					}
334				}
335			}
336		}
337	}
338	/** Constrain the total area of each operation (regardless of sequence) to equal the crop area. 
339	 * 
340	 * @author Ira Cooke 
341	 * 
342	 * */
343	public final class NonSequentialOperationConstraint extends ConstraintBuilder{
344		public NonSequentialOperationConstraint(){
345			super(ConstraintBuilder.CBType.NONSEQOP,ModelComponent.MCType.CROPPING);
346		}
347		protected void build(){
348			int row=matrix.numRows();
349			for ( CropType ct:crops.keySet()){ // Loop Crops
350				for(CropCopy cp:crops.get(ct).getYearCopies()){
351					List<Operation> cropOps=cp.operations();
352					int nops = cropOps.size();
353					for ( int o=1;o<nops;o++){ // Loop operations 1 to max
354						Operation op = cropOps.get(o);
355						MatrixRow rowpointer =new MatrixRow(0,0,LPX.LPX_FX,
356							row,constraintTag,type().tag+"_"+ct+"_"+op.type);
357						matrix.addRow(rowpointer);						
358						rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),-1)); // Make a constraint for each crop op combo
359						row++;
360						for ( Integer p:op.unfoldedAllowedSet()){
361							rowpointer.addElement(new MatrixElement(op.getDependentColumn(p),1));
362						}
363					}
364				}
365			}
366		}
367	}
368	/** Constrain the operational areas to conform to sequencing requirements. The sum of operation areas in a given period cannot exceed 
369	 * the sum of areas of the preceeding operation up to that period (or up to an earlier period if a gap is enforced) */	
370	public final class OperationSequencingConstraint extends ConstraintBuilder {
371
372		public OperationSequencingConstraint(){
373			super(ConstraintBuilder.CBType.OPSEQ,ModelComponent.MCType.CROPPING);
374		}
375		protected void build(){
376			int row=matrix.numRows();
377			for(CropType ct:crops.keySet())  { // Loop Crops
378				List<CropCopy> clist=crops.get(ct).getYearCopies();			
379				for(int i=0;i<clist.size();i++){ // Loop crop copies
380					CropCopy cp = clist.get(i);	
381					List<Operation> cpOps=cp.operations();
382					int nops = cpOps.size();
383					for ( int o=1;o<nops;o++){ // Loop operations 1 to max
384						Operation op = cpOps.get(o);
385						if ( op.isSequential()){
386							Operation prevop=cpOps.get(o-1);
387						/* Because we are considering operations within a
388						 single crop growth cycle we use the unfolded periods */
389							Set<Integer> opallow=(Set<Integer>)op.unfoldedAllowedSet(); 
390							Set<Integer> prevopallow=(Set<Integer>)prevop.unfoldedAllowedSet();
391							Set<Integer> peitherallow=new HashSet<Integer>(opallow);						
392							Set<Integer> pbothallow = new HashSet<Integer>(opallow);
393							peitherallow.addAll(prevopallow);
394							
395							pbothallow.retainAll(prevopallow);
396							/* For the case where a gap spans the allowable operations we need to 
397							 * reconstruct the set of periods to include those in the gap */
398							Set<Integer> pbothallowPlusGap = new HashSet<Integer>(pbothallow);
399							int popmin=Collections.min(opallow);
400							for ( int g=1;g<=prevop.gap;g++){
401								int pgap=popmin-g;
402								if ( prevopallow.contains(pgap)){
403									pbothallowPlusGap.add(pgap);
404								}
405							}
406							// Make a check to see if the gap is feasible
407							if ( Collections.min(prevopallow)+prevop.gap > Collections.max(opallow)){
408								throw new Error("The sequence of operations "+prevop.type+" followed by "+
409										op.type+" in crop "+cp.name()+" has a sequencing infeasibility \n ---> check dates and mingap requirements \n");
410							}
411							
412							if ( pbothallowPlusGap.size()>0){ // The intersection is not null
413								for ( Integer pboth:pbothallowPlusGap){
414									MatrixRow rowpointer =new MatrixRow(0,0,LPX.LPX_LO,
415											row,constraintTag,type().tag+"_"+ct+"_"+op.type+"_p"+pboth);
416									matrix.addRow(rowpointer);		
417									row++;
418									for ( int p=Collections.min(peitherallow);p<=pboth;p++){
419										if ( opallow.contains(p)){
420											rowpointer.addElement(new MatrixElement(op.getDependentColumn(p),-1)); // Make a constraint for each op,period,crop combo
421										}
422									//	if ( prevop.gap > 0 ){
423									//		System.out.println(pbothallowPlusGap);
424									//		throw new Error("Stop "+prevop.gap);
425									//	}
426										if ( prevopallow.contains(p) && (p+prevop.gap)<=pboth){ 
427											rowpointer.addElement(new MatrixElement(prevop.getDependentColumn(p),1));
428										}
429									}
430								}
431							}
432						}
433					}
434				}
435			}
436		}
437	}
438	/** If specified, the area of a particular operation is constrained to be at least 
439	 * a certain minimum percentage of the total crop area */
440	public final class OperationMinAreaConstraint extends ConstraintBuilder {
441		public OperationMinAreaConstraint(){
442			super(ConstraintBuilder.CBType.OPMINAREA,ModelComponent.MCType.CROPPING);
443		}
444		protected void build(){
445			int row=matrix.numRows();
446			for(CropType ct:crops.keySet())  { // Loop Crops
447				List<CropCopy> clist=crops.get(ct).getYearCopies();			
448				for(int i=0;i<clist.size();i++){ // Loop crop copies
449					CropCopy cp = clist.get(i);	
450					List<Operation> cpOps=cp.operations();
451					int nops = cpOps.size();
452					for ( int o=0;o<nops;o++){ // Loop operations 1 to max
453						Operation op = cpOps.get(o);
454						if ( op.hasMinAreas()){
455						
456							Set<Integer> pallow=op.unfoldedAllowedSet();
457							for ( Integer p:pallow){
458								if (op.minArea(p)>0){
459									MatrixRow rowpointer =new MatrixRow(0,0,LPX.LPX_LO,
460											row,constraintTag,type().tag+"_"+ct+"_"+op.type+"_p"+p);
461									matrix.addRow(rowpointer);		
462									row++;
463									rowpointer.addElement(new MatrixElement(op.getDependentColumn(p),1));
464									rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),-op.minArea(p)));
465								}
466							}
467						}
468					}
469				}
470			}		
471		}
472	}
473	
474	/** Constrain the area of crops with a disease to be no greater than  */
475	public final class DiseaseConstraint extends ConstraintBuilder{
476		public DiseaseConstraint(){
477			super(ConstraintBuilder.CBType.DISEASE,ModelComponent.MCType.CROPPING);
478		}
479		protected void build(){
480			int row=matrix.numRows();
481			for ( DiseaseType dt:diseases.keySet()){
482				Disease dis=diseases.get(dt);
483				Disease assocdis=diseases.get(dis.associated);
484				MatrixRow rowpointer =new MatrixRow(0,0,LPX.LPX_LO,
485					row,constraintTag,"disease_"+dt);
486				matrix.addRow(rowpointer);		
487				row++;
488				Set<CropType> withdisease=cropsWithDisease(dt);
489				Set<CropType> withassociated=cropsWithDisease(dis.associated);
490				for(CropType croptype:crops.keySet()){
491					CropCopy cp=crops.get(croptype).getCopy(0);
492					if ( !withdisease.contains(croptype) && !withassociated.contains(croptype)){
493						rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),1));
494					} else {
495						if ( withdisease.contains(croptype) && !withassociated.contains(croptype)){
496							rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),-dis.minBreak));
497						} else  if ( withassociated.contains(croptype) && !withdisease.contains(croptype)){
498							rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),-assocdis.minBreak));
499						} else {
500							rowpointer.addElement(new MatrixElement(cp.getDependentColumn(0),-Math.min(assocdis.minBreak,dis.minBreak)));
501						}					
502					}
503				}
504			}
505		}
506	}
507	/** Constrain the areas of specific crops. 
508	 * @author Ira Cooke 
509	 * 
510	 * */
511	public final class AreaLimitConstraint extends  ConstraintBuilder {
512
513		public AreaLimitConstraint(){
514			super(ConstraintBuilder.CBType.AREALIMIT,ModelComponent.MCType.CROPPING);
515		}
516		protected void build(){
517			for(Limit lim:limits){
518				switch(lim.limitType){
519				case AREA:
520					buildLimitAreaConstraint(lim);
521					break;
522				default:
523					throw new Error("No constraint defined for limit type"+lim.limitType);
524				}
525			}	
526		}
527	}
528	
529	public String name(){return "cropping";};
530	// ---- Reporting --- //
531	public String toString(){
532		StringBuffer outstring = new StringBuffer();
533		outstring.append("--- Cropping --- \n");
534		outstring.append("Total Area ");
535		outstring.append("\n");
536		for(Crop cp:crops.values()){
537			outstring.append(cp);			
538		}
539		
540		outstring.append("Diseases \n");
541		for ( Disease ds:diseases.values()){
542			outstring.append(ds);
543		}/*
544		outstring.append("Rotations \n");
545		for ( Rotation rt:rotations.values()){
546			outstring.append(rt);
547		}*/
548		outstring.append("Limits \n");
549		for ( Limit lt:limits){
550			outstring.append(lt);
551		}
552		return outstring.toString();
553	}
554	
555	/** Pretty print the solved areas of each of the crops. 
556	 * Optionally print the detailed breakdown of operations for each crop in 
557	 * each period 
558	 * 
559	 * @param model The parent model holding this cropping object 
560	 * @param detailed Flag whether or not to print detailed operations data */
561	public String printSolution(Farm model,boolean detailed){
562		StringBuffer outstring = new StringBuffer();
563		DecimalFormat f1 = new DecimalFormat("#0.00");
564		outstring.append("--- Cropping --- \n");
565		outstring.append("Total Area ");
566		outstring.append("\n");
567
568		for ( CropYear cy:cropYearsSet){
569			CropCopy cp = crops.get(cy.base).getCopy(cy.copyYear);
570			if ( cp.solvedArea()>0){
571				outstring.append(cp.name()+": "+f1.format(cp.solvedArea())+"\n");	
572			}
573		}
574		if ( detailed ){
575			outstring.append("\n");
576			for ( CropYear cy:cropYearsSet){
577				CropCopy cp = crops.get(cy.base).getCopy(cy.copyYear);
578				if ( cp.solvedArea()>0){
579					outstring.append(cp.printSolution(' '));			
580				}
581			}
582		}
583		return outstring.toString();
584	}
585	
586	public String printCSVWorkPlan(){
587		StringBuffer outstring = new StringBuffer();
588		for ( CropYear cy:cropYearsSet){
589			CropCopy cp = crops.get(cy.base).getCopy(cy.copyYear);
590			outstring.append(cp.name()+"\n");
591			if ( cp.solvedArea()>0){
592				outstring.append(cp.printSolution(','));			
593			}
594		}
595		return outstring.toString();
596	}
597	
598}