Project 2B: NGordnet (Wordnet)


Each assignment will have an FAQ linked at the top. You can also access it by adding “/faq” to the end of the URL. The FAQ for Project 2b is located here.

Due 10/28/2022 #

In this project, you’ll complete your implementation of the NGordnet tool.

Unlike Project 2a, the implementation for this part of the project is very open-ended. Deciding on an overall design is an important skill that we’ll also revisit in Project 3. The number of lines of code for this project isn’t necessarily large, but there are a lot of independent decisions that you’ll need to make along the way.

DISCLAIMER: As this is a totally new project, there may be occasional bugs or confusions with the spec. If you notice anything of this sort, please post on Ed or contact Professor Hug directly with any anomalies that you observe at

Project Setup #

  1. As you’ve done with other assignments in this class, run git pull skeleton main to get the skeleton code for this project.
    1. NOTE: You’ll notice that this skeleton is (almost) the exact same as the Project 2a skeleton. Rather than having you use your own implementations of TimeSeries, NGramMap, HistoryTextHandler, and HistoryHandler, we’ve instead provided you with working (and obfuscated) implementations of these classes in library-fa22 (see the next step).
    2. Our provided NGramMap class has the behavior that if you call a function like countHistory but there are no valid words in the time frame specified, e.g. TimeSeries ts = ngm.countHistory("gwexlbexl", 1900, 1950);, then the resulting ts is just an empty TimeSeries, i.e. a map with an empty keySet.
  2. To get the additional libraries for this project, cd into your library-fa22 directory and run git pull. Then, import all the libraries from library-fa22 into this project like you normally would.
    1. Now that you’ve pulled and imported the libraries, you’ll notice that the code in (including the lines that use NGramMap) should no longer be red.
  3. Download the new data files for this project using this link and move them into your proj2b folder on the same level as ngordnet.

Getting Started Videos (added Oct 23) #

The course staff has also created a couple of introductory videos to the project and the starter code available here.

Using the WordNet Dataset #

Before we can incorporate WordNet into our project, we first need to understand the WordNet dataset.

WordNet is a “semantic lexicon for the English language” that is used extensively by computational linguists and cognitive scientists; for example, it was a key component in IBM’s Watson. WordNet groups words into sets of synonyms called synsets and describes semantic relationships between them. One such relationship is the is-a relationship, which connects a hyponym (more specific synset) to a hypernym (more general synset). For example, “change” is a hypernym of “demotion”, since “demotion” is-a (type of) “change”. “ change” is in turn a hyponym of “action”, since “change” is-a (type of) “action”. A visual depiction of some hyponym relationships in English is given below:


Each node in the graph above is a synset. Synsets consist of one or more words in English that all have the same meaning. For example, one synset is “jump, parachuting” , which represents the act of descending to the ground with a parachute. “jump, parachuting” is a hyponym of “descent”, since “jump, parachuting” is-a “descent”.

Words in English may belong to multiple synsets. This is just another way of saying words may have multiple meanings. For example, the word “jump” also belongs to the synset “jump, leap” , which represents the more figurative notion of jumping (e.g. a jump in attendance) rather the literal meaning of jump from the other synset (e.g. a jump over a puddle). The hypernym of the synset “jump, leap” is “increase”, since “jump, leap” is-an “increase”. Of course, there are other ways to “increase” something: for example, we can increase something through “augmentation,” and thus it is no surprise that we have an arrow pointing downwards from “increase” to “augmentation” in the diagram above.

Synsets may include not just words, but also what are known as collocations. You can think of these as single words that occur next to each other so often that they are considered a single word, e.g. nasal_decongestant . To avoid ambiguity, we will represent the constituent words of collocations as being separated with an underscore _ instead of the usual convention in English of separating them with spaces. For simplicity, we will refer to collocations as simply “words” throughout this document.

A synset may be a hyponym of multiple synsets. For example, “actifed” is a hyponym of both “antihistamine” and “ nasal_decongestant”, since “actifed” is both of these things.

If you’re curious, you can browse the Wordnet database by using the web interface , though this is not necessary for this project.

Hyponyms (Basic Case) #

Setting up a HyponymsHandler #

  1. In your web browser, open the ngordnet.html file in the static folder. You’ll see that there is a new button: “ Hyponyms”. Note that there is also a new input box called k.

  2. Try clicking the Hyponyms button. You’ll see nothing happens (and if you open the developer tools feature of your web browser, you’ll see that your browser shows an error).

In Project 2B, your primary task is to implement this button, which will require reading in a different type of dataset and synthesizing the results with the dataset from Project 2A. Unlike 2A, it will be entirely up to you to decide what classes you need to support this task.

  1. Start by opening your file.
  2. Register a new handler called HyponymsHandler that simply returns the word “Hello!” when the user clicks the Hyponyms button in the browser. You’ll need to create a new HyponymsHandler class that extends the NgordnetQueryHandler class. See your other Handler classes for examples. Make sure when you register your handler that you use the string “hyponyms” as the first argument to the register method, and not “hyponym”.
  3. Once you’ve modified Main so that your new handler is registered to handle hyponyms requests, start up Main and try clicking the Hyponyms button in your web browser again. You should see text appear that says “Hello”.

Hyponyms Handler (Basic Case) #

Next, you’ll create a partial implementation of the Hyponyms button. For now, this button should:

  • Assume that the “words” entered is only a single word.
  • Ignore startYear, endYear, and k.
  • Return a string representation of a list of the hyponyms of the single word, including the word itself. The list should be in alphabetical order, with no repeated words.

For example, suppose the WordNet dataset looks like the diagram below (given to you as the input files synsets11.txt and hyponyms11.txt). Suppose that the user enters “descent” and clicks on the Hyponyms button.

fig 1

In this case, the output of your handler should be the string representation of a list containing “descent”, “jump” and “ parachuting”, i.e [descent, jump, parachuting]. Note that the words are in alphabetical order.

As another example, suppose we’re using a bigger dataset such as the one below (given to you as the input files synsets16.txt and hyponyms16.txt):


Suppose the user enters “change” and clicks on the Hyponyms button. In this case, the hyponyms are all the words in the blue nodes in the diagram below:


That is the output is [alteration, change, demotion, increase, jump, leap, modification, saltation, transition, variation]. Note that even though “change” belongs to two different synsets, it only appears once.

Note: Don’t overthink this and make life harder than it needs to be. Specifically, observe that the output does not include:

  • Synonyms of synonyms (e.g. does not include adjustment)
  • Hyponyms of synonyms (e.g. does not include conversion)
  • Hyponyms of other definitions of hyponyms (e.g. does not include “flashback”, which is a hyponym of another definition of “transition”)

To complete this task, you’ll need to decide what classes you need to create to support the HyponymHandler. DO NOT DO ALL THE WORK IN HYPONYMS HANDLER. Instead, you should have helper classes. For example, to handle the “History” button, we created an NGramMap class. You’ll want to do something similar.

In order to complete this task, you’ll need to understand the input format of the WordNet dataset. This description is given in the section below.

For this part, you may NOT import any existing graph library into your code. That is you can’t import, for example, the graph implementations from the optional Princeton algorithms textbook. Instead, you should build your own graph class or classes.

Tips #

  • Just like NGramMap, you’ll want your helper classes to only parse the input files once, in the constructor. DO NOT CREATE METHODS WHICH HAVE TO READ THE ENTIRE INPUT FILE EVERY TIME THEY ARE CALLED. This will be too slow!
  • We strongly recommend creating at least two classes for this part of the project as follows: One which implements the idea of a directed graph. One which reads in the WordNet dataset and constructs an instance of the directed graph class. This second class should also be able to take a word and return its hyponyms. You may also want additional helper classes that represent the idea of a traversal.
  • Don’t worry about writing JUnit tests yet, we’ll talk about how to do that later in the spec. Simply use the web front end to check the two input examples (“descent” and “change”) from the diagrams above for synsets16.txt and hyponyms16.txt.
  • While you can (and should) write unit tests for the helper classes/methods that you create for this project, another good way to test and see what’s going on with your code is to simply run, open ngordnet.html, enter some inputs into the boxes, and click the “Hyponyms” button. You may find visual debugging can lead to some useful discoveries in this project.
  • Because of the obfuscation that we applied to the Project 2a files (in particular, TimeSeries and NGramMap), the argument name previews when using these classes in IntelliJ may look a little weird. You may see long, random strings; these are intentional in order to obfuscate the code, and they do not represent an issue with your own code in any way.

WordNet File Format #

We now describe the two types of data files that store the WordNet dataset. These files are in comma separated format, meaning that each line contains a sequence of fields, separated by commas.

  • File type #1: List of noun synsets. The file synsets.txt (and other smaller files with synset in the name) lists all the synsets in WordNet. The first field is the synset id (an integer), the second field is the synonym set (or synset) , and the third field is its dictionary definition (also called its “gloss” for some reason). For example, the line

     36,AND_circuit AND_gate,a circuit in a computer that fires only when all of its inputs fire

    means that the synset { AND_circuit, AND_gate } has an id number of 36 and its definition is “a circuit in a computer that fires only when all of its inputs fire”. The individual nouns that comprise a synset are separated by spaces (and a synset element is not permitted to contain a space). The S synset ids are numbered 0 through S − 1; the id numbers will appear consecutively in the synset file. You will not (officially) use the definitions in this project, though you’re welcome to use them in some interesting way if you’d like if you decide to add optional features at the end of this project. The id numbers are useful because they also appear in the hyponym files, described as file type #2.

  • File type #2: List of hyponyms. The file hyponyms.txt (and other smaller files with hyponym in the name) contains the hyponym relationships: The first field is a synset id; subsequent fields are the id numbers of the synset’s direct hyponyms. For example, the following line


    means that the synset 79537 (“viceroy vicereine”) has two hyponyms: 38611 (“exarch”) and 9007 (“Khedive”), representing that exarchs and Khedives are both types of viceroys (or vicereine). The synsets are obtained from the corresponding lines in the file synsets.txt:

    79537,viceroy vicereine,governor of a country or province who rules...
    38611,exarch,a viceroy who governed a large province in the Roman Empire
    9007,Khedive,one of the Turkish viceroys who ruled Egypt between...

    There may be more than one line that starts with the same synset ID. For example, in hyponyms16.txt, we have


    This indicates that both synsets 12 and 13 are direct hyponyms of synset 11. These two could also have been combined on to one line, i.e. the line below would have the exact same meaning, namely that synsets 12 and 13 are direct hyponyms of synset 11.


    You might ask why there are two ways of specifying the same thing. Real world data is often messy, and we have to deal with it.

Suggested Steps to Take #

To get the Hyponyms button working you’ll need to:

  • Develop a graph class. If you aren’t familiar with this data structure, take a look at lectures 21 and 22. You should test this with operations that are independent of the given data files. For example, my tests evaluated that my createNode and addEdge functions yielded appropriate graphs by using my graph classes’s getNodes and neighbors functions.
  • Write code that converts the WordNet dataset files into a graph. This could be part of your graph class, or it could be a class that uses your graph class.
  • Write code that takes a word, and uses a graph traversal to find all hyponyms of that word in the given graph.

We strongly recommended writing tests that evaluate queries on the examples above (for example, you might look at the hyponyms of “descent” on synsets11/hypernyms11, or the hyponyms of “change” on synsets16/hypernyms16).

Tests should be written at a level of abstraction appropriate to what they’re evaluating. For example, I have a class called TestGraph that evaluates various aspects of my Graph class.

Or as another example, my code has a class called TestWordNet containing the function below.

public void testHyponymsSimple() {
    WordNet wn = new WordNet("./data/wordnet/synsets11.txt", "./data/wordnet/hyponyms11.txt");
    assertEquals(Set.of("antihistamine", "actifed"), wn.hyponyms("antihistamine"));

Note your WordNet class may not have the same functions as mine so the test shown will probably not work verbatim with your code. Note that my test does NOT use an NGramMap anywhere, nor is it using a HyponymHandler, nor is it directly invoking an object of type GraPH. It is specifically tailored to testing the WordNet class. Relying on only browser tests will be incredibly frustrating (and slow!). Use your JUnit skills to build confidence in the foundational abstractions that you build (e.g. Graph, WordNet, etc.).

Design Tips #

Based on discussions with students at office hours, we added this section on 10/28/2022.

This project involves having to do all sorts of different lookups, graph operations, and data processing operations. There is no one right way to do this.

Some example lookups that you might need to perform:

  • Given a word (e.g. “change”), what nodes contain that word?
    • Example in synsets16.txt: change is in synsets 2 and 8
  • Given an integer, what node goes with that index?
    • Necessary for processing hyponyms.txt. For example in hyponyms16.txt, we know that the node with synset 8 points at synsets 9 and 10, so we need to be able to find node 8 to get its adjacency list.
  • Given a node, what words are in that node?
    • Example in synsets16.txt: synset 11 contains alteration, modification, and adjustment

Some example graph operations you might need to perform:

  • Creating a node, e.g. each line of synsets16.txt contains the information for a node.
  • Adding an edge to a node, e.g. each line of hyponyms16.txt contains one or more edges that should be added to the corresponding node.
  • Finding reachable vertices, e.g. the vertices reachable from vertex #7 in hyponyms16.txt are 7, 8, 9, 10.

Your life will be a lot easier if you select instance variables for your classes that naturally help solve all six of the problems above.

Some example data processing operations:

  • Given a collection of things, how do you find all non-duplicate items? (Hint: There is a data structure that makes this very easy and efficient). Don’t be afraid to also google documentation for the data structure that you choose (e.g. if you choose to use a TreeMap for whatever reason, feel free to look up “TreeMap methods java”, “Map methods java”, or “Collection methods java”, etc).
  • Given a collection of things, how do you sort them? (Hint: Google how to sort the collection that you’re using)

Also, a reminder from proj2a: Deeply nested generics are a warning sign that you are doing something too complicated. Either find a simpler way or create a helper class to help manage the complexity. For example, if you find yourself trying to use something like Map<Set<Set<…, you have started a walk down an unnecessarily difficult path.

As usual, if you have a design that is painful and with which you cannot make progress, don’t be afraid to delete your existing instance variables and try again. The hard part of this project is the design, not the programming. You can always use git to recover your old design if you decide you actually liked it.

Handling Lists of Words #

Your next task is to handle lists of words. As an example, if the user enters “change, occurrence” for the diagram below, we should get only words from the nodes in blue, i.e [alteration, change, increase, jump, leap, modification, saltation, transition]. “Demotion” and “variation” are not included because they are not hyponyms of both words; specifically, they are not hyponyms of “occurrence”.


As you can see, we only want to return words which are hyponyms of ALL words in the list. Furthermore, note that the list of words provided by the user can include more than just 2 words, even though our examples in this spec do not.

As another example which demonstrates the usefulness of this feature, let’s say we are using the full synsets.txt and hyponyms.txt and enter “female, animal” in the words box. Then, clicking “Hyponyms” should display [amazon, bird, cat, chick, dam, demoiselle, female, female_mammal, filly, hag, hen, nanny, nymph, siren], as these are all the words which are hyponyms of female and animal. Or, if we enter “female, leader” in the words box and then click “Hyponyms”, you should get back [crown_princess, marchioness, materfamilias, matriarch, mayoress, mistress, vicereine, viscountess].

As a more verbose example, considerl “bowl, gallery”:

  • The hyponymns of bowl are [Amphitheatrum_Flavium, Colosseum, amphitheater, amphitheatre, arena, ballpark, bowl, bowlful, bowling_ball, bullring, cereal_bowl, circus, coliseum, covered_stadium, dome, domed_stadium, finger_bowl, fish_bowl, fishbowl, football_stadium, goldfish_bowl, hippodrome, jorum, mazer, mixing_bowl, park, pipe_bowl, porringer, punch_bowl, roll, salad_bowl, skybox, slop_basin, slop_bowl, soup_bowl, sports_stadium, stadium, toilet_bowl, trough]
  • The hyponyms of gallery are [amphitheater, amphitheatre, art_gallery, choir_loft, drift, gallery, heading, lanai, organ_loft, picture_gallery, salon, veranda, verandah]
  • The intersection of these two sets is [amphitheater, amphitheatre]

Note (10/23/2022): An older version of the spec had incorrect answers for these queries which referred to an even older version of this spec that was never released.

To test this part of your code, we recommend manually constructing examples using synsets16.txt and hyponyms16.txt and using the front end to evaluate correctness.

Handling k > 0 #

Above, we handled the situation where k = 0, which is the default value when the user does not enter a k value.

Your final required task is to handle the case where the user enters k. k represents the maximum number of hyponyms that we want in our output. For example, if someone enters the word “dog”, and then enters k = 5, your code would return exactly 5 words.

To choose the 5 hyponyms, you should return the k words which occurred the most times in the time range requested. For example, if someone entered words = "food, cake", startYear = 1950, endYear = 1990, and k = 5, then you would find the 5 most popular words in that time period that are hyponyms of both food and cake. Here, the popularity is defined as the total number of times the word appears over the entire time period. The words should be returned in alphabetical order. In this case, the answer is [biscuit, cake, kiss, snap, wafer] if we’re using top_49887_words.csv, synsets.txt, and hyponyms.txt. (EDIT 10/24/2022: An earlier version of this spec had you return words in order of decreasing popularity, but this was changed to alphabetical order to maintain consistency with the k = 0 case).

Note that if the front end doesn’t supply a year, default values of startYear = 1900 and endYear = 2020 are provided by NGordnetQueryHandler.readQueryMap.

If k = 0, or the user does not enter k (which results in a default value of zero), then the startYear and endYear should be totally ignored.

If a word never occurs in the time frame specified, i.e. the count is zero, it should not be returned. In other words, if k > 0, we should not show any words that do not appear in the ngrams dataset.

If there are no words that have non-zero counts, you should return an empty list, i.e. “[]”.

If there are fewer than k words with non-zero counts, return only those words. For example if you enter the word “potato” and enter “k = 15”, but only 7 hyponyms of potato have non-zero counts, you’d return only 7 words.

This task will be a little trickier since you’ll need to figure out how to pass information around so that the HyponymsHandler knows how to access a useful NGramMap.

IMPORTANT: DO NOT MAKE A STATIC NGRAMMAP FOR THIS TASK! It might be tempting to simply make some sort of public static NGramMap that can be accessed from anywhere in your code. This is called a “global variable”. We strongly discourage this way of thinking about programming, and instead suggest that you should be passing an NGramMap to either constructors or methods. We’ll come back to talking about this during the software engineering lectures.

Tips #

  • Until you use the autograder, you’ll need to construct your own test cases. We provide one above: words = "food, cake" , startYear = 1950, endYear = 1990, k = 5.
  • When constructing your own test cases, consider making your own input files. Using the large input files we provide is extremely tedious.
  • In the coming sections of this spec, we’ll tell you how to set up your code for submission to the autograder, and how to write your own JUnit tests to mimic the test cases provided by the grader.

Grading Details and Deliverables #

For Project 2b, the only required deliverable is the file, in addition to any helper classes. However, we will not be directly grading these classes, since they can vary from student to student.

This portion of project 2 will be worth 2400 points. The Gradescope autograder will be up on 10/25, and this section will be updated with additional grader details at that time.

Submitting Your Code, Automated Hyponym Testing, and Grader Compatibility #

Throughout this assignment, we’ve had you use your front end to test your code. Our grader is not sophisticated enough to pretend to be a web browser and call your code. Instead, we’ll need you to provide a method in the proj2b_testing.AutograderBuddy class that provides a handler that can deal with hyponyms requests.

When you ran git pull skeleton main at the start of this spec, the following five files should have been pulled into your repo:

  • proj2b_testing/
  • proj2b_testing/
  • proj2b_testing/
  • proj2b_testing/
  • proj2b_testing/

Open and fill in the getHyponymHandler method such that it returns a HyponymsHandler that uses the four given files. Your code here should be quite similar to your code in Then, run the class and ensure that your code passes the test in that file.

The four provided test files correspond to the cases that you solved in this project, that is:

  • Finding hyponyms of a single word where k = 0.
  • Finding hyponyms of multiple words where k = 0 (e.g. gallery, bowl).
  • Finding hyponums of a single word where k > 0.
  • Finding hyponyms of multiple words where k > 0.

After you’ve passed the first test file, try running the tests in Note that you’ll need to fill in two of the test with information from this spec. You can use this technique to attempt additional queries, for example if you’re failing something on the autograder.

After passing TestMultiWordK0Hyponyms, try running, then

Now that you’ve created proj2b_testing.AutograderBuddy and passed all the local tests, you can submit to the autograder. If you fail any tests, you should be able to replicate them locally as JUnit tests by building on the test files above. If any additional datafiles are needed, they will be added to this section as links.

Optional: Adding New Buttons #

The remainder of this assignment is optional, but strongly recommended.

Getting a list of hyponyms is cool, but what can sometimes be even cooler is plotting their relative frequencies. For example, if the user enters the words “food, cake”, startYear = 1900, endYear = 2020, k = 8 and clicks “Hypohist”, they’d be able to see the relative frequency of the 8 most popular words which were hyponyms of food and cake over the time period between 1900 and 2020.

In this part, you’ll edit three different types of files:

  • HTML
  • JavaScript
  • Java

We assume that you have NO prior familiarity with HTML or JavaScript. It is very common in real world projects to have to modify code with which you are not familiar, even possibly in programming languages you have never seen.

Adding the Hypohist and Hypohist (Text) Buttons #

Open the ngordnet.html file. Locate the code that creates the existing buttons, e.g. History and Hyponyms. Using your intuition, copy and paste the pieces of code that you think are necessary to create two new buttons that say “Hypohist” and “Hypohist (text)”.

When you’re done, try clicking the Hypohist button, and nothing will happen.

Creating a Hypohist Handler #

Back in Ngordnet.main.Main, register a new Handler called HypohistHandler. It should be registered to the String hypohist. This handler should simply return the text “hello i am hypohist”. Run your Java server, and it is now ready to listen for Hypohist clicks.

With your server running, try clicking the Hypohist button, and … still nothing will happen!

Hacking Exercise: Setting Up JavaScript Callbacks #

Even though our server is listening for Hypohist clicks, and we are clicking the Hypohist button, nothing is happening!

That is, your browser isn’t even trying to send the query over to your Java file. This is because HTML code is generally dumb, i.e. basically doesn’t do anything but specify what the website should look like.

The language typically used to describe how a page works is called JavaScript. Despite the name, it has literally nothing to do with Java, and is widely believed to have been a marketing ploy (see this page or this video by JavaScript’s creator Brendan Eich) in the mid 1990s when Java was new and cool, and JavaScript was just coming into existence.

Let’s peer inside the dark universe of front end JavaScript programming. Open “ngordnet.js”. This is the code that acts as the middleman between the beautiful (?) visual user interface in the browser and your Java code. Note that the HTML and Javascript files for this project are not up to professional standards, and I honestly hacked them together pretty quickly, keeping them as simple as possible so you would feel at least slightly comfortable playing around with them.

Your difficult task: Try modifying the code so that when you click the “Hypohist” button, you successfully get back the text outputted by your HypohistHandler, which should be “hello i am hypohist” if you used my exact suggestion above.

The very old school word for this process of just fumbling your way through a quick and dirty programming job is “hacking”, though the word has many competing meaning these days.


  • Pattern match carefully!
  • Feel free to edit, test, and experiment. You’re not going to break anything permanently.
  • Use git checkout to get the original version of the JS file if you break something.
  • Don’t cheat by just asking someone what to do. This skill of editing and experimenting with code you don’t understand is VERY important when prototyping and hacking together code.
  • In the real world, production code should never ship that was created via this hacking process. However, it can be very useful for prototyping!

Hypohist #

Next, fill out the handler for the Hypohist button so that it behaves as expected, that is, this button should return a plot of the relative frequency of the words returned by Hyponyms over the period stated.

That is, we’ll do what we said above: For example, if the user enters the words "food, cake", sets startYear=1900, endYear=2020 and k=8, and clicks the “Hypohist” button, they’d be able to see the relative frequency of the 8 most popular words which were hyponyms of food and cake over the time period between 1900 and 2020.

Note: Behavior is pretty straightforward if k > 0 for Hypohist. If k = 0, it’s not clear what should happen. Maybe come up with a cool idea.

Adding Even More Features #

Optionally, add even more features to your tool. Some possibilities:

  • Adding additional buttons that use one or both datasets in some creative way. For example, you might plot the average length of all words in a given year. Or you might create a visualization of all of a words’ hyponyms. Or you might have a feature that prints the shortest path between two words.
  • The hyponyms search finds all hyponyms, no matter how distant from the source. For example, there are a huge number of hyponyms of “dog”. Add a new field d, which finds only words that are at a distance of d or less from the given words.
  • Have a ! operator, e.g. if someone enters “!person, leader”, your code will find all leaders which are not a person.

Acknowledgements #

The WordNet part of this assignment is loosely adapted from Alina Ene and Kevin Wayne’s Wordnet assignment at Princeton University.

Last built: 2023-04-08 01:29 UTC