# Macierze: operacje elementarne ## Tablice i pętle Tablice w Pythonie noszą nazwę list. Używamy ich następująco: - Stworzenie lity (tablicy): - z określonymi elementami: `tab = [1, 2, 3, 4, 5, 6, 10]` - o określonej liczbie elementów: `tab = [0] * 100` - Przypisanie albo odczytanie $k$-tego elementu listy (**indeksy od 0**): `tab[k] = 42` lub `tab[k]` - Sprawdzenie długości lity: `len(tab)` Python ma pętlę `while`, która powtarza podany zestaw instrukcji tak długo, jak długo podany warunek jest spełniony. Do warunków możemy równości ($a = b$ to `a == b`, $a \ne b$ to `a != b`), nierówności ($a < b$ to `a < b`, $a \le b$ to `a <= b`, podobnie nierówności w drugą stronę) i spójników logicznych `and` oraz `or`. ![Ilustracja składni instrukcji while wygenerowana przez Gemini](static/Gemini_generated/while_statement.png) :::{important} Napisanie: ```py while warunek: instrukcja1 instrukcja2 ... ``` Powoduje powtarzanie podanych instrukcji (1, 2, ...) tak długo, jak długo warunek jest prawdziwy. Warunek musi być wyrażeniem logicznym — takim o wartości `True` albo `False`. Na przykład: ```py pierwiastek = 0 while pierwiastek * pierwiastek < 1997: print(f"{pierwiastek} to nadal za mało") pierwiastek += 1 print("{pierwiastek} jest pierwszą liczbą, której kwadrat przekracza 1997") ``` ::: :::{topic} Zadanie Oblicz sumę wektorów z poniższej listy. Możesz kopiować kod stąd. Możesz zrobić to korzystając z Pythona interaktywnie, lub zrobić kolejny skrypt (w skrypcie również możesz wczytać inny skrypt jako moduł, np. `vectors.py`). ```py vectors = [ vectors.XY("vec1", 1, 2), vectors.XY("vec2", 5, 4), vectors.XY("vec3", math.log2(8), math.log2(math.pi)), vectors.XY("vec4", 1**3 + 2**3 + 3**3, 4**4 + 5**3 + 6**3), vectors.XY("vec5", math.log10(2), math.log2(10)) ] ``` ::: ## Podstawowa tabelka liczb Zaczniemy od napisania klasy, która pozwoli nam przechowywać dwuwymiarową tabelkę liczb: - `m = Matrix(nr, nc)` utworzy wypełnioną zerami macierz o `nr` wierszach i `nc` kolumnach. - `m.nrows()` zwróci liczbę wierszy. - `m.ncols()` zwróci liczbę kolumn. - `set(r, c, v)` ustawi w `r`-tym wierszu `k`-tej kolumny wartość `v`. - `get(r, c)` zwróci wartość w macierzy w `r`-tym wierszu `k`-tej kolumny. Kod startowy, który możesz wkleić do nowego pliku (nazwij go `matrix.py`): ```python class Matrix: def __init__(self, rows, cols): ... # TODO: zadanie 1 def nrows(self): ... # TODO: zadanie 3 def ncols(self): ... # TODO: zadanie 3 def get(self, row, col): ... # TODO: zadanie 3 def set(self, row, col, val): ... # TODO: zadanie 3 ``` ### Zadanie 1: tworzenie pustej macierzy Do pamiętania macierzy w naszym programie użyjemy listy list. Macierz $$M = \begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}$$ Będziemy pamiętać jako listę list: ```py lista = [[1, 2, 3], [4, 5, 6]] ``` Czyli `lista` zawiera kolejne wiersze macierzy: - `lista[0]` to pierwszy wiersz `[1,2,3]`, jego pierwszy element (czyli $m_{1\,1}$) to `lista[0][0]`. - `lista[1]` to drugi wiersz `[1, 2, 3]`, jego trzeci element (czyli $m_{2\,3}$) to `lista[1][2]`. W kodzie startowym uzupełnij funkcję `Matrix.__init__`, w której wewnątrz klasy w `self._data` zapamiętasz listę list samych zer o odpowiednich rozmiarach. :::{attention} Funkcje wewnątrz klasy wołamy (prawie) zawsze pisząc `zmienna.funkcja()`. Dlatego funkcje wewnątrz klasy `MojaKlasa` będziemy pisać jako `MojaKlasa.moja_funkcja()`. Funkcje spoza klasy będziemy pisać bez kropki - `moja_funkcja()`. ::: Możesz potem przetestować swoje rozwiązanie, dopisując na końcu pliku stworzenie zmiennej zawierającej macierz $\mathbb{R}^{4 \times 6}$: ```py m = Matrix(4, 6) ``` ### Zadanie 2: rozmiar macierzy Przypomnienie: `len(l)` to liczba elementów bezpośrednio w liście `l`. Zagadka: ile wynosi `len([1,2,[3,4,5]])`, dlaczego? Teraz: jak możemy powiedzieć tylko na podstawia listy wierszy w `self._data`, jakie są wymiary macierzy? Uzupełnij kod funkcji `Matrix.nrows()` i `Matrix.ncols()`. ### Zadanie 3: dostęp do elementów macierzy Uzupełnij teraz kod funkcji `Matrix.get` i `Matrix.set`. Opcjonalnie: dodaj do każdej z nich sprawdzenie, czy `r` i `c` nie są za duże, lub za małe: ```py if not (0 <= row < self.nrows() and 0 <= col < self.ncols()): raise IndexError(f"Niepoprawny index ({row}, {col}) w macierzy o wymiarach {self.nrows()}x{self.ncols()}") ``` Przetestuj, czy działa: zapisz do pliku macierz jednostkową o wymiarach 10 na 10: $$\begin{pmatrix} 1 & 0 & 0 & \cdots & 0 & 0 \\ 0 & 1 & 0 & \cdots & 0 & 0 \\ 0 & 0 & 1 & \cdots & 0 & 0 \\ \vdots & \vdots & \vdots & \ddots & \vdots & \vdots \\ 0 & 0 & 0 & \cdots & 1 & 0 \\ 0 & 0 & 0 & \cdots & 0 & 1 \end{pmatrix}$$ ## Operacje elementarne na macierzach Zaimplementujemy teraz kolejne funkcje do klasy macierzy, które wykorzystamy do rozwiązywania układów równań: - `Matrix.swap_rows(row1, row2)`: zamienia miejscami wiersze o podanych indeksach. - `Matrix.multiply(row, scalar)`: mnoży wiersz o podanym indeksie przez podaną wartość. - `Matrix.add_row(dst_row, scalar, src_row)`: dodaje do wiersza o indeksie `dst_row` wiersz o indeksie `src_row` pomnożony przez `scalar`. ### Zadanie 4: zamiana wierszy Uzupełnij kod funkcji `Matrix.swap_rows`. Pamiętaj, że w Pythonie indeksy zaczynają się od zera. Przykładowo, jeżeli macierz `m` to: $$\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}$$ to po wykonaniu `m.swap_rows(0, 1)` macierz `m` powinna być równa: $$\begin{pmatrix} 4 & 5 & 6 \\ 1 & 2 & 3 \end{pmatrix}$$ ### Zadanie 5: pomnożenie wiersza Uzupełnij kod funkcji `Matrix.multiply_row`, która pomnoży wiersz o danym indeksie (pierwszy wiersz ma *index* zero) przez podaną wartość. Przykładowo, jeżeli macierz `m` to: $$\begin{pmatrix} 1 & 2 & 3 \\ 4 & 5 & 6 \end{pmatrix}$$ to po wykonaniu `m.multiply_row(0, 2)` macierz `m` powinna być równa: $$\begin{pmatrix} 2 & 4 & 6 \\ 4 & 5 & 6 \end{pmatrix}$$ ### Zadanie 6: dodawanie pomnożonego wiersza Uzupełnij kod funkcji `Matrix.add_row`. Pamiętaj, że po jej wykonaniu jedynym zmienionym wierszem powinien być ten o indeksie podanym w pierwszym argumencie do funkcji; wszystkie pozostałe wiersze mają zostać, jak były. Przykładowo, jeśli macierz `m` jest równa: $$\begin{pmatrix} 1 & 1 & 1 \\ 6 & 7 & 7 \\ 1 & 2 & 3 \end{pmatrix}$$ To po wykonaniu `m.add_row(1, -6.0, 0)` powinna wynosić: $$\begin{pmatrix} 1 & 1 & 1 \\ 0 & 1 & 1 \\ 1 & 2 & 3 \end{pmatrix}$$ ### Wczytywanie i zapisywanie macierzy Możesz teraz dokleić to zaklęcie do swojego pliku `matrix.py`, żeby odblokować swoim macierzom skilla zapisywania do pliku i wczytywania z niego: ```py def read_matrix_file(filename): number_of_columns = None number_of_rows = 0 elements = [] with open(filename) as opened_file: for lineno, line in enumerate(opened_file): line_elements = line.split() if not line_elements: continue number_of_rows += 1 if number_of_columns is None: number_of_columns = len(line_elements) else: if len(line_elements) != number_of_columns: raise ValueError(f"{filename}:{lineno}: zła liczba elementów" f" - {len(line_elements)} zamiast {number_of_columns}") for element in line_elements: try: elements.append(float(element)) except ValueError: raise ValueError(f"{filename}:{lineno}: niepoprawna liczba '{element}'") result = Matrix(number_of_rows, number_of_columns) elements.reverse() for r in range(number_of_rows): for c in range(number_of_columns): result.set(r, c, elements.pop()) return result def write_matrix_file(matrix, filename=None): sizes = [0] * matrix.ncols() for r in range(matrix.nrows()): for c in range(matrix.ncols()): sizes[c] = max(sizes[c], len(str(matrix.get(r, c)))) if filename: opened_file = open(filename, "w") else: from sys import stdout opened_file = stdout for r in range(matrix.nrows()): for c in range(matrix.ncols()): if c != 0: opened_file.write(" ") opened_file.write(str(matrix.get(r, c)).rjust(sizes[c])) opened_file.write("\n") if filename: opened_file.close() ``` ## Instrukcje warunkowe Do rozwiązania tego zadania będziesz potrzebować instrukcji warunkowych. W Pythonie instrukcja warunkowa składa się z: - Głównej części `if warunek:` wraz z instrukcjami wykonywanymi, *gdy warunek jest spełniony* (czyli gdy wartość logiczna wyrażenia `warunek` to `True`); - Opcjonalnie: części `elif warunek:` wraz z instrukcjami wykonywanymi: gdy: - żadna z poprzednich części nie była wykonana oraz... - ...`warunek` jest prawdziwy. - Opcjonalnie: części `else:` wraz z instrukcjami wykonywanymi, gdy żadna z wcześniejszych części nie została wykonana. Przykładowo: ```py if ile == 0: print("Janek nie ma jabłek") elif ile == 1: print(f"Janek ma {ile} jabłko") elif 2 <= ile <= 4: print(f"Janek ma {ile} jabłka") else: print(f"Janek ma {ile} jabłek") ``` W jaki sposób możemy uzyskać ten sam efekt, co dodając część `else`, ale używając `elif`? :::{tip} :class: dropdown Napisać: ```py else: ... ``` to to samo, co: ```py elif True: ... ``` ::: Dlaczego potrzebujemy `elif`, skoro moglibyśmy (jak w C++) użyć `else` wraz z kolejnym `if`? :::{tip} :class: dropdown Problemem jest to, że Python wymusza zwiększenie wcięcia dla każdej kolejnej grupy instrukcji, więc pisząc jak w C++ musielibyśmy pisać: ```py if ile == 0: print("Janek nie ma jabłek") else: if ile == 1: print(f"Janek ma {ile} jabłko") else: if 2 <= ile <= 4: print(f"Janek ma {ile} jabłka") else: print(f"Janek ma {ile} jabłek") ``` ::: :::{important} Instrukcja warunkowa w Pythonie umożliwia wykonywanie różnego kodu, zależnie od prawdziwości warunków, które odwołują się do wartości zmiennych: ```py if jablka > pomarancze: print(f"Mam więcej jabłek niż pomarańczy") elif jablka < pomarancze: print(f"Mam więcej pomarańczy niż jabłek") else: print(f"Mam tyle samo jabłek, co pomarańczy") ``` ::: ![Ilustracja składni instrukcji warunkowej wygenerowana przez Gemini](static/Gemini_generated/cond_statement.png) ### Zadanie 7: wypisywanie macierzy jako układu równań Napisz funkcję `Matrix.eqs_print()`, która wypisze macierz w postaci układu równań. Na przykład dla macierzy: $$\begin{pmatrix} 1 & 2 & 3 & 4\\ 1 & -2 & 3 & -2 \\ 4 & 0 & 6 & 4 \end{pmatrix}$$ Powinna wypisać: ``` x1 + 2*x2 + 3*x3 = 4 x1 - 2*x2 + 3*x3 = -2 4*x1 + 6*x3 = 4 ``` Zwróć uwagę, że: - Kiedy współczynik przy jakiejś zmiennej wynosi 0, to w ogóle nic tam nie wypisujemy ($x_2$ w trzecim równaniu). - Kiedy współczynnik przy jakiejś zmiennej wynosi 1, to pomijamy jego wartość ($x_1$ w pierwszych dwóch równaniach). - Kiedy współczynnik jest ujemny, to piszemy $-$ zamiast $+$ ($x_2$ w drugim równaniu). ## Zadania domowe :::{topic} Zadanie `m.rotate` **[1 punkt]** Napisz funkcję `rotate_2d(alpha)`, która dla podanego w stopniach kąta `alpha` zwróci macierz obrotu o ten kąt. ::: :::{topic} Zadanie `m.invert22` **[1 punkt]** Napisz funkcję `invert_2x2(m1)`, która dla podanej macierzy $M_1$ o dwóch wierszach i dwóch kolumnach zwróci macierz $M_2$ taką, że $M_1 \cdot M_2 = \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}$. ::: :::{topic} Zadanie: `m.eqs` **[2 punkty]** Wykorzystując metody `Matrix.add_row()` oraz `Matrix.swap_rows()`, rozwiąż metodą eliminacji Gaussa następujący układ równań o pięciu niewiadomych: $$ \begin{cases} 0 \cdot x_1 + 1 \cdot x_2 + 1 \cdot x_3 + 1 \cdot x_4 + 1 \cdot x_5 = 14 \\ 1 \cdot x_1 + 1 \cdot x_2 + 1 \cdot x_3 + 1 \cdot x_4 + 1 \cdot x_5 = 15 \\ 1 \cdot x_1 + 1 \cdot x_2 + 2 \cdot x_3 + 2 \cdot x_4 + 2 \cdot x_5 = 27 \\ 1 \cdot x_1 + 1 \cdot x_2 + 2 \cdot x_3 + 3 \cdot x_4 + 3 \cdot x_5 = 36 \\ 1 \cdot x_1 + 1 \cdot x_2 + 2 \cdot x_3 + 3 \cdot x_4 + 4 \cdot x_5 = 41 \end{cases} $$ Zacznij od zainicjowania macierzy rozszerzonej (o rozmiarze $5 \times 6$) w Pythonie za pomocą poniższego kodu: ```python m = Matrix(5, 6) # Wiersz 0 m.set(0, 0, 0); m.set(0, 1, 1); m.set(0, 2, 1); m.set(0, 3, 1); m.set(0, 4, 1); m.set(0, 5, 14) # Wiersz 1 m.set(1, 0, 1); m.set(1, 1, 1); m.set(1, 2, 1); m.set(1, 3, 1); m.set(1, 4, 1); m.set(1, 5, 15) # Wiersz 2 m.set(2, 0, 1); m.set(2, 1, 1); m.set(2, 2, 2); m.set(2, 3, 2); m.set(2, 4, 2); m.set(2, 5, 27) # Wiersz 3 m.set(3, 0, 1); m.set(3, 1, 1); m.set(3, 2, 2); m.set(3, 3, 3); m.set(3, 4, 3); m.set(3, 5, 36) # Wiersz 4 m.set(4, 0, 1); m.set(4, 1, 1); m.set(4, 2, 2); m.set(4, 3, 3); m.set(4, 4, 4); m.set(4, 5, 41) ``` **Uwaga:** rozwiązaniem nie jest tylko wynik, ale cały kod, który schodkuje tę macierz. ::: :::{topic} Zadanie: `m.check` **[2 punkty]** Napisz funkcję `Matrix.check_solution(xs)`, która dla rozszerzonej macierzy układu równań sprawdzi, czy podana lista wartości zmiennych jest rozwiązaniem układu. Ze względu na prawdopodobne błędy przbliżeń na komputerze musisz sprawdzić, czy lewa strona każdego równania różni się od prawej o co najwyżej $10^{-6}$. **Uwaga:** możesz dostać punkty za co najwyżej jedno z zadań `m.check` i `m.check2`. ::: :::{topic} Zadanie `m.check2` **[3 punkty]** Rozwiąż zadanie `m.check`, ale zamiast dopuszczać $10^{-6}$ jako maksymalny dopuszczalny błąd, dopuszczaj błąd o wartości $10^{-6} \cdot m$, gdzie $m$ to największa wartość bezwzględna współczynnika występującego w macierzy. Przykładowo dla poniżeszej macierzy $m=4$ (od pierwszego elementu drugiego wiersza): $$\begin{pmatrix} 1 & -2 & 3 \\ -4 & 3 & 2 \end{pmatrix}$$ Natomiast dla tej macierzy, $m=101$ (od ostatniego elementu pierwszego wiersza): $$\begin{pmatrix} 0 & -3 & 50 \\ -44 & -99 & 101 \end{pmatrix}$$ **Uwaga:** możesz dostać punkty za co najwyżej jedno z zadań `m.check` i `m.check2`. :::