1 package org.opentrafficsim.road.gtu.generator.od;
2
3 import java.util.ArrayList;
4 import java.util.Arrays;
5 import java.util.Comparator;
6 import java.util.LinkedHashMap;
7 import java.util.LinkedHashSet;
8 import java.util.List;
9 import java.util.Map;
10 import java.util.Map.Entry;
11 import java.util.Set;
12 import java.util.stream.Collectors;
13
14 import org.djunits.unit.FrequencyUnit;
15 import org.djunits.value.vdouble.scalar.Duration;
16 import org.djunits.value.vdouble.scalar.Frequency;
17 import org.djunits.value.vdouble.scalar.Length;
18 import org.djunits.value.vdouble.scalar.Time;
19 import org.djutils.exceptions.Throw;
20 import org.opentrafficsim.base.parameters.ParameterException;
21 import org.opentrafficsim.core.distributions.Generator;
22 import org.opentrafficsim.core.distributions.ProbabilityException;
23 import org.opentrafficsim.core.dsol.OTSSimulatorInterface;
24 import org.opentrafficsim.core.gtu.GTUDirectionality;
25 import org.opentrafficsim.core.gtu.GTUException;
26 import org.opentrafficsim.core.gtu.GTUType;
27 import org.opentrafficsim.core.idgenerator.IdGenerator;
28 import org.opentrafficsim.core.math.Draw;
29 import org.opentrafficsim.core.network.Link;
30 import org.opentrafficsim.core.network.LinkType;
31 import org.opentrafficsim.core.network.Node;
32 import org.opentrafficsim.core.network.OTSNetwork;
33 import org.opentrafficsim.road.gtu.generator.GeneratorPositions;
34 import org.opentrafficsim.road.gtu.generator.GeneratorPositions.LaneBiases;
35 import org.opentrafficsim.road.gtu.generator.LaneBasedGTUGenerator;
36 import org.opentrafficsim.road.gtu.generator.LaneBasedGTUGenerator.RoomChecker;
37 import org.opentrafficsim.road.gtu.generator.MarkovCorrelation;
38 import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGTUCharacteristics;
39 import org.opentrafficsim.road.gtu.generator.characteristics.LaneBasedGTUCharacteristicsGenerator;
40 import org.opentrafficsim.road.gtu.generator.headway.Arrivals;
41 import org.opentrafficsim.road.gtu.generator.headway.ArrivalsHeadwayGenerator;
42 import org.opentrafficsim.road.gtu.generator.headway.ArrivalsHeadwayGenerator.HeadwayDistribution;
43 import org.opentrafficsim.road.gtu.generator.headway.DemandPattern;
44 import org.opentrafficsim.road.gtu.strategical.od.Categorization;
45 import org.opentrafficsim.road.gtu.strategical.od.Category;
46 import org.opentrafficsim.road.gtu.strategical.od.ODMatrix;
47 import org.opentrafficsim.road.network.lane.CrossSectionLink;
48 import org.opentrafficsim.road.network.lane.DirectedLanePosition;
49 import org.opentrafficsim.road.network.lane.Lane;
50
51 import nl.tudelft.simulation.dsol.SimRuntimeException;
52 import nl.tudelft.simulation.dsol.simulators.DEVSSimulatorInterface;
53 import nl.tudelft.simulation.jstats.streams.MersenneTwister;
54 import nl.tudelft.simulation.jstats.streams.StreamInterface;
55
56
57
58
59
60
61
62
63
64
65
66
67 public final class ODApplier
68 {
69
70
71
72
73 private ODApplier()
74 {
75
76 }
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119 public static Map<String, GeneratorObjects> applyOD(final OTSNetwork network, final ODMatrix od,
120 final OTSSimulatorInterface simulator, final ODOptions odOptions) throws ParameterException, SimRuntimeException
121 {
122 Throw.whenNull(network, "Network may not be null.");
123 Throw.whenNull(od, "OD matrix may not be null.");
124 Throw.whenNull(simulator, "Simulator may not be null.");
125 Throw.whenNull(odOptions, "OD options may not be null.");
126 Throw.when(!simulator.getSimulatorTime().eq0(), SimRuntimeException.class,
127 "Method ODApplier.applyOD() should be invoked at simulation time 0.");
128
129
130 final Categorization categorization = od.getCategorization();
131 final boolean laneBased = categorization.entails(Lane.class);
132 boolean markovian = od.getCategorization().entails(GTUType.class);
133
134
135 StreamInterface stream = simulator.getReplication().getStream("generation");
136 if (stream == null)
137 {
138 stream = simulator.getReplication().getStream("default");
139 if (stream == null)
140 {
141 System.out
142 .println("Using locally created stream (not from the simulator) for vehicle generation, with seed 1.");
143 stream = new MersenneTwister(1L);
144 }
145 else
146 {
147 System.out.println("Using stream 'default' for vehicle generation.");
148 }
149 }
150
151 Map<String, GeneratorObjects> output = new LinkedHashMap<>();
152 for (Node origin : od.getOrigins())
153 {
154
155 DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> rootNode = null;
156
157
158
159
160
161
162
163
164 Map<Lane, DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>> originNodePerLane = new LinkedHashMap<>();
165 MarkovChain markovChain = null;
166 if (!laneBased)
167 {
168 rootNode = new DemandNode<>(origin, stream, null);
169 LinkType linkType = getLinkTypeFromNode(origin);
170 if (markovian)
171 {
172 MarkovCorrelation<GTUType, Frequency> correlation = odOptions.get(ODOptions.MARKOV, null, origin, linkType);
173 if (correlation != null)
174 {
175 Throw.when(!od.getCategorization().entails(GTUType.class), IllegalArgumentException.class,
176 "Markov correlation can only be used on OD categorization entailing GTU type.");
177 markovChain = new MarkovChain(correlation);
178 }
179 }
180 }
181 for (Node destination : od.getDestinations())
182 {
183 Set<Category> categories = od.getCategories(origin, destination);
184 if (!categories.isEmpty())
185 {
186 DemandNode<Node, DemandNode<Category, ?>> destinationNode = null;
187 if (!laneBased)
188 {
189 destinationNode = new DemandNode<>(destination, stream, markovChain);
190 rootNode.addChild(destinationNode);
191 }
192 for (Category category : categories)
193 {
194 if (laneBased)
195 {
196
197 Lane lane = category.get(Lane.class);
198 rootNode = originNodePerLane.get(lane);
199 if (rootNode == null)
200 {
201 rootNode = new DemandNode<>(origin, stream, null);
202 originNodePerLane.put(lane, rootNode);
203 }
204 destinationNode = rootNode.getChild(destination);
205 if (destinationNode == null)
206 {
207 markovChain = null;
208 if (markovian)
209 {
210 MarkovCorrelation<GTUType, Frequency> correlation =
211 odOptions.get(ODOptions.MARKOV, lane, origin, lane.getParentLink().getLinkType());
212 if (correlation != null)
213 {
214 Throw.when(!od.getCategorization().entails(GTUType.class),
215 IllegalArgumentException.class,
216 "Markov correlation can only be used on OD categorization entailing GTU type.");
217 markovChain = new MarkovChain(correlation);
218 }
219 }
220 destinationNode = new DemandNode<>(destination, stream, markovChain);
221 rootNode.addChild(destinationNode);
222 }
223 }
224 DemandNode<Category, ?> categoryNode =
225 new DemandNode<>(category, od.getDemandPattern(origin, destination, category));
226 if (markovian)
227 {
228 destinationNode.addLeaf(categoryNode, category.get(GTUType.class));
229 }
230 else
231 {
232 destinationNode.addChild(categoryNode);
233 }
234 }
235 }
236 }
237
238
239 Map<DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>>, Set<DirectedLanePosition>> initialPositions =
240 new LinkedHashMap<>();
241 Map<CrossSectionLink, Double> linkWeights = null;
242 if (laneBased)
243 {
244 for (Lane lane : originNodePerLane.keySet())
245 {
246 DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> demandNode = originNodePerLane.get(lane);
247 Set<DirectedLanePosition> initialPosition = new LinkedHashSet<>();
248 try
249 {
250 initialPosition.add(lane.getParentLink().getStartNode().equals(demandNode.getObject())
251 ? new DirectedLanePosition(lane, Length.ZERO, GTUDirectionality.DIR_PLUS)
252 : new DirectedLanePosition(lane, lane.getLength(), GTUDirectionality.DIR_MINUS));
253 }
254 catch (GTUException ge)
255 {
256 throw new RuntimeException(ge);
257 }
258 initialPositions.put(demandNode, initialPosition);
259 }
260 }
261 else
262 {
263 Set<DirectedLanePosition> positionSet = new LinkedHashSet<>();
264 for (Link link : origin.getLinks())
265 {
266 if (link.getLinkType().isConnector())
267 {
268 if (link.getStartNode().equals(origin))
269 {
270 Node connectedNode = link.getEndNode();
271
272 int served = 0;
273 for (Link connectedLink : connectedNode.getLinks())
274 {
275 if (connectedLink instanceof CrossSectionLink && !connectedLink.getLinkType().isConnector())
276 {
277 served++;
278 }
279 }
280 for (Link connectedLink : connectedNode.getLinks())
281 {
282 if (connectedLink instanceof CrossSectionLink)
283 {
284 if (link instanceof CrossSectionLink && ((CrossSectionLink) link).getDemandWeight() != null)
285 {
286 if (linkWeights == null)
287 {
288 linkWeights = new LinkedHashMap<>();
289 }
290
291 linkWeights.put(((CrossSectionLink) connectedLink),
292 ((CrossSectionLink) link).getDemandWeight() / served);
293 }
294 setDirectedLanePosition((CrossSectionLink) connectedLink, connectedNode, positionSet);
295 }
296 }
297 }
298 }
299 else if (link instanceof CrossSectionLink)
300 {
301 setDirectedLanePosition((CrossSectionLink) link, origin, positionSet);
302 }
303 }
304 initialPositions.put(rootNode, positionSet);
305 }
306
307
308 initialPositions = sortByValue(initialPositions);
309 Map<Node, Integer> originGeneratorCounts = new LinkedHashMap<>();
310 for (DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> root : initialPositions.keySet())
311 {
312 Set<DirectedLanePosition> initialPosition = initialPositions.get(root);
313
314 Node o = root.getObject();
315 String id = o.getId();
316 if (laneBased)
317 {
318 Integer count = originGeneratorCounts.get(o);
319 if (count == null)
320 {
321 count = 0;
322 }
323 count++;
324 id += count;
325 originGeneratorCounts.put(o, count);
326 }
327
328 Lane lane;
329 LinkType linkType;
330 if (laneBased)
331 {
332 lane = initialPosition.iterator().next().getLane();
333 linkType = lane.getParentLink().getLinkType();
334 }
335 else
336 {
337 lane = null;
338 linkType = getLinkTypeFromNode(o);
339 }
340 HeadwayDistribution randomization = odOptions.get(ODOptions.HEADWAY_DIST, lane, o, linkType);
341 ArrivalsHeadwayGenerator headwayGenerator =
342 new ArrivalsHeadwayGenerator(root, simulator, stream, randomization);
343 GTUCharacteristicsGeneratorODWrapper characteristicsGenerator = new GTUCharacteristicsGeneratorODWrapper(root,
344 simulator, odOptions.get(ODOptions.GTU_TYPE, lane, o, linkType), stream);
345 RoomChecker roomChecker = odOptions.get(ODOptions.ROOM_CHECKER, lane, o, linkType);
346 IdGenerator idGenerator = odOptions.get(ODOptions.GTU_ID, lane, o, linkType);
347 LaneBiases biases = odOptions.get(ODOptions.LANE_BIAS, lane, o, linkType);
348
349 try
350 {
351 LaneBasedGTUGenerator generator = new LaneBasedGTUGenerator(id, headwayGenerator, characteristicsGenerator,
352 GeneratorPositions.create(initialPosition, stream, biases, linkWeights), network, simulator,
353 roomChecker, idGenerator);
354 generator.setNoLaneChangeDistance(odOptions.get(ODOptions.NO_LC_DIST, lane, o, linkType));
355 output.put(id, new GeneratorObjects(generator, headwayGenerator, characteristicsGenerator));
356 }
357 catch (SimRuntimeException exception)
358 {
359
360 throw new RuntimeException(exception);
361 }
362 catch (ProbabilityException exception)
363 {
364
365 throw new RuntimeException(exception);
366 }
367 }
368 }
369 return output;
370 }
371
372
373
374
375
376
377 private static LinkType getLinkTypeFromNode(final Node node)
378 {
379 return getLinkTypeFromNode0(node, false);
380 }
381
382
383
384
385
386
387
388 private static LinkType getLinkTypeFromNode0(final Node node, final boolean ignoreConnectors)
389 {
390 LinkType linkType = null;
391 for (Link link : node.getLinks())
392 {
393 LinkType next = link.getLinkType();
394 if (!ignoreConnectors && next.isConnector())
395 {
396 Node otherNode = link.getStartNode().equals(node) ? link.getEndNode() : link.getStartNode();
397 next = getLinkTypeFromNode0(otherNode, true);
398 }
399 if (next != null && !next.isConnector())
400 {
401 if (linkType == null)
402 {
403 linkType = next;
404 }
405 else
406 {
407 linkType = linkType.commonAncestor(next);
408 if (linkType == null)
409 {
410
411 return null;
412 }
413 }
414 }
415 }
416 return linkType;
417 }
418
419
420
421
422
423
424
425
426 private static <K, V extends Set<DirectedLanePosition>> Map<K, V> sortByValue(final Map<K, V> map)
427 {
428 return map.entrySet().stream().sorted(new Comparator<Map.Entry<K, V>>()
429 {
430 @Override
431 public int compare(final Entry<K, V> o1, final Entry<K, V> o2)
432 {
433 DirectedLanePosition lanePos1 = o1.getValue().iterator().next();
434 String linkId1 = lanePos1.getLane().getParentLink().getId();
435 DirectedLanePosition lanePos2 = o2.getValue().iterator().next();
436 String linkId2 = lanePos2.getLane().getParentLink().getId();
437 int c = linkId1.compareToIgnoreCase(linkId2);
438 if (c == 0)
439 {
440 Length pos1 = lanePos1.getGtuDirection().isPlus() ? Length.ZERO : lanePos1.getLane().getLength();
441 Length lat1 = lanePos1.getLane().getLateralCenterPosition(pos1);
442 Length pos2 = lanePos2.getGtuDirection().isPlus() ? Length.ZERO : lanePos2.getLane().getLength();
443 Length lat2 = lanePos2.getLane().getLateralCenterPosition(pos2);
444 return lat1.compareTo(lat2);
445 }
446 return c;
447 }
448 }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
449 }
450
451
452
453
454
455
456
457
458 private static void setDirectedLanePosition(final CrossSectionLink link, final Node node,
459 final Set<DirectedLanePosition> positionSet)
460 {
461 for (Lane lane : link.getLanes())
462 {
463 try
464 {
465 positionSet.add(lane.getParentLink().getStartNode().equals(node)
466 ? new DirectedLanePosition(lane, Length.ZERO, GTUDirectionality.DIR_PLUS)
467 : new DirectedLanePosition(lane, lane.getLength(), GTUDirectionality.DIR_MINUS));
468 }
469 catch (GTUException ge)
470 {
471 throw new RuntimeException(ge);
472 }
473 }
474 }
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498 private static class DemandNode<T, K extends DemandNode<?, ?>> implements Arrivals
499 {
500
501
502 private final T object;
503
504
505 private final StreamInterface stream;
506
507
508 private final List<K> children = new ArrayList<>();
509
510
511 private final DemandPattern demandPattern;
512
513
514 private final List<GTUType> gtuTypes = new ArrayList<>();
515
516
517 private final List<Integer> gtuTypeCounts = new ArrayList<>();
518
519
520 private final Map<K, GTUType> gtuTypesPerChild = new LinkedHashMap<>();
521
522
523 private final MarkovChain markov;
524
525
526
527
528
529
530
531 DemandNode(final T object, final StreamInterface stream, final MarkovChain markov)
532 {
533 this.object = object;
534 this.stream = stream;
535 this.demandPattern = null;
536 this.markov = markov;
537 }
538
539
540
541
542
543
544 DemandNode(final T object, final DemandPattern demandPattern)
545 {
546 this.object = object;
547 this.stream = null;
548 this.demandPattern = demandPattern;
549 this.markov = null;
550 }
551
552
553
554
555
556 public void addChild(final K child)
557 {
558 this.children.add(child);
559 }
560
561
562
563
564
565
566 public void addLeaf(final K child, final GTUType gtuType)
567 {
568 Throw.when(this.gtuTypes == null, IllegalStateException.class,
569 "Adding leaf with GTUType in not possible on a non-Markov node.");
570 addChild(child);
571 this.gtuTypesPerChild.put(child, gtuType);
572 if (!this.gtuTypes.contains(gtuType))
573 {
574 this.gtuTypes.add(gtuType);
575 this.gtuTypeCounts.add(1);
576 }
577 else
578 {
579 int index = this.gtuTypes.indexOf(gtuType);
580 this.gtuTypeCounts.set(index, this.gtuTypeCounts.get(index) + 1);
581 }
582 }
583
584
585
586
587
588
589 public K draw(final Time time)
590 {
591 Throw.when(this.children.isEmpty(), RuntimeException.class, "Calling draw on a leaf node in the demand tree.");
592 Map<K, Double> weightMap = new LinkedHashMap<>();
593 if (this.markov == null)
594 {
595
596 for (K child : this.children)
597 {
598 double f = child.getFrequency(time, true).si;
599 weightMap.put(child, f);
600 }
601 }
602 else
603 {
604
605 GTUType[] gtuTypeArray = new GTUType[this.gtuTypes.size()];
606 gtuTypeArray = this.gtuTypes.toArray(gtuTypeArray);
607 Frequency[] steadyState = new Frequency[this.gtuTypes.size()];
608 Arrays.fill(steadyState, Frequency.ZERO);
609 Map<K, Frequency> frequencies = new LinkedHashMap<>();
610 for (K child : this.children)
611 {
612 GTUType gtuType = this.gtuTypesPerChild.get(child);
613 int index = this.gtuTypes.indexOf(gtuType);
614 Frequency f = child.getFrequency(time, true);
615 frequencies.put(child, f);
616 steadyState[index] = steadyState[index].plus(f);
617 }
618 GTUType nextGtuType = this.markov.draw(gtuTypeArray, steadyState, this.stream);
619
620 for (K child : this.children)
621 {
622 if (this.gtuTypesPerChild.get(child).equals(nextGtuType))
623 {
624 double f = frequencies.get(child).si;
625 weightMap.put(child, f);
626 }
627 }
628 }
629 return Draw.drawWeighted(weightMap, this.stream);
630 }
631
632
633
634
635
636 public T getObject()
637 {
638 return this.object;
639 }
640
641
642
643
644
645
646 public K getChild(final Object obj)
647 {
648 for (K child : this.children)
649 {
650 if (child.getObject().equals(obj))
651 {
652 return child;
653 }
654 }
655 return null;
656 }
657
658
659 @Override
660 public Frequency getFrequency(final Time time, final boolean sliceStart)
661 {
662 if (this.demandPattern != null)
663 {
664 return this.demandPattern.getFrequency(time, sliceStart);
665 }
666 Frequency f = new Frequency(0.0, FrequencyUnit.PER_HOUR);
667 for (K child : this.children)
668 {
669 f = f.plus(child.getFrequency(time, sliceStart));
670 }
671 return f;
672 }
673
674
675 @Override
676 public Time nextTimeSlice(final Time time)
677 {
678 if (this.demandPattern != null)
679 {
680 return this.demandPattern.nextTimeSlice(time);
681 }
682 Time out = null;
683 for (K child : this.children)
684 {
685 Time childSlice = child.nextTimeSlice(time);
686 out = out == null || (childSlice != null && childSlice.lt(out)) ? childSlice : out;
687 }
688 return out;
689 }
690
691
692 @Override
693 public String toString()
694 {
695 return "DemandNode [object=" + this.object + ", stream=" + this.stream + ", children=" + this.children
696 + ", demandPattern=" + this.demandPattern + ", gtuTypes=" + this.gtuTypes + ", gtuTypeCounts="
697 + this.gtuTypeCounts + ", gtuTypesPerChild=" + this.gtuTypesPerChild + ", markov=" + this.markov + "]";
698 }
699
700 }
701
702
703
704
705
706 private static class MarkovChain
707 {
708
709 private final MarkovCorrelation<GTUType, Frequency> markov;
710
711
712 private GTUType previousGtuType = null;
713
714
715
716
717
718 MarkovChain(final MarkovCorrelation<GTUType, Frequency> markov)
719 {
720 this.markov = markov;
721 }
722
723
724
725
726
727
728
729
730 public GTUType draw(final GTUType[] gtuTypes, final Frequency[] intensities, final StreamInterface stream)
731 {
732 this.previousGtuType = this.markov.drawState(this.previousGtuType, gtuTypes, intensities, stream);
733 return this.previousGtuType;
734 }
735 }
736
737
738
739
740
741
742
743
744
745
746
747
748
749 private static class GTUCharacteristicsGeneratorODWrapper implements LaneBasedGTUCharacteristicsGenerator
750 {
751
752
753 private final DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> root;
754
755
756 private final DEVSSimulatorInterface.TimeDoubleUnit simulator;
757
758
759 private final GTUCharacteristicsGeneratorOD charachteristicsGenerator;
760
761
762 private final StreamInterface randomStream;
763
764
765
766
767
768
769
770 GTUCharacteristicsGeneratorODWrapper(final DemandNode<Node, DemandNode<Node, DemandNode<Category, ?>>> root,
771 final DEVSSimulatorInterface.TimeDoubleUnit simulator,
772 final GTUCharacteristicsGeneratorOD charachteristicsGenerator, final StreamInterface randomStream)
773 {
774 this.root = root;
775 this.simulator = simulator;
776 this.charachteristicsGenerator = charachteristicsGenerator;
777 this.randomStream = randomStream;
778 }
779
780
781 @Override
782 public LaneBasedGTUCharacteristics draw() throws ProbabilityException, ParameterException, GTUException
783 {
784
785 Time time = this.simulator.getSimulatorTime();
786 Node origin = this.root.getObject();
787 DemandNode<Node, DemandNode<Category, ?>> destinationNode = this.root.draw(time);
788 Node destination = destinationNode.getObject();
789 Category category = destinationNode.draw(time).getObject();
790
791 return this.charachteristicsGenerator.draw(origin, destination, category, this.randomStream);
792 }
793
794
795 @Override
796 public String toString()
797 {
798 return "GTUCharacteristicsGeneratorODWrapper [root=" + this.root + ", simulator=" + this.simulator
799 + ", charachteristicsGenerator=" + this.charachteristicsGenerator + ", randomStream=" + this.randomStream
800 + "]";
801 }
802
803 }
804
805
806
807
808
809
810
811
812
813
814
815
816
817 public static class GeneratorObjects
818 {
819
820
821 private final LaneBasedGTUGenerator generator;
822
823
824 private final Generator<Duration> headwayGenerator;
825
826
827 private final LaneBasedGTUCharacteristicsGenerator charachteristicsGenerator;
828
829
830
831
832
833
834 public GeneratorObjects(final LaneBasedGTUGenerator generator, final Generator<Duration> headwayGenerator,
835 final LaneBasedGTUCharacteristicsGenerator charachteristicsGenerator)
836 {
837 this.generator = generator;
838 this.headwayGenerator = headwayGenerator;
839 this.charachteristicsGenerator = charachteristicsGenerator;
840 }
841
842
843
844
845
846 public LaneBasedGTUGenerator getGenerator()
847 {
848 return this.generator;
849 }
850
851
852
853
854
855 public Generator<Duration> getHeadwayGenerator()
856 {
857 return this.headwayGenerator;
858 }
859
860
861
862
863
864 public LaneBasedGTUCharacteristicsGenerator getCharachteristicsGenerator()
865 {
866 return this.charachteristicsGenerator;
867 }
868
869 }
870
871 }