# -*- coding:utf-8 -*- # title :天体基类 # description :天体基类(所有星体都继承了该类) # author :Python超人 # date :2023-02-11 # link :https://gitcode.net/pythoncr/ # python_version :3.8 # ============================================================================== from abc import ABCMeta, abstractmethod import json import numpy as np import math from common.consts import AU import copy from simulators.ursina.entities.entity_utils import get_value_direction_vectors class Body(metaclass=ABCMeta): """ 天体基类 """ def __init__(self, name, mass, init_position, init_velocity, rotate_angle=0, density=5e3, color=(125 / 255, 125 / 255, 125 / 255), text_color=None, texture=None, size_scale=1.0, distance_scale=1.0, rotation_speed=None, parent=None, ignore_mass=False, is_fixed_star=False, show_trail=True, trail_color=None, show_name=False): """ 天体类 @param name: 天体名称 @param mass: 天体质量 (kg) @param init_position: 初始位置 (km) @param init_velocity: 初始速度 (km/s) @param rotate_angle: 转轴倾角 @param density: 平均密度 (kg/m³) @param color: 天体颜色(纹理图片优先) @param texture: 纹理图片 @param size_scale: 尺寸缩放 @param distance_scale: 距离缩放 @param rotation_speed: 自旋速度(度/小时) @param parent: 天体的父对象 @param ignore_mass: 是否忽略质量(如果为True,则不计算引力) TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在天体物理学中是不严谨) @param is_fixed_star: 是否为恒星 @param show_trail: 天体拖尾是否显示 @param trail_color: 天体拖尾颜色(默认天体颜色) @param show_name: 是否显示天体名称 """ self.__his_pos = [] self.__his_vel = [] self.__his_acc = [] self.__his_reserved_num = 200 if name is None: name = getattr(self.__class__, '__name__') self.name = name self.__mass = mass self.text_color = text_color # 是否忽略质量(如果为True,则不计算引力) # TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在天体物理学中是不严谨) if self.__mass <= 0: # 质量小于等于0就忽略 self.ignore_mass = True else: self.ignore_mass = ignore_mass self.__init_position = None self.__init_velocity = None self.init_position = np.array(init_position, dtype='float32') self.init_velocity = np.array(init_velocity, dtype='float32') self.rotate_angle = rotate_angle # self.__position = copy.deepcopy(self.init_position) # self.__velocity = copy.deepcopy(self.init_velocity) self.__density = density self.__rotation_speed = rotation_speed self.color = color self.show_trail = show_trail self.trail_color = color if trail_color is None else trail_color self.texture = texture self.size_scale = size_scale self.distance_scale = distance_scale # 初始化后,加速度为0,只有多个天体的引力才会影响到加速度 # km/s² self.__acceleration = np.array([0, 0, 0], dtype='float32') self.__record_history() # 是否显示 self.appeared = True self.parent = parent self.__is_fixed_star = is_fixed_star self.show_name = show_name self.resolution = None self.light_disable = False self.__has_rings = False def set_ignore_gravity(self, value=True): """ 设置忽略质量,True为引力失效 @param value: @return: """ # TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在天体物理学中是不严谨) if self.__mass <= 0: # 质量小于等于0就忽略 self.ignore_mass = True else: self.ignore_mass = value return self def set_light_disable(self, value=True): """ 设置灯光为无效 @param value: @return: """ self.light_disable = value return self def set_resolution(self, value): """ 设置天体的分辨率 @param value: @return: """ self.resolution = value return self @property def init_position(self): """ 获取天体的初始位置(单位:km) @return: """ return self.__init_position @init_position.setter def init_position(self, value): """ 设置天体的初始位置(单位:km) @param value: @return: """ self.__init_position = np.array(value, dtype='float32') self.__position = copy.deepcopy(self.__init_position) @property def init_velocity(self): """ 获取天体的初始速度 (km/s) @return: """ return self.__init_velocity @init_velocity.setter def init_velocity(self, value): """ 设置天体的初始速度 (km/s) @param value: @return: """ self.__init_velocity = np.array(value, dtype='float32') self.__velocity = copy.deepcopy(self.__init_velocity) @property def has_rings(self): """ 是否为带光环的天体(土星为 True) @return: """ return self.__has_rings @has_rings.setter def has_rings(self, value): self.__has_rings = value @property def is_fixed_star(self): """ 是否为恒星(太阳为 True) @return: """ return self.__is_fixed_star @is_fixed_star.setter def is_fixed_star(self, value): self.__is_fixed_star = value @property def position(self): """ 获取天体的位置(单位:km) @return: """ return self.__position @position.setter def position(self, value): """ 设置天体的位置(单位:km) @param value: @return: """ self.__position = value self.__record_history() def acceleration_value(self): """ 获取加速度的值(km/s²) @return: """ direction_vectors = get_value_direction_vectors(self.acceleration) return direction_vectors[0] # km/s² def velocity_value(self): """ 获取速度的值(km/s) @return: """ direction_vectors = get_value_direction_vectors(self.velocity) return direction_vectors[0] # km/s def acceleration_value_direction(self): """ 获取加速度的值(km/s²)和方向 @return: 值[0],方向[1] """ return get_value_direction_vectors(self.acceleration) def velocity_value_direction(self): """ 获取速度的值(km/s)和方向 @return: 值[0],方向[1] """ return get_value_direction_vectors(self.velocity) @property def acceleration(self): """ 获取天体的加速度(单位:km/s²) @return: """ return self.__acceleration @acceleration.setter def acceleration(self, value): """ 设置天体的加速度(单位:km/s²) @param value: @return: """ self.__acceleration = np.array(value, dtype=float) self.__record_history() def stop(self): """ 停止运动,将加速度和速度置零 @return: """ self.velocity = [0.0, 0.0, 0.0] self.acceleration = [0.0, 0.0, 0.0] def stop_and_ignore_gravity(self): """ 停止运动,并忽略质量(不受引力影响) TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在天体物理学中是不严谨) @return: """ self.set_ignore_gravity() self.stop() @property def velocity(self): """ 获取天体的速度(单位:km/s) @return: """ return self.__velocity @velocity.setter def velocity(self, value): """ 设置天体的速度(单位:km/s) @param value: @return: """ self.__velocity = value self.__record_history() def __append_history(self, his_list, data): """ 追加每个位置时刻的历史数据 @param his_list: @param data: @return: """ # 如果历史记录为0 或者 新增数据和最后的历史数据不相同,则添加 if len(his_list) == 0 or \ np.sum(data == his_list[-1]) < len(data): his_list.append(data.copy()) def __record_history(self): """ 记录每个位置时刻的历史数据 @return: """ # 如果历史记录数超过了保留数量,则截断,只保留 __his_reserved_num 数量的历史 if len(self.__his_pos) > self.__his_reserved_num: self.__his_pos = self.__his_pos[len(self.__his_pos) - self.__his_reserved_num:] self.__his_vel = self.__his_vel[len(self.__his_vel) - self.__his_reserved_num:] self.__his_acc = self.__his_acc[len(self.__his_acc) - self.__his_reserved_num:] # 追加历史记录(位置、速度、加速度) self.__append_history(self.__his_pos, self.position) self.__append_history(self.__his_vel, self.velocity) self.__append_history(self.__his_acc, self.acceleration) # print(self.name, "his pos->", self.__his_pos) def clear_his_position(self): self.__his_pos.clear() def his_position(self): """ 历史位置 @return: """ return self.__his_pos def his_velocity(self): """ 历史瞬时速度 @return: """ return self.__his_vel def his_acceleration(self): """ 历史瞬时加速度 @return: """ return self.__his_acc @property def mass(self): """ 天体质量 (单位:kg) @return: """ return self.__mass @property def rotation_speed(self): """ 自旋速度(度/小时) @return: """ return self.__rotation_speed @rotation_speed.setter def rotation_speed(self, value): """ 自旋速度(度/小时) @return: """ self.__rotation_speed = value @property def density(self): """ 平均密度 (单位:kg/m³) @return: """ return self.__density @property def volume(self): """ 天体的体积(单位:km³) """ if self.mass <= 0: # 质量为0或者负数,就给一个虚拟体积数 return 1e10 # v = m/ρ # 体积(m³) = 质量(kg) / 密度(kg/m³) # 体积(km³) = 体积(m³) / 1e9 v = self.mass / self.density / 1e9 return v @property def radius(self): """ 天体的半径(单位:km) @return: """ # V = ⁴⁄₃πr³ -> r = pow((3V)/(4π),1/3) return pow(3 * self.volume / (4 * math.pi), 1 / 3) @property def diameter(self): """ 天体的直径(单位:km) @return: """ return self.radius * 2 def __repr__(self): return '<%s(%s)> m=%.3e(kg), r|d=%.3e|%.3e(km), v=%.3e(km³), d=%.3e(kg/m³), p=[%.3e,%.3e,%.3e](km), v=%s(km/s)' % \ (self.name, self.__class__.__name__, self.mass, self.radius, self.diameter, self.volume, self.density, self.position[0], self.position[1], self.position[2], self.velocity) def ignore_gravity_with(self, body): """ 是否忽略指定天体的引力 @param body: @return: """ # TODO: 注意:这里的算法是基于牛顿的万有引力(质量为0不受引力的影响在天体物理学中是不严谨) if self.ignore_mass: return True return False def position_au(self): """ 获取天体的位置(单位:天文单位 A.U.) @return: """ pos = self.position pos_au = pos / AU return pos_au # def change_velocity(self, dv): # self.velocity += dv # # def move(self, dt): # self.position += self.velocity * dt def reset(self): """ 重新设置初始速度和初始位置 @return: """ self.position = copy.deepcopy(self.init_position) self.velocity = copy.deepcopy(self.init_velocity) # def kinetic_energy(self): # """ # 计算动能(千焦耳) # 表示动能,单位为焦耳j,m为质量,单位为千克,v为速度,单位为米/秒。 # ek=(1/2).m.v^2 # m(kg) v(m/s) -> j # m(kg) v(km/s) -> kj # """ # v = self.velocity # return 0.5 * self.mass * (v[0] ** 2 + v[1] ** 2 + v[2] ** 2) @staticmethod def build_bodies_from_json(json_file): """ JSON文件转为天体对象 @param json_file: @return: """ bodies = [] params = {} from bodies import FixedStar with open(json_file, "r", encoding='utf-8') as read_content: json_data = json.load(read_content) for body_data in json_data["bodies"]: try: body_data = Body.exp(body_data) # print(body_data) except Exception as e: err_msg = f"{json_file} 格式错误:" + str(e) raise Exception(err_msg) is_fixed_star = False if "is_fixed_star" in body_data: if body_data["is_fixed_star"]: is_fixed_star = True if is_fixed_star: body_data.pop("is_fixed_star") body = FixedStar(**body_data) else: has_rings = False if "has_rings" in body_data: if body_data["has_rings"]: has_rings = True body_data.pop("has_rings") body = Body(**body_data) if has_rings: body.has_rings = True # [x, y, z]->[-y, z, x] # body.init_velocity = [-body.init_velocity[1],body.init_velocity[2],body.init_velocity[0]] # body.init_position = [-body.init_position[1],body.init_position[2],body.init_position[0]] bodies.append(body) if "params" in json_data: params = json_data["params"] # print(body.position_au()) return bodies, params @staticmethod def exp(body_data): """ 进行表达式分析,将表达式改为eval执行后的结果 @param body_data: @return: """ # for k in body_data.keys(): v = body_data[k] if isinstance(v, str): if v.startswith("$exp:"): exp = v[5:] body_data[k] = eval(exp) elif isinstance(v, list): for idx, item in enumerate(v): if isinstance(item, str): if item.startswith("$exp:"): exp = item[5:] v[idx] = eval(exp) return body_data def show_demo(self, size_scale=1000, position=(0, 0, -5000000), on_ready_fun=None, on_timer_changed_fun=None, directional_light_pos=None): from sim_scenes.func import ursina_run from sim_scenes.func import camera_look_at from simulators.ursina.ursina_event import UrsinaEvent self.size_scale = size_scale # self.init_position = np.array([0, -3000000, 9000000]) self.init_position = np.array([0, 0, 0]) self.init_velocity = np.array([0, 0, 0]) def on_ready(): camera_look_at(self) if directional_light_pos is not None: from simulators.ursina.entities.entity_utils import create_directional_light create_directional_light(position=directional_light_pos, light_num=3, target=self) if callable(on_ready_fun): on_ready_fun() def on_timer_changed(time_data): on_timer_changed_fun(time_data) if callable(on_ready_fun) or directional_light_pos is not None: UrsinaEvent.on_ready_subscription(on_ready) if callable(on_timer_changed_fun): UrsinaEvent.on_timer_changed_subscription(on_timer_changed) timer_enabled = True else: timer_enabled = False ursina_run(bodies=[self], position=position, timer_enabled=timer_enabled) if __name__ == '__main__': # build_bodies_from_json('../data/sun.json') bodies, params = Body.build_bodies_from_json('../data/sun_earth.json') # 太阳半径 / 地球半径 print("太阳半径 / 地球半径 =", bodies[0].radius / bodies[1].radius) print("params:", params) for body in bodies: print(body)