課題07-1 点と線分#
点(Point)と線分(Line)を表す2つのクラスを作成してください。
Pointクラスは、2次元平面上の点を表すクラスです。x座標とy座標を持ち、コンストラクタで初期化します。- 他の
Pointオブジェクトからの距離を計算するメソッドdistanceを持ちます。- 他の
Pointオブジェクトが渡されなかった場合は、原点(0, 0)からの距離を計算します。
- 他の
__str__メソッドでは、f"({self._x}, {self._y})"という文字列表現を返します。
Lineクラスは、2つのPointオブジェクトからなる線分を表すクラスです。- 2つの
Pointオブジェクトを持ち、コンストラクタで初期化します。 - 線分の長さを計算するメソッド
distanceを持ちます。 __str__メソッドでは、f"({self._p1}, {self._p2})"という文字列表現を返します。is_onメソッドは、引数にPointオブジェクトを受け取り、その点が線分上にあればTrueを返し、そうでなければFalseを返します。- 線分は両端の点を含みます。端点と一致する点は線分上にあるものとします。
- 2つの端点が同じ点(長さ 0 の線分)のときは、その点と一致する点だけが線分上にあるものとします。
- 2つの
is_on の判定方法#
点 \(P\) が線分 \(AB\) 上にあるかどうかは、次の 2つの条件を両方 満たすかどうかで判定します。
- \(P\) が \(A\)、\(B\) を通る 直線 の上にある。
- \(P\) が \(A\) と \(B\) の 間 にある。すなわち、\(x\) 座標、\(y\) 座標のそれぞれについて、\(A\) と \(B\) の座標の小さい方以上、大きい方以下である。
条件 1 だけでは、線分を延長した直線上の点(例えば線分 \((1, 1)\)〜\((5, 5)\) に対する \((0, 0)\))も線分上と判定してしまいます。
条件 1 は、\(A = (x_1, y_1)\)、\(B = (x_2, y_2)\)、\(P = (x, y)\) として、 \((x_2 - x_1)(y - y_1) - (y_2 - y_1)(x - x_1)\) が \(0\) かどうかで判定できます(\(AB\) と \(AP\) が平行なら \(0\) になります)。 傾きを求めて \(y = ax + b\) に代入する方法でも構いませんが、その場合は \(x_1 = x_2\) となる垂直な線分で 0 割りになるので、別に扱う必要があります。
ただし、浮動小数点数(float)の計算には誤差があり、本来 \(0\) になるはずの値が \(0\) ちょうどにならないことがあります(例えば 0.1 * 3 - 0.3 は 0 ではなく 5.55e-17 になります)。
そのため、求めた値 got が 0 かどうかは got == 0 ではなく、abs(got) < 0.00001 のように誤差を許容して判定してください。
以下のプログラム(assignment07-1.py)を実行したときに、テストが全て通るように plane_figures.py を作成してください。
import plane_figures as pf
p1 = pf.Point(3, 4)
assert abs(p1.distance() - 5.0) < 0.00001, "原点からの距離が正しくありません"
assert abs(p1.distance(p1)) < 0.00001, "同じ点からの距離が正しくありません"
assert str(p1) == "(3, 4)", "文字列表現が正しくありません"
p2 = pf.Point(1, 1)
assert abs(p2.distance() - 2 ** 0.5) < 0.00001, "原点からの距離が正しくありません"
l1 = pf.Line(p2, pf.Point(5, 5))
assert abs(l1.distance() - 32 ** 0.5) < 0.00001, "線分の長さが正しくありません"
assert str(l1) == "((1, 1), (5, 5))", "文字列表現が正しくありません"
assert l1.is_on(pf.Point(3, 3)), "点 (3, 3) は線分上にあるはずです"
assert l1.is_on(pf.Point(1, 1)), "端点 (1, 1) は線分上にあるはずです"
assert l1.is_on(pf.Point(5, 5)), "端点 (5, 5) は線分上にあるはずです"
assert not l1.is_on(pf.Point(0, 0)), "点 (0, 0) は直線上にはありますが、線分の外なので線分上にはないはずです"
assert not l1.is_on(pf.Point(6, 6)), "点 (6, 6) は直線上にはありますが、線分の外なので線分上にはないはずです"
assert not l1.is_on(pf.Point(2, 3)), "点 (2, 3) は線分上にないはずです"
l2 = pf.Line(pf.Point(2, 0), pf.Point(2, 4)) # 垂直な線分
assert l2.is_on(pf.Point(2, 2)), "点 (2, 2) は垂直な線分上にあるはずです"
assert not l2.is_on(pf.Point(2, 5)), "点 (2, 5) は垂直な線分の外なので線分上にはないはずです"
assert not l2.is_on(pf.Point(3, 2)), "点 (3, 2) は垂直な線分上にないはずです"
l3 = pf.Line(pf.Point(0, 0), pf.Point(1, 3)) # 浮動小数点の誤差が出る例
assert l3.is_on(pf.Point(0.1, 0.3)), "点 (0.1, 0.3) は線分上にあるはずです(誤差を許容していますか)"
l4 = pf.Line(pf.Point(1, 1), pf.Point(1, 1)) # 長さ 0 の線分
assert l4.is_on(pf.Point(1, 1)), "長さ 0 の線分上にあるのは端点だけです"
assert not l4.is_on(pf.Point(2, 2)), "点 (2, 2) は長さ 0 の線分上にないはずです"課題07-2 乱数ジェネレータ#
以下の特徴を持つ乱数生成クラス MyRandom(my_random.py)を作成してください。
MyRandomクラスは、線形合同法(Linear Congruential Generator; LCG)による乱数生成器です。- 線形合同法は \(X_{n+1} = (a * X_n + c) \mod m\) という漸化式で表されます。
- 上記の
aは乗数(multiplier)、cは加数(increment)、mは剰余(modulus)と呼びます。
- 上記の
- 線形合同法は \(X_{n+1} = (a * X_n + c) \mod m\) という漸化式で表されます。
- コンストラクタは、
seedとmodulusとmultiplierとincrementを受け取り、初期化します。seedは乱数の初期値です。デフォルト値は0です。modulusは剰余演算に使う値です。デフォルト値は2 ** 32です。multiplierは乗算に使う値です。デフォルト値は1664525です。incrementは加算に使う値です。デフォルト値は1013904223です。
nextメソッドは、次の0以上1未満の乱数を生成し、返します。- 漸化式の \(X_n\) には直前に生成した値を使います(最初は
seed)。生成した \(X_{n+1}\) は、次の呼び出しのためにオブジェクトに保存しておく必要があります。 - 得られた\(X_{n+1}\)は\(0\)以上\(m\)未満の整数となりますので、範囲を \(0\)以上\(1\)未満に変換するために、\(X_{n+1} / m\) を返します。
- 引数
skipを指定すると、skip個の乱数を読み飛ばしてから、その次の乱数を返します。 例えばnext(2)は乱数を 3 個生成し、最初の 2 個を捨てて 3 個目を返します。next()とnext(0)は同じ意味です。
- 漸化式の \(X_n\) には直前に生成した値を使います(最初は
class MyRandom:
def __init__(self): # 引数(seed、modulus, multiplier, increment)を追加してください。
pass # seed, modulus, multiplier, increment はデフォルト引数を持つようにしてください。
def next(self, skip = 0): # skip 個の乱数を読み飛ばしてから、次の乱数を返します。
for _ in range(skip):
self._next_impl()
return self._next_impl()
def _next_impl(self):
pass # 線形合同法により X_{n+1} を求めて保存し、X_{n+1} / m を返してください。以下のテストコード(assignment07-2.py)を通るようにしてください。
import my_random as mr
rg = mr.MyRandom(0, 2 ** 32, 1664525, 1013904223)
r1 = rg.next()
r2 = rg.next()
assert abs(r1 - 0.236067972844467) < 0.000001, "1個目の乱数が正しくありません"
assert abs(r2 - 0.278566908556968) < 0.000001, "2個目の乱数が正しくありません(next のたびに X_n を更新していますか)"
assert abs(rg.next(2) - 0.384077370865270) < 0.000001, "3, 4個目を読み飛ばして、5個目の乱数を返すはずです"
assert abs(rg.next() - 0.621807487215847) < 0.000001, "6個目の乱数が正しくありません"
rg2 = mr.MyRandom() # デフォルト引数は上と同じ値なので、同じ乱数列になる。
assert abs(rg2.next() - r1) < 0.000001, "デフォルト引数が正しくありません"
rg3 = mr.MyRandom(seed = 42)
assert abs(rg3.next() - 0.252345174783841) < 0.000001, "seed を変えると別の乱数列になるはずです"乱数の期待値との比較は abs(得られた値 - 期待値) < 0.000001 のように、差の 絶対値 で行っています。
得られた値 - 期待値 < 0.000001 と書くと、得られた値が期待値より小さければ何でも通ってしまう(例えば常に 0 を返す実装でも通る)ので注意してください。
課題07-3 ベクトル#
次のプログラム(assignment07-3.py)を実行したときに、テストが全て通るように vector.py を作成してください。
vector.py 中で Vectorクラスを定義し、以下のメソッドを用意してください。
__init__コンストラクタ。n個の引数(可変長)を受け取り、ベクトルを表す。__add__ベクトル同士の足し算を計算し、新しいベクトルを返す。__mul__ベクトル同士の内積を計算し、スカラー値を返す。__getitem__ベクトルの要素を取得する。__len__ベクトルの長さを返す。__str__ベクトルの文字列表現を返す。normベクトルのノルム(大きさ)を計算し、返す。
import vector as v
v1 = v.Vector(3, 4, 5)
v2 = v.Vector(1, 2, 3)
v3 = v1 + v2
assert v3[0] == 4 and v3[1] == 6 and v3[2] == 8, "ベクトルの足し算が正しくありません"
assert v1 * v2 == 26, "ベクトルの内積の計算が正しくありません"
assert str(v1) == "(3, 4, 5)", "ベクトルの文字列表現が正しくありません"
assert len(v1) == 3, "ベクトルの長さが正しくありません"
assert v1.norm() == 50 ** 0.5, "ベクトルのノルム(大きさ)の計算が正しくありません。"
v4 = v.Vector(1, 1, 1, 1, 1)
assert len(v4) == 5, "ベクトルの長さが正しくありません"
assert v4.norm() == 5 ** 0.5, "ベクトルのノルム(大きさ)の計算が正しくありません。"
assert v4 + v1 == None, "要素数の異なるベクトル同士の足し算は計算できないので、None が返るはずです。"
assert v1 * v4 == None, "要素数の異なるベクトル同士の内積は計算できないので、None が返るはずです。"なお、リストを可変長引数として受け取る方法は以下の通りです。
>>> list = [1, 2, 3]
>>> v1 = Vector(list) # これは、[1, 2, 3] という1つの要素を持つベクトルになってしまう。
>>> v1.elements
([1, 2, 3],) # ベクトルの要素は [1, 2, 3] という値一つ。
>>> v2 = Vector(*list) # このようにリスト変数の前に * をつけると、リストの要素を展開して引数として渡すことができる。
>>> v2.elements
(1, 2, 3)