In situations like this, I think it's less helpful to dwell on abstract models of the domain (domains can be modelled very differently depending on perspective) and more consider the work that needs to be completed and focus on encapsulation and - more specifically - the concept of *tell, don't ask*.

If I want to calculate a GPA, it's clearly not the Student's job to know how to do that. So I want a GPA Calculator, which I'm going to pass to the Student. The student can pass that through its Notes and each Note can report itself to the GPA calculator, which will calculate the GPA according to some algorithm that both Student and Notes are ignorant of.

Something along the lines of:

 public class Student {
 private List<Note> notes;

 public void CalculateGpa(GPACalculator calculator) {
 foreach(var note in notes) {
 note.ReportValue(calculator);
 }
 }
 }

 public class Note {
 private double value;
 
 public void ReportValue(GPACalculator calculator) {
 calculator.AddNoteValue(value);
 }
 }

 public class GPACalculator {
 private List<double> values;

 public void AddNoteValue(double value) {
 values.Add(value);
 }
 }

Then, you simply pass your calculator to your student and end up with a fully populated GPACalculator (either storing the note values as above and then calculating at the end, or calculating as you go, depending on what makes the most sense algorithmically).

Your GPACalculator is ignorant of your Student and its properties, your Student is ignorant of your GPA calculation.

When you want to retrieve your GPA, you can either expose a property from the GPA calculator or (better) have another method that allows you to pass another object for your GPACalculator to report its calculated GPA to.

"Tell, Don't Ask" is, for me, the essence of good OOP.

It's of course very important to remember that the *storage* model and the *domain* model are not necessarily the same as the concerns for the two things are very different.