From 39d056fb47d3f65cee4fde0794646744c6f2c801 Mon Sep 17 00:00:00 2001 From: snowy Date: Tue, 27 Aug 2024 21:39:36 +0800 Subject: [PATCH] =?UTF-8?q?:bug:=20fix=20=CE=B5=20accuracy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 +- main.py | 189 ++++++++---------------------------- mbcp/mp_math/angle.py | 20 ++-- mbcp/mp_math/line.py | 192 +++++++++++++++++++------------------ mbcp/mp_math/plane.py | 159 ++++++++++++++++++++++++++---- mbcp/mp_math/point.py | 35 ++++--- mbcp/mp_math/utils.py | 35 +++++++ mbcp/mp_math/vector.py | 93 +++++++++++------- py.typed | 0 pyproject.toml | 23 +++++ requirements.txt | 2 - tests/__init__.py | 0 tests/answer.py | 32 +++++++ tests/test_line3.py | 15 +-- tests/test_plane3.py | 6 +- tests/test_vector3.py | 14 ++- tests/test_word_problem.py | 51 ++++++++-- 17 files changed, 518 insertions(+), 356 deletions(-) create mode 100644 py.typed create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/answer.py diff --git a/.gitignore b/.gitignore index 3d1b3b5..ef5ce8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ *script* -.idea* \ No newline at end of file +.idea* + +# pdm toolchain +.pdm-build +.pdm-python +pdm.lock +.pdm-build/ \ No newline at end of file diff --git a/main.py b/main.py index 4e86335..724f01b 100644 --- a/main.py +++ b/main.py @@ -1,153 +1,46 @@ -# -*- coding: utf-8 -*- -""" -Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved - -@Time : 2024/8/6 下午1:30 -@Author : snowykami -@Email : snowykami@outlook.com -@File : main.py -@Software: PyCharm -""" -import logging - -from mbcp.mp_math.line import Line3 -from mbcp.mp_math.plane import Plane3 -from mbcp.mp_math.point import Point3 - -# def ac8s4e4(): -# """ -# 第八章第四节例4 -# 问题:求与两平面x-4z-3=0和2x-y-5z-1=0的交线平行且过点(-3, 2, 5)的直线方程。 -# """ -# correct_ans = Line3(4, 3, 1, 1) -# -# pl1 = Plane3(1, 0, -4, -3) -# pl2 = Plane3(2, -1, -5, -1) -# p = Point3(-3, 2, 5) -# """解法1""" -# # 求直线方向向量s -# s = pl1.normal @ pl2.normal -# actual_ans = Line3.from_point_and_direction(p, s) -# -# logging.info(f"正确答案:{correct_ans} 实际答案:{actual_ans}") -# assert actual_ans == correct_ans -# -# """解法2""" -# # 过点p且与pl1平行的平面pl3 -# pl3 = pl1.cal_parallel_plane3(p) -# # 过点p且与pl2平行的平面pl4 -# pl4 = pl2.cal_parallel_plane3(p) -# # 求pl3和pl4的交线 -# actual_ans = pl3.cal_intersection_line3(pl4) -# print(pl3, pl4, actual_ans) -# -# logging.info(f"正确答案:{correct_ans} 实际答案:{actual_ans}") -# assert actual_ans == correct_ans -# -# -# ac8s4e4() -import logging - -from mbcp.mp_math.mp_math_typing import RealNumber -from mbcp.mp_math.utils import Approx +from typing import overload -def three_var_func(x: RealNumber, y: RealNumber) -> RealNumber: - return x ** 3 * y ** 2 - 3 * x * y ** 3 - x * y + 1 +class Vector: + def __init__(self, x: float, y: float, z: float): + """ + 向量 + Args: + x: x轴分量 + y: y轴分量 + z: z轴分量 + """ + self.x = x + self.y = y + self.z = z + @overload + def __mul__(self, other: float) -> 'Vector': + ... + + @overload + def __mul__(self, other: 'Vector') -> float: + ... + + def __mul__(self, other): + """ + 点乘和数乘 + Args: + other: + + Returns: + """ + if isinstance(other, (float, int)): + return Vector(self.x * other, self.y * other, self.z * other) + elif isinstance(other, Vector): + return self.x * other.x + self.y * other.y + self.z * other.z + else: + raise TypeError(f"unsupported operand type(s) for *: 'Vector' and '{type(other)}'") + + def __rmul__(self, other: float) -> 'Vector': + return self.__mul__(other) -class TestPartialDerivative: - # 样例来源:同济大学《高等数学》第八版下册 第九章第二节 例6 - def test_2v_1o_1v(self): - """测试二元函数关于第一个变量(x)的一阶偏导 df/dx""" +v: Vector = Vector(1, 2, 3) * 3.0 +v2: Vector = 3.0 * Vector(1, 2, 3) - from mbcp.mp_math.utils import Approx - from mbcp.mp_math.equation import get_partial_derivative_func - - partial_derivative_func = get_partial_derivative_func(three_var_func, 0) - - # assert partial_derivative_func(1, 2, 3) == 4.0 - def df_dx(x, y): - """原函数关于x的偏导""" - return 3 * (x ** 2) * (y ** 2) - 3 * (y ** 3) - y - - logging.info(f"Expected: {df_dx(1, 2)}, Actual: {partial_derivative_func(1, 2)}") - assert Approx(partial_derivative_func(1, 2)) == df_dx(1, 2) - - def test_2v_1o_2v(self): - """测试二元函数关于第二个变量(y)的一阶偏导 df/dy""" - - from mbcp.mp_math.utils import Approx - from mbcp.mp_math.equation import get_partial_derivative_func - - partial_derivative_func = get_partial_derivative_func(three_var_func, 1) - - def df_dy(x, y): - """原函数关于y的偏导""" - return 2 * (x ** 3) * y - 9 * x * (y ** 2) - x - - logging.info(f"Expected: {df_dy(1, 2)}, Actual: {partial_derivative_func(1, 2)}") - assert Approx(partial_derivative_func(1, 2)) == df_dy(1, 2) - - def test_2v_2o_12v(self): - """高阶偏导d^2f/(dxdy)""" - - from mbcp.mp_math.utils import Approx - from mbcp.mp_math.equation import get_partial_derivative_func - - partial_derivative_func = get_partial_derivative_func(three_var_func, (0, 1)) - - def df_dxdy(x, y): - """原函数关于y和x的偏导""" - return 6 * x ** 2 * y - 9 * y ** 2 - 1 - - logging.info(f"Expected: {df_dxdy(1, 2)}, Actual: {partial_derivative_func(1, 2)}") - assert Approx(partial_derivative_func(1, 2)) == df_dxdy(1, 2) - - def test_2v_2o_1v2(self): - """二阶偏导d^2f/(dx^2)""" - - from mbcp.mp_math.utils import Approx - from mbcp.mp_math.equation import get_partial_derivative_func - - partial_derivative_func = get_partial_derivative_func(three_var_func, (0, 0)) - - def df_dydx(x, y): - """原函数关于x和y的偏导""" - return 6 * x * y ** 2 - - logging.info(f"Expected: {df_dydx(1, 2)}, Actual: {partial_derivative_func(1, 2)}") - assert Approx(partial_derivative_func(1, 2)) == df_dydx(1, 2) - - def test_2v_3o_1v3(self): - """高阶偏导d^3f/(dx^3)""" - - from mbcp.mp_math.utils import Approx - from mbcp.mp_math.equation import get_partial_derivative_func - - partial_derivative_func = get_partial_derivative_func(three_var_func, (0, 0, 0)) - - def d3f_dx3(x, y): - """原函数关于x的三阶偏导""" - return 6 * (y ** 2) - - logging.info(f"Expected: {d3f_dx3(1, 2)}, Actual: {partial_derivative_func(1, 2)}") - assert Approx(partial_derivative_func(1, 2)) == d3f_dx3(1, 2) - - def test_possible_error(self): - from mbcp.mp_math.equation import get_partial_derivative_func - def two_vars_func(x: RealNumber, y: RealNumber) -> RealNumber: - return x ** 2 * y ** 2 - - partial_func = get_partial_derivative_func(two_vars_func, 0) - partial_func_2 = get_partial_derivative_func(two_vars_func, (0, 0)) - assert Approx(partial_func_2(1, 2)) == 8 - - -TestPartialDerivative().test_2v_1o_1v() -TestPartialDerivative().test_2v_1o_2v() -TestPartialDerivative().test_2v_2o_12v() -TestPartialDerivative().test_2v_2o_1v2() -TestPartialDerivative().test_2v_3o_1v3() - -TestPartialDerivative().test_possible_error() +print(v, v2) \ No newline at end of file diff --git a/mbcp/mp_math/angle.py b/mbcp/mp_math/angle.py index 5dc140f..f339c06 100644 --- a/mbcp/mp_math/angle.py +++ b/mbcp/mp_math/angle.py @@ -10,7 +10,7 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved """ from typing import overload -from .const import PI +from .const import PI # type: ignore class AnyAngle: @@ -27,7 +27,7 @@ class AnyAngle: self.radian = value * PI / 180 @property - def complementary(self) -> "AnyAngle": + def complementary(self) -> 'AnyAngle': """ 余角:两角的和为90°。 Returns: @@ -36,7 +36,7 @@ class AnyAngle: return AnyAngle(PI / 2 - self.minimum_positive.radian, is_radian=True) @property - def supplementary(self) -> "AnyAngle": + def supplementary(self) -> 'AnyAngle': """ 补角:两角的和为180°。 Returns: @@ -54,7 +54,7 @@ class AnyAngle: return self.radian * 180 / PI @property - def minimum_positive(self) -> "AnyAngle": + def minimum_positive(self) -> 'AnyAngle': """ 最小正角。 Returns: @@ -63,7 +63,7 @@ class AnyAngle: return AnyAngle(self.radian % (2 * PI)) @property - def maximum_negative(self) -> "AnyAngle": + def maximum_negative(self) -> 'AnyAngle': """ 最大负角。 Returns: @@ -71,21 +71,21 @@ class AnyAngle: """ return AnyAngle(-self.radian % (2 * PI), is_radian=True) - def __add__(self, other: "AnyAngle") -> "AnyAngle": + def __add__(self, other: 'AnyAngle') -> 'AnyAngle': return AnyAngle(self.radian + other.radian, is_radian=True) - def __sub__(self, other: "AnyAngle") -> "AnyAngle": + def __sub__(self, other: 'AnyAngle') -> 'AnyAngle': return AnyAngle(self.radian - other.radian, is_radian=True) - def __mul__(self, other: float) -> "AnyAngle": + def __mul__(self, other: float) -> 'AnyAngle': return AnyAngle(self.radian * other, is_radian=True) @overload - def __truediv__(self, other: float) -> "AnyAngle": + def __truediv__(self, other: float) -> 'AnyAngle': ... @overload - def __truediv__(self, other: "AnyAngle") -> float: + def __truediv__(self, other: 'AnyAngle') -> float: ... def __truediv__(self, other): diff --git a/mbcp/mp_math/line.py b/mbcp/mp_math/line.py index 07f9537..93945ac 100644 --- a/mbcp/mp_math/line.py +++ b/mbcp/mp_math/line.py @@ -8,87 +8,55 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved @File : other.py @Software: PyCharm """ -import math -from typing import TYPE_CHECKING, overload +from typing import TYPE_CHECKING + +from .mp_math_typing import OneSingleVarFunc, RealNumber +from .utils import sign_format from .vector import Vector3 if TYPE_CHECKING: from .angle import AnyAngle - from .plane import Plane3 from .point import Point3 class Line3: - def __init__(self, a: float, b: float, c: float, d: float): + def __init__(self, point: 'Point3', direction: 'Vector3'): """ - 三维空间中的直线。 + 三维空间中的直线。由一个点和一个方向向量确定。 Args: - a: 直线方程的系数a - b: 直线方程的系数b - c: 直线方程的系数c - d: 直线方程的常数项d + point: 直线上的一点 + direction: 直线的方向向量 """ - self.a = a - self.b = b - self.c = c - self.d = d + self.point = point + self.direction = direction - def cal_angle(self, other: "Line3") -> "AnyAngle": + def cal_angle(self, other: 'Line3') -> 'AnyAngle': """ - 计算直线和直线或面之间的夹角。 + 计算直线和直线之间的夹角。 Args: - other: 另一条直线或面 + other: 另一条直线 Returns: 夹角弧度 Raises: TypeError: 不支持的类型 """ - if isinstance(other, Line3): - return self.direction.cal_angle(other.direction) - elif isinstance(other, Plane3): - return self.direction.cal_angle(other.normal).complementary # 方向向量和法向量的夹角的余角 - else: - raise TypeError(f"Unsupported type: {type(other)}") + return self.direction.cal_angle(other.direction) - @property - def direction(self) -> "Vector3": - """ - 直线的方向向量。 - Returns: - 方向向量 - """ - return Vector3(self.a, self.b, self.c) - - def cal_intersection(self, line: "Line3") -> "Point3": + def cal_intersection(self, other: 'Line3') -> 'Point3': """ 计算两条直线的交点。 Args: - line: 另一条直线 + other: 另一条直线 Returns: 交点 """ - - if self.is_parallel(line): + if self.is_parallel(other): raise ValueError("Lines are parallel and do not intersect.") - - if self.is_collinear(line): - raise ValueError("Lines are collinear and do not have a single intersection point.") - - if not self.is_coplanar(line): + if not self.is_coplanar(other): raise ValueError("Lines are not coplanar and do not intersect.") + return self.point + self.direction.cross(other.direction) - a1, b1, c1, d1 = self.a, self.b, self.c, self.d - a2, b2, c2, d2 = line.a, line.b, line.c, line.d - - t = (b1 * (c2 * d1 - c1 * d2) - b2 * (c1 * d1 - c2 * d2)) / (b1 * c2 - b2 * c1) - - x = self.a * t + self.b * (-d1 / self.b) - y = -self.b * t + self.a * (d1 / self.a) - z = 0 - - return Point3(x, y, z) - - def cal_perpendicular(self, point: "Point3") -> "Line3": + def cal_perpendicular(self, point: 'Point3') -> 'Line3': """ 计算直线经过指定点p的垂线。 Args: @@ -96,64 +64,78 @@ class Line3: Returns: 垂线 """ - a = -self.b - b = self.a - c = 0 - d = -(a * point.x + b * point.y + self.c * point.z) - return Line3(a, b, c, d) + return Line3(point, self.direction.cross(point - self.point)) - def is_parallel(self, line: "Line3") -> bool: + def get_point(self, t: RealNumber) -> 'Point3': + """ + 获取直线上的点。同一条直线,但起始点和方向向量不同,则同一个t对应的点不同。 + Args: + t: 参数t + Returns: + 点 + """ + return self.point + t * self.direction + + def get_parametric_equations(self) -> tuple[OneSingleVarFunc, OneSingleVarFunc, OneSingleVarFunc]: + """ + 获取直线的参数方程。 + Returns: + x(t), y(t), z(t) + """ + return (lambda t: self.point.x + self.direction.x * t, + lambda t: self.point.y + self.direction.y * t, + lambda t: self.point.z + self.direction.z * t) + + def is_parallel(self, other: 'Line3') -> bool: """ 判断两条直线是否平行。 - 直线平行的条件是它们的法向量成比例 Args: - line: 另一条直线 + other: 另一条直线 Returns: 是否平行 """ - return self.direction.is_parallel(line.direction) + return self.direction.is_parallel(other.direction) - def is_collinear(self, line: "Line3") -> bool: + def is_collinear(self, other: 'Line3') -> bool: """ 判断两条直线是否共线。 - 直线共线的条件是它们的法向量成比例且常数项也成比例 Args: - line: 另一条直线 + other: 另一条直线 Returns: 是否共线 """ - return self.is_parallel(line) and (self.d * line.b - self.b * line.d) / (self.a * line.b - self.b * line.a) == 0 + return self.is_parallel(other) and (self.point - other.point).is_parallel(self.direction) - def is_coplanar(self, line: "Line3") -> bool: + def is_coplanar(self, other: 'Line3') -> bool: """ 判断两条直线是否共面。 - 两条直线共面的条件是它们的方向向量和法向量的叉乘为零向量 Args: - line: 另一条直线 + other: 另一条直线 Returns: 是否共面 """ - direction1 = (-self.c, 0, self.a) - direction2 = (line.c, -line.b, 0) - cross_product = direction1[0] * direction2[1] - direction1[1] * direction2[0] - return cross_product == 0 + return self.direction.cross(other.direction).is_parallel(self.direction) + + def simplify(self): + """ + 简化直线方程,等价相等。 + 自体简化,不返回值。 + + 按照可行性一次对x y z 化 0 处理,并对向量单位化 + """ + self.direction.normalize() + # 平行与zy平面,x始终为0 + if self.direction.x == 0: + self.point.x = 0 + # 平行与xz平面,y始终为0 + if self.direction.y == 0: + self.point.y = 0 + # 平行与xy平面,z始终为0 + if self.direction.z == 0: + self.point.z = 0 @classmethod - def from_point_and_direction(cls, point: "Point3", direction: "Vector3") -> "Line3": - """ - 工厂函数 由点和方向向量构造直线(点向式构造)。 - Args: - point: 点 - direction: 方向向量 - Returns: - 直线 - """ - a, b, c = direction.x, direction.y, direction.z - d = -(a * point.x + b * point.y + c * point.z) - return cls(a, b, c, d) - - @classmethod - def from_two_points(cls, p1: "Point3", p2: "Point3") -> "Line3": + def from_two_points(cls, p1: 'Point3', p2: 'Point3') -> 'Line3': """ 工厂函数 由两点构造直线。 Args: @@ -163,21 +145,45 @@ class Line3: 直线 """ direction = p2 - p1 - return cls.from_point_and_direction(p1, direction) + return cls(p1, direction) + + def __and__(self, other: 'Line3') -> 'Point3': + """ + 计算两条直线点集合的交集。交点 + Args: + other: 另一条直线 + Returns: + 交点 + """ + return self.cal_intersection(other) def __eq__(self, other) -> bool: """ 判断两条直线是否等价。 + + v1 // v2 ∧ (p1 - p2) // v1 Args: other: Returns: """ - return self.a / other.a == self.b / other.b == self.c / other.c == self.d / other.d - - def __repr__(self): - return f"Line3({self.a}, {self.b}, {self.c}, {self.d})" + return self.direction.is_parallel(other.direction) and (self.point - other.point).is_parallel(self.direction) def __str__(self): - return f"Line3({self.a}, {self.b}, {self.c}, {self.d})" + """ + 返回点向式(x-x0) + Returns: + + """ + s = "Line3: " + if self.direction.x != 0: + s += f"(x{sign_format(-self.point.x)})/{self.direction.x}" + if self.direction.y != 0: + s += f" = (y{sign_format(-self.point.y)})/{self.direction.y}" + if self.direction.z != 0: + s += f" = (z{sign_format(-self.point.z)})/{self.direction.z}" + return s + + def __repr__(self): + return f"Line3({self.point}, {self.direction})" diff --git a/mbcp/mp_math/plane.py b/mbcp/mp_math/plane.py index 5041303..a979e62 100644 --- a/mbcp/mp_math/plane.py +++ b/mbcp/mp_math/plane.py @@ -3,13 +3,14 @@ 平面模块 """ import math -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, overload import numpy as np -from .vector import Vector3 +from .vector import Vector3, zero_vector3 from .line import Line3 from .point import Point3 +from .utils import sign if TYPE_CHECKING: from .angle import AnyAngle @@ -30,7 +31,7 @@ class Plane3: self.c = c self.d = d - def cal_angle(self, other: "Line3 | Plane3") -> "AnyAngle": + def cal_angle(self, other: 'Line3 | Plane3') -> 'AnyAngle': """ 计算平面与平面之间的夹角。 Args: @@ -43,11 +44,11 @@ class Plane3: if isinstance(other, Line3): return self.normal.cal_angle(other.direction).complementary elif isinstance(other, Plane3): - return AnyAngle(math.acos(self.normal * other.normal / (self.normal.length * other.normal.length)), is_radian=True) + return AnyAngle(math.acos(self.normal @ other.normal / (self.normal.length * other.normal.length)), is_radian=True) else: raise TypeError(f"Unsupported type: {type(other)}") - def cal_distance(self, other: "Plane3 | Point3") -> float: + def cal_distance(self, other: 'Plane3 | Point3') -> float: """ 计算平面与平面或点之间的距离。 Args: @@ -64,27 +65,58 @@ class Plane3: else: raise TypeError(f"Unsupported type: {type(other)}") - def cal_intersection_line3(self, other: "Plane3") -> "Line3": + def cal_intersection_line3(self, other: 'Plane3') -> 'Line3': """ 计算两平面的交线。该方法有问题,待修复。 Args: other: 另一个平面 Returns: 交线 + Raises: """ - # 计算两法向量的叉积作为交线的方向向量 - s = self.normal.cross(other.normal) # 交线的方向向量 - # 联立两平面方程求交线的一点 - # 两平面方程联立得到的方程组 - # | a1x + b1y + c1z = -d1 - # | a2x + b2y + c2z = -d2 - # 用numpy解方程组 - a = np.array([[self.a, self.b, self.c], [other.a, other.b, other.c]]) - b = np.array([-self.d, -other.d]) - p = np.linalg.lstsq(a, b, rcond=None)[0] - return Line3.from_point_and_direction(Point3(*p), s) + if self.normal.is_parallel(other.normal): + raise ValueError("Planes are parallel and have no intersection.") + direction = self.normal.cross(other.normal) # 法向量叉乘得到方向向量 + # 寻找直线上的一点,依次假设x=0, y=0, z=0,找到合适的点 + x, y, z = 0, 0, 0 + # 依次判断条件假设x=0, y=0, z=0,找到合适的点 + # 先假设其中一个系数不为0,则令此坐标为0,构建增广矩阵,解出另外两个坐标 + if self.a != 0 and other.a != 0: + A = np.array([[self.b, self.c], [other.b, other.c]]) + B = np.array([-self.d, -other.d]) + y, z = np.linalg.solve(A, B) + elif self.b != 0 and other.b != 0: + A = np.array([[self.a, self.c], [other.a, other.c]]) + B = np.array([-self.d, -other.d]) + x, z = np.linalg.solve(A, B) + elif self.c != 0 and other.c != 0: + A = np.array([[self.a, self.b], [other.a, other.b]]) + B = np.array([-self.d, -other.d]) + x, y = np.linalg.solve(A, B) - def cal_parallel_plane3(self, point: "Point3") -> "Plane3": + return Line3(Point3(x, y, z), direction) + + def cal_intersection_point3(self, other: 'Line3') -> 'Point3': + """ + 计算平面与直线的交点。 + Args: + other: 不与平面平行或在平面上的直线 + Returns: + 交点 + Raises: + ValueError: 平面与直线平行或重合 + """ + # 若平面的法向量与直线方向向量垂直,则直线与平面平行或重合 + if self.normal @ other.direction == 0: + raise ValueError("The plane and the line are parallel or coincident.") + # 获取直线的参数方程 + # 代入平面方程,解出t + x, y, z = other.get_parametric_equations() + t = (-(self.a * other.point.x + self.b * other.point.y + self.c * other.point.z + self.d) / + (self.a * other.direction.x + self.b * other.direction.y + self.c * other.direction.z)) + return Point3(x(t), y(t), z(t)) + + def cal_parallel_plane3(self, point: 'Point3') -> 'Plane3': """ 计算平行于该平面且过指定点的平面。 Args: @@ -95,7 +127,7 @@ class Plane3: return Plane3.from_point_and_normal(point, self.normal) @property - def normal(self) -> "Vector3": + def normal(self) -> 'Vector3': """ 平面的法向量。 Returns: @@ -104,7 +136,7 @@ class Plane3: return Vector3(self.a, self.b, self.c) @classmethod - def from_point_and_normal(cls, point: "Point3", normal: "Vector3") -> "Plane3": + def from_point_and_normal(cls, point: 'Point3', normal: 'Vector3') -> 'Plane3': """ 工厂函数 由点和法向量构造平面(点法式构造)。 Args: @@ -117,8 +149,93 @@ class Plane3: d = -a * point.x - b * point.y - c * point.z # d = -ax - by - cz return cls(a, b, c, d) + @classmethod + def from_three_points(cls, p1: 'Point3', p2: 'Point3', p3: 'Point3') -> 'Plane3': + """ + 工厂函数 由三点构造平面。 + Args: + p1: 点1 + p2: 点2 + p3: 点3 + Returns: + 平面 + """ + # 两个向量 + v1 = p2 - p1 + v2 = p3 - p1 + # 法向量 + normal = v1.cross(v2) + return cls.from_point_and_normal(p1, normal) + + @classmethod + def from_two_lines(cls, l1: 'Line3', l2: 'Line3') -> 'Plane3': + """ + 工厂函数 由两直线构造平面。 + Args: + l1: 直线1 + l2: 直线2 + Returns: + 平面 + """ + v1 = l1.direction + v2 = l2.point - l1.point + if v2 == zero_vector3: + v2 = l2.get_point(1) - l1.point + return cls.from_point_and_normal(l1.point, v1.cross(v2)) + + @classmethod + def from_point_and_line(cls, point: 'Point3', line: 'Line3') -> 'Plane3': + """ + 工厂函数 由点和直线构造平面。 + Args: + point: 面上一点 + line: 面上直线,不包含点 + Returns: + 平面 + """ + return cls.from_point_and_normal(point, line.direction) + def __repr__(self): return f"Plane3({self.a}, {self.b}, {self.c}, {self.d})" def __str__(self): - return f"{self.a}x + {self.b}y + {self.c}z + {self.d} = 0" + s = "Plane3: " + if self.a != 0: + s += f"{sign(self.a, only_neg=True)}{abs(self.a)}x" + if self.b != 0: + s += f" {sign(self.b)} {abs(self.b)}y" + if self.c != 0: + s += f" {sign(self.c)} {abs(self.c)}z" + if self.d != 0: + s += f" {sign(self.d)} {abs(self.d)}" + return s + " = 0" + + @overload + def __and__(self, other: 'Line3') -> 'Point3 | None': + ... + + @overload + def __and__(self, other: 'Plane3') -> 'Line3 | None': + ... + + def __and__(self, other): + """ + 取两平面的交集(人话:交线) + Args: + other: + Returns: + 不平行平面的交线,平面平行返回None + """ + if isinstance(other, Plane3): + if self.normal.is_parallel(other.normal): + return None + return self.cal_intersection_line3(other) + elif isinstance(other, Line3): + if self.normal @ other.direction == 0: + return None + return self.cal_intersection_point3(other) + else: + raise TypeError(f"unsupported operand type(s) for &: 'Plane3' and '{type(other)}'") + + def __rand__(self, other: 'Line3') -> 'Point3': + return self.cal_intersection_point3(other) diff --git a/mbcp/mp_math/point.py b/mbcp/mp_math/point.py index 4583994..ff6ce9f 100644 --- a/mbcp/mp_math/point.py +++ b/mbcp/mp_math/point.py @@ -8,9 +8,10 @@ class Point3: def __init__(self, x: float, y: float, z: float): """ 笛卡尔坐标系中的点。 - :param x: - :param y: - :param z: + Args: + x: x 坐标 + y: y 坐标 + z: z 坐标 """ self.x = x self.y = y @@ -31,26 +32,30 @@ class Point3: """ P + V -> P P + P -> P - :param other: - :return: + Args: + other: + Returns: """ return Point3(self.x + other.x, self.y + other.y, self.z + other.z) + def __eq__(self, other): + """ + 判断两个点是否相等。 + Args: + other: + Returns: + """ + return self.x == other.x and self.y == other.y and self.z == other.z + def __sub__(self, other: "Point3") -> "Vector3": """ P - P -> V P - V -> P 已在 :class:`Vector3` 中实现 - :param other: - :return: + Args: + other: + Returns: + """ from .vector import Vector3 return Vector3(self.x - other.x, self.y - other.y, self.z - other.z) - - def __truediv__(self, other: float) -> "Point3": - """ - P / n -> P - :param other: - :return: - """ - return Point3(self.x / other, self.y / other, self.z / other) diff --git a/mbcp/mp_math/utils.py b/mbcp/mp_math/utils.py index 368a302..ef5c072 100644 --- a/mbcp/mp_math/utils.py +++ b/mbcp/mp_math/utils.py @@ -56,3 +56,38 @@ def approx(x: float, y: float = 0.0, epsilon: float = 0.0001) -> bool: 是否近似相等 """ return abs(x - y) < epsilon + + +def sign(x: float, only_neg: bool = False) -> str: + """获取数的符号。 + Args: + x: 数 + only_neg: 是否只返回负数的符号 + Returns: + 符号 + - "" + """ + if x > 0: + return "+" if not only_neg else "" + elif x < 0: + return "-" + else: + return "" + + +def sign_format(x: float, only_neg: bool = False) -> str: + """格式化符号数 + -1 -> -1 + 1 -> +1 + 0 -> "" + Args: + x: 数 + only_neg: 是否只返回负数的符号 + Returns: + 符号 + - "" + """ + if x > 0: + return f"+{x}" if not only_neg else f"{x}" + elif x < 0: + return f"-{abs(x)}" + else: + return "" diff --git a/mbcp/mp_math/vector.py b/mbcp/mp_math/vector.py index ed2c3c3..dcfec2c 100644 --- a/mbcp/mp_math/vector.py +++ b/mbcp/mp_math/vector.py @@ -1,6 +1,7 @@ import math from typing import overload, TYPE_CHECKING +from .mp_math_typing import RealNumber from .point import Point3 if TYPE_CHECKING: @@ -10,10 +11,11 @@ if TYPE_CHECKING: class Vector3: def __init__(self, x: float, y: float, z: float): """ - 笛卡尔坐标系中的向量。 - :param x: - :param y: - :param z: + 3维向量 + Args: + x: x轴分量 + y: y轴分量 + z: z轴分量 """ self.x = x self.y = y @@ -27,7 +29,7 @@ class Vector3: Returns: 夹角 """ - return AnyAngle(math.acos(self * other / (self.length * other.length)), is_radian=True) + return AnyAngle(math.acos(self @ other / (self.length * other.length)), is_radian=True) def is_parallel(self, other: 'Vector3') -> bool: """ @@ -37,20 +39,39 @@ class Vector3: Returns: 是否平行 """ - return self @ other == Vector3(0, 0, 0) + return self.cross(other) == Vector3(0, 0, 0) def cross(self, other: 'Vector3') -> 'Vector3': """ - 向量积 叉乘:V1 @ V2 -> V3 + 向量积 叉乘:v1 cross v2 -> v3 + 返回如下行列式的结果: + + ``i j k`` + + ``x1 y1 z1`` + + ``x2 y2 z2`` + Args: other: Returns: - 叉乘结果,为0向量则两向量平行,否则垂直于两向量 + 行列式的结果 """ return Vector3(self.y * other.z - self.z * other.y, self.z * other.x - self.x * other.z, self.x * other.y - self.y * other.x) + def normalize(self): + """ + 将向量归一化。 + + 自体归一化,不返回值。 + """ + length = self.length + self.x /= length + self.y /= length + self.z /= length + @property def length(self) -> float: """ @@ -117,7 +138,7 @@ class Vector3: ... @overload - def __sub__(self, other: 'Point3') -> 'Point3': + def __sub__(self, other: 'Point3') -> "Point3": ... def __sub__(self, other): @@ -133,9 +154,9 @@ class Vector3: elif isinstance(other, Point3): return Point3(self.x - other.x, self.y - other.y, self.z - other.z) else: - raise TypeError(f"unsupported operand type(s) for -: 'Vector3' and '{type(other)}'") + raise TypeError(f"unsupported operand type(s) for -: \"Vector3\" and \"{type(other)}\"") - def __rsub__(self, other: Point3): + def __rsub__(self, other: 'Point3'): """ P - V -> P Args: @@ -150,54 +171,41 @@ class Vector3: raise TypeError(f"unsupported operand type(s) for -: '{type(other)}' and 'Vector3'") @overload - def __mul__(self, other: float) -> 'Vector3': + def __mul__(self, other: RealNumber) -> 'Vector3': ... @overload - def __mul__(self, other: 'Vector3') -> float: + def __mul__(self, other: 'Vector3') -> 'Vector3': ... - def __mul__(self, other): + def __mul__(self, other: 'RealNumber | Vector3') -> 'Vector3': """ - 点乘法。包括点乘和数乘。 - V * V -> float\n + 数组运算 非点乘。点乘使用@,叉乘使用cross。 Args: other: + Returns: - float - Raises: - TypeError: 不支持的类型 """ - if isinstance(other, (int, float)): + if isinstance(other, RealNumber): return Vector3(self.x * other, self.y * other, self.z * other) elif isinstance(other, Vector3): - return self.x * other.x + self.y * other.y + self.z * other.z + return Vector3(self.x * other.x, self.y * other.y, self.z * other.z) else: raise TypeError(f"unsupported operand type(s) for *: 'Vector3' and '{type(other)}'") - def __rmul__(self, other: float) -> 'Vector3': - """ - 右乘。 - Args: - other: - Returns: - 乘积 - """ + def __rmul__(self, other: RealNumber) -> 'Vector3': return Vector3(self.x * other, self.y * other, self.z * other) - def __matmul__(self, other: 'Vector3') -> 'Vector3': + def __matmul__(self, other: 'Vector3') -> float: """ - 向量积 叉乘:V1 @ V2 -> V3 + 点乘。 Args: other: Returns: - 叉乘结果,为0向量则两向量平行,否则垂直于两向量 """ - return Vector3(self.y * other.z - self.z * other.y, - self.z * other.x - self.x * other.z, - self.x * other.y - self.y * other.x) + return self.x * other.x + self.y * other.y + self.z * other.z - def __truediv__(self, other: float) -> 'Vector3': + def __truediv__(self, other: RealNumber) -> 'Vector3': return Vector3(self.x / other, self.y / other, self.z / other) def __neg__(self): @@ -208,3 +216,16 @@ class Vector3: def __str__(self): return f"Vector3({self.x}, {self.y}, {self.z})" + + +zero_vector3 = Vector3(0, 0, 0) +"""零向量""" +x_axis = Vector3(1, 0, 0) +"""x轴单位向量""" +y_axis = Vector3(0, 1, 0) +"""y轴单位向量""" +z_axis = Vector3(0, 0, 1) +"""z轴单位向量""" + +v1: Vector3 = Vector3(1, 2, 3) * 3.0 +v2: Vector3 = 3.0 * Vector3(1, 2, 3) diff --git a/py.typed b/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ef3a892 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "mbcp" +version = "0.1.0" +description = "A tool for Minecraft particle production" +authors = [ + {name = "snowykami", email = "snowykami@outlook.com"}, +] +dependencies = [ + "pytest~=8.3.2", + "numpy~=2.0.1", + "liteyukibot>=6.3.9", +] +requires-python = ">=3.10" +readme = "README.md" +license = {text = "MIT"} + +[build-system] +requires = ["pdm-backend"] +build-backend = "pdm.backend" + + +[tool.pdm] +distribution = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index a2325b4..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest~=8.3.2 -numpy~=2.0.1 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/answer.py b/tests/answer.py new file mode 100644 index 0000000..4d67b07 --- /dev/null +++ b/tests/answer.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +""" +Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved + +@Time : 2024/8/27 下午1:03 +@Author : snowykami +@Email : snowykami@outlook.com +@File : .answer.py +@Software: PyCharm +""" +from liteyuki.log import logger # type: ignore + + +def output_answer(correct_ans, actual_ans, question: str = None): + """ + 输出答案 + Args: + correct_ans: + actual_ans: + question: + + Returns: + + """ + print("") + if question is not None: + logger.info(f"问题:{question}") + r = correct_ans == actual_ans + if r: + logger.success(f"测试正确 正确答案:{correct_ans} 实际答案:{actual_ans}") + else: + logger.error(f"测试错误 正确答案:{correct_ans} 实际答案:{actual_ans}") diff --git a/tests/test_line3.py b/tests/test_line3.py index 9ea3c3c..7388e5f 100644 --- a/tests/test_line3.py +++ b/tests/test_line3.py @@ -13,21 +13,8 @@ import logging from mbcp.mp_math.point import Point3 from mbcp.mp_math.vector import Vector3 from mbcp.mp_math.line import Line3 +from tests.answer import output_answer -class TestLine3: - - def test_point_and_normal_factory(self): - """ - 测试通过点和法向量构造直线 - """ - correct_ans = Line3(1, -2, 3, -8) - - p = Point3(2, -3, 0) - n = Vector3(1, -2, 3) - - actual_ans = Line3.from_point_and_direction(p, n) - logging.info(f"正确答案:{correct_ans} 实际答案:{actual_ans}") - assert actual_ans == correct_ans diff --git a/tests/test_plane3.py b/tests/test_plane3.py index 2729177..4212701 100644 --- a/tests/test_plane3.py +++ b/tests/test_plane3.py @@ -12,6 +12,8 @@ import logging from mbcp.mp_math.line import Line3 from mbcp.mp_math.plane import Plane3 +from mbcp.mp_math.point import Point3 +from mbcp.mp_math.vector import Vector3 class TestPlane3: @@ -20,10 +22,10 @@ class TestPlane3: """ 测试平面的交线 """ - correct_ans = Line3(4, 3, 1, 1) + correct_ans = Line3(Point3(-3, 2, 5), Vector3(4, 3, 1)) pl1 = Plane3(1, 0, -4, 23) pl2 = Plane3(2, -1, -5, 33) - actual_ans = pl1.cal_intersection_line3(pl2) + actual_ans = pl1 & pl2 # 平面交线 logging.info(f"正确答案:{correct_ans} 实际答案:{actual_ans}") assert actual_ans == correct_ans diff --git a/tests/test_vector3.py b/tests/test_vector3.py index e7f737e..66acead 100644 --- a/tests/test_vector3.py +++ b/tests/test_vector3.py @@ -12,6 +12,8 @@ import logging from mbcp.mp_math.vector import Vector3 +from tests.answer import output_answer + class TestVector3: @@ -23,7 +25,7 @@ class TestVector3: """ v1 = Vector3(1, 2, 3) v2 = Vector3(3, 4, 5) - actual_ans = v1 @ v2 + actual_ans = v1.cross(v2) correct_ans = Vector3(-2, 4, -2) logging.info(f"正确答案{correct_ans} 实际答案{v1 @ v2}") @@ -34,18 +36,20 @@ class TestVector3: 测试判断向量是否平行 Returns: """ + """小题1""" + correct_ans = True v1 = Vector3(1, 2, 3) v2 = Vector3(3, 6, 9) actual_ans = v1.is_parallel(v2) - correct_ans = True - logging.info("v1和v2是否平行:%s", v1.is_parallel(v2)) + output_answer(correct_ans, actual_ans) assert correct_ans == actual_ans + """小题2""" + correct_ans = False v1 = Vector3(1, 2, 3) v2 = Vector3(3, 6, 8) actual_ans = v1.is_parallel(v2) - correct_ans = False - logging.info("v1和v2是否平行:%s", v1.is_parallel(v2)) + output_answer(correct_ans, actual_ans) assert correct_ans == actual_ans diff --git a/tests/test_word_problem.py b/tests/test_word_problem.py index 72f09b5..56d5ad2 100644 --- a/tests/test_word_problem.py +++ b/tests/test_word_problem.py @@ -2,11 +2,12 @@ """ 应用题测试集 """ -import logging - +from liteyuki.log import logger # type: ignore from mbcp.mp_math.line import Line3 from mbcp.mp_math.plane import Plane3 from mbcp.mp_math.point import Point3 +from mbcp.mp_math.vector import Vector3 +from .answer import output_answer class TestWordProblem: @@ -16,17 +17,17 @@ class TestWordProblem: 同济大学《高等数学》第八版 下册 第八章第四节例4 问题:求与两平面x-4z-3=0和2x-y-5z-1=0的交线平行且过点(-3, 2, 5)的直线方程。 """ - correct_ans = Line3(4, 3, 1, 1) - + question = "求与两平面x-4z-3=0和2x-y-5z-1=0的交线平行且过点(-3, 2, 5)的直线方程。" + correct_ans = Line3(Point3(-3, 2, 5), Vector3(4, 3, 1)) pl1 = Plane3(1, 0, -4, -3) pl2 = Plane3(2, -1, -5, -1) p = Point3(-3, 2, 5) """解法1""" # 求直线方向向量s - s = pl1.normal @ pl2.normal - actual_ans = Line3.from_point_and_direction(p, s) + s = pl1.normal.cross(pl2.normal) + actual_ans = Line3(p, s) - logging.info(f"正确答案:{correct_ans} 实际答案:{actual_ans}") + output_answer(correct_ans, actual_ans, question) assert actual_ans == correct_ans """解法2""" @@ -36,8 +37,40 @@ class TestWordProblem: pl4 = pl2.cal_parallel_plane3(p) # 求pl3和pl4的交线 actual_ans = pl3.cal_intersection_line3(pl4) - print(pl3, pl4, actual_ans) - logging.info(f"正确答案:{correct_ans} 实际答案:{actual_ans}") + output_answer(correct_ans, actual_ans, question) assert actual_ans == correct_ans + def test_c8s4e5(self): + """ + 同济大学《高等数学》第八版 下册 第八章第四节例5 + + 求直线(x-2)/1=(y-3)/1=(z-4)/2与平面2x+y+z-6=0的交点。 + """ + question = "求直线(x-2)/1=(y-3)/1=(z-4)/2与平面2x+y+z-6=0的交点。" + """正确答案""" + correct_ans = Point3(1, 2, 2) + """题目已知量""" + line = Line3(Point3(2, 3, 4), Vector3(1, 1, 2)) + plane = Plane3(2, 1, 1, -6) + + """解""" + actual_ans = plane & line + output_answer(correct_ans, actual_ans, question) + + def test_c8s4e6(self): + question = "求过点(2, 3, 1)且与直线(x+1)/3 = (y-1)/2 = z/-1垂直相交的直线的方程。" + """正确答案""" + correct_ans = Line3(Point3(2, 1, 3), Vector3(2, -1, 4)) + """题目已知量""" + point = Point3(2, 3, 1) + line = Line3(Point3(-1, 1, 0), Vector3(3, 2, -1)) + + """解""" + # 先作平面过点且垂直与已知直线 + pl = line.cal_perpendicular(point) + logger.debug(line.get_point(1)) + + # output_answer(correct_ans, actual_ans, question) + +