refactor: 重组项目目录结构

以讲义内容为骨架迁移到标准目录格式:
- officefile/ 主内容(12章 + 附录 + CC4SI补充)
- dofile/ 代码示例(11个Python脚本)
- data/ 图片资源
- output/ 生成输出(忽略)
- Archive/ 归档旧目录(忽略)
- .claude/skills/ 保留markdown-to-docx工具链
- .pandoc/ 保留CSL和本地化配置

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 14:00:56 +08:00
parent af083069d0
commit 219232de74
91 changed files with 39365 additions and 10 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,961 @@
"""
空间优化示例 (Spatial Optimization Example)
==========================================
本示例展示空间智能系统中的空间优化方法。
空间优化是在空间约束下寻找最优解的过程。
核心概念:
1. 目标函数 - 需要最大化或最小化的目标
2. 约束条件 - 空间和非空间限制
3. 决策变量 - 可控制的空间变量
4. 优化算法 - 求解最优解的方法
5. 帕累托前沿 - 多目标优化中的解集
应用场景:
- 选址优化
- 路径优化
- 空间配置优化
- 资源分配优化
作者: CC4SI 项目组
"""
import math
import random
from typing import List, Dict, Tuple, Optional, Any, Callable
from dataclasses import dataclass, field
from enum import Enum
from abc import ABC, abstractmethod
import heapq
# ============================================================================
# 基础数据结构
# ============================================================================
@dataclass
class Point:
"""二维点"""
x: float
y: float
def distance_to(self, other: 'Point') -> float:
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
def __repr__(self) -> str:
return f"({self.x:.2f}, {self.y:.2f})"
@dataclass
class DemandPoint:
"""需求点"""
id: str
location: Point
demand: float # 需求量
population: int = 0
def __repr__(self) -> str:
return f"Demand({self.id}, demand={self.demand})"
@dataclass
class Facility:
"""设施"""
id: str
location: Point
capacity: float # 服务能力
fixed_cost: float = 0 # 固定成本
variable_cost: float = 0 # 单位可变成本
def __repr__(self) -> str:
return f"Facility({self.id}, loc={self.location})"
@dataclass
class OptimizationResult:
"""优化结果"""
success: bool
objective_value: float
solution: Any
iterations: int = 0
convergence_history: List[float] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
# ============================================================================
# 约束条件
# ============================================================================
class ConstraintType(Enum):
"""约束类型"""
EQUALITY = "equality" # 等式约束
INEQUALITY = "inequality" # 不等式约束
BOUNDS = "bounds" # 边界约束
@dataclass
class Constraint:
"""约束条件"""
name: str
constraint_type: ConstraintType
rhs: float # 右端值
lhs_function: Optional[Callable[[Any], float]] = None # 左端函数
def is_satisfied(self, variables: Any, tolerance: float = 1e-6) -> bool:
"""检查约束是否满足"""
if self.lhs_function is None:
return True
lhs = self.lhs_function(variables)
if self.constraint_type == ConstraintType.EQUALITY:
return abs(lhs - self.rhs) < tolerance
elif self.constraint_type == ConstraintType.INEQUALITY:
return lhs <= self.rhs + tolerance
return True
# ============================================================================
# 优化问题定义
# ============================================================================
class OptimizationProblem(ABC):
"""优化问题抽象基类"""
def __init__(self, name: str = ""):
self.name = name
self.constraints: List[Constraint] = []
self.objective_calls = 0
@abstractmethod
def objective(self, variables: Any) -> float:
"""目标函数"""
pass
@abstractmethod
def get_initial_solution(self) -> Any:
"""获取初始解"""
pass
def add_constraint(self, constraint: Constraint) -> None:
"""添加约束"""
self.constraints.append(constraint)
def is_feasible(self, variables: Any) -> bool:
"""检查解是否可行"""
return all(c.is_satisfied(variables) for c in self.constraints)
# ============================================================================
# 贪心算法
# ============================================================================
class GreedyOptimizer:
"""
贪心优化器
每一步选择当前最优的选项。
"""
def __init__(self, problem: OptimizationProblem):
self.problem = problem
def optimize(self, max_iterations: int = 1000) -> OptimizationResult:
"""
执行贪心优化
Args:
max_iterations: 最大迭代次数
Returns:
优化结果
"""
current_solution = self.problem.get_initial_solution()
current_value = self.problem.objective(current_solution)
history = [current_value]
for iteration in range(max_iterations):
# 生成邻居解
neighbors = self._generate_neighbors(current_solution)
# 找最优邻居
best_neighbor = None
best_neighbor_value = float('inf')
for neighbor in neighbors:
if self.problem.is_feasible(neighbor):
value = self.problem.objective(neighbor)
if value < best_neighbor_value:
best_neighbor = neighbor
best_neighbor_value = value
# 如果没有改进,停止
if best_neighbor is None or best_neighbor_value >= current_value:
break
current_solution = best_neighbor
current_value = best_neighbor_value
history.append(current_value)
return OptimizationResult(
success=True,
objective_value=current_value,
solution=current_solution,
iterations=len(history),
convergence_history=history
)
def _generate_neighbors(self, solution: Any) -> List[Any]:
"""生成邻居解 (需要根据具体问题实现)"""
return []
# ============================================================================
# 模拟退火算法
# ============================================================================
class SimulatedAnnealing:
"""
模拟退火算法
一种概率性全局优化算法,能够跳出局部最优。
"""
def __init__(self, problem: OptimizationProblem):
self.problem = problem
def optimize(self,
initial_temp: float = 1000.0,
cooling_rate: float = 0.95,
min_temp: float = 0.01,
max_iterations: int = 10000) -> OptimizationResult:
"""
执行模拟退火优化
Args:
initial_temp: 初始温度
cooling_rate: 冷却速率
min_temp: 最小温度
max_iterations: 最大迭代次数
Returns:
优化结果
"""
current_solution = self.problem.get_initial_solution()
current_value = self.problem.objective(current_solution)
best_solution = current_solution
best_value = current_value
temperature = initial_temp
history = [current_value]
iteration = 0
while temperature > min_temp and iteration < max_iterations:
# 生成邻居解
neighbor = self._generate_neighbor(current_solution)
if self.problem.is_feasible(neighbor):
neighbor_value = self.problem.objective(neighbor)
# 决定是否接受新解
delta = neighbor_value - current_value
if delta < 0 or random.random() < math.exp(-delta / temperature):
current_solution = neighbor
current_value = neighbor_value
# 更新最优解
if current_value < best_value:
best_solution = current_solution
best_value = current_value
history.append(best_value)
temperature *= cooling_rate
iteration += 1
return OptimizationResult(
success=True,
objective_value=best_value,
solution=best_solution,
iterations=iteration,
convergence_history=history
)
def _generate_neighbor(self, solution: Any) -> Any:
"""生成邻居解 (需要根据具体问题实现)"""
return solution
# ============================================================================
# 遗传算法
# =============================================================================
class GeneticAlgorithm:
"""
遗传算法
模拟自然进化的全局优化算法。
"""
def __init__(self, problem: OptimizationProblem):
self.problem = problem
def optimize(self,
population_size: int = 50,
generations: int = 100,
mutation_rate: float = 0.1,
crossover_rate: float = 0.8,
elitism_count: int = 2) -> OptimizationResult:
"""
执行遗传算法优化
Args:
population_size: 种群大小
generations: 迭代代数
mutation_rate: 变异率
crossover_rate: 交叉率
elitism_count: 精英保留数量
Returns:
优化结果
"""
# 初始化种群
population = self._initialize_population(population_size)
history = []
for generation in range(generations):
# 评估适应度
fitness = []
for individual in population:
value = self.problem.objective(individual)
fitness.append(1.0 / (1.0 + value)) # 转换为适应度 (越小越好 -> 越大越好)
# 记录最优值
best_idx = max(range(len(fitness)), key=lambda i: fitness[i])
best_value = self.problem.objective(population[best_idx])
history.append(best_value)
# 选择
selected = self._selection(population, fitness)
# 交叉
offspring = self._crossover(selected, crossover_rate)
# 变异
offspring = self._mutation(offspring, mutation_rate)
# 精英保留
if elitism_count > 0:
elite_indices = sorted(range(len(fitness)),
key=lambda i: fitness[i], reverse=True)[:elitism_count]
for i, idx in enumerate(elite_indices):
offspring[i] = population[idx]
population = offspring
# 返回最优解
final_fitness = [self.problem.objective(ind) for ind in population]
best_idx = min(range(len(final_fitness)), key=lambda i: final_fitness[i])
return OptimizationResult(
success=True,
objective_value=final_fitness[best_idx],
solution=population[best_idx],
iterations=generations,
convergence_history=history
)
def _initialize_population(self, size: int) -> List[Any]:
"""初始化种群"""
return [self.problem.get_initial_solution() for _ in range(size)]
def _selection(self, population: List[Any], fitness: List[float]) -> List[Any]:
"""锦标赛选择"""
selected = []
tournament_size = max(3, len(population) // 10)
for _ in range(len(population)):
# 随机选择tournament_size个个体
contestants = random.sample(list(zip(population, fitness)), tournament_size)
# 选择适应度最高的
winner = max(contestants, key=lambda x: x[1])[0]
selected.append(winner)
return selected
def _crossover(self, population: List[Any], rate: float) -> List[Any]:
"""交叉操作"""
offspring = []
for i in range(0, len(population), 2):
parent1 = population[i]
parent2 = population[i + 1] if i + 1 < len(population) else population[0]
if random.random() < rate:
child1, child2 = self._crossover_operators(parent1, parent2)
else:
child1, child2 = parent1, parent2
offspring.extend([child1, child2])
return offspring[:len(population)]
def _crossover_operators(self, parent1: Any, parent2: Any) -> Tuple[Any, Any]:
"""交叉算子 (需要根据具体问题实现)"""
return parent1, parent2
def _mutation(self, population: List[Any], rate: float) -> List[Any]:
"""变异操作"""
mutated = []
for individual in population:
if random.random() < rate:
mutated.append(self._mutate(individual))
else:
mutated.append(individual)
return mutated
def _mutate(self, individual: Any) -> Any:
"""变异算子 (需要根据具体问题实现)"""
return individual
# ============================================================================
# 选址优化问题
# ============================================================================
class LocationProblem(OptimizationProblem):
"""
设施选址问题
在给定候选位置中选择最优的设施位置组合。
"""
def __init__(self,
demand_points: List[DemandPoint],
candidate_locations: List[Point],
num_facilities: int,
fixed_costs: List[float] = None,
transportation_cost: float = 1.0):
"""
初始化选址问题
Args:
demand_points: 需求点列表
candidate_locations: 候选位置列表
num_facilities: 设施数量
fixed_costs: 各候选位置的固定成本
transportation_cost: 单位运输成本
"""
super().__init__("设施选址问题")
self.demand_points = demand_points
self.candidate_locations = candidate_locations
self.num_facilities = num_facilities
self.fixed_costs = fixed_costs or [0] * len(candidate_locations)
self.transportation_cost = transportation_cost
def objective(self, solution: List[int]) -> float:
"""
计算目标函数值
Args:
solution: 选中的候选位置索引列表
Returns:
总成本
"""
self.objective_calls += 1
if not solution or len(solution) != self.num_facilities:
return float('inf')
total_cost = 0.0
# 固定成本
for idx in solution:
if 0 <= idx < len(self.fixed_costs):
total_cost += self.fixed_costs[idx]
# 运输成本 (每个需求点分配到最近的设施)
for demand in self.demand_points:
min_dist = float('inf')
for facility_idx in solution:
if 0 <= facility_idx < len(self.candidate_locations):
facility_loc = self.candidate_locations[facility_idx]
dist = demand.location.distance_to(facility_loc)
min_dist = min(min_dist, dist)
total_cost += min_dist * demand.demand * self.transportation_cost
return total_cost
def get_initial_solution(self) -> List[int]:
"""获取初始解 (随机选择)"""
n = len(self.candidate_locations)
if n <= self.num_facilities:
return list(range(n))
return random.sample(range(n), self.num_facilities)
class LocationSAOptimizer(SimulatedAnnealing):
"""针对选址问题的模拟退火优化器"""
def _generate_neighbor(self, solution: List[int]) -> List[int]:
"""生成邻居解"""
if not solution:
return solution
neighbor = solution.copy()
n = len(self.problem.candidate_locations)
# 随机选择一个操作
operation = random.choice(['replace', 'swap'])
if operation == 'replace':
# 替换一个设施
idx = random.randint(0, len(neighbor) - 1)
available = [i for i in range(n) if i not in neighbor]
if available:
neighbor[idx] = random.choice(available)
elif operation == 'swap' and len(neighbor) >= 1:
# 交换一个设施
idx = random.randint(0, len(neighbor) - 1)
available = [i for i in range(n) if i not in neighbor]
if available:
neighbor[idx] = random.choice(available)
return neighbor
# ============================================================================
# P-中值问题
# ============================================================================
class PMedianProblem(LocationProblem):
"""
P-中值问题
选择P个设施位置,使需求点到最近设施的总加权距离最小。
"""
def __init__(self,
demand_points: List[DemandPoint],
candidate_locations: List[Point],
p: int):
super().__init__(demand_points, candidate_locations, p, [0] * len(candidate_locations))
# ============================================================================
# 覆盖问题
# ============================================================================
class MaxCoverageProblem(OptimizationProblem):
"""
最大覆盖问题
在给定设施数量限制下,最大化覆盖的需求量。
"""
def __init__(self,
demand_points: List[DemandPoint],
candidate_locations: List[Point],
num_facilities: int,
coverage_radius: float):
"""
初始化最大覆盖问题
Args:
demand_points: 需求点列表
candidate_locations: 候选位置列表
num_facilities: 设施数量
coverage_radius: 覆盖半径
"""
super().__init__("最大覆盖问题")
self.demand_points = demand_points
self.candidate_locations = candidate_locations
self.num_facilities = num_facilities
self.coverage_radius = coverage_radius
# 预计算覆盖关系
self.coverage_matrix = self._compute_coverage()
def _compute_coverage(self) -> List[List[bool]]:
"""计算覆盖矩阵"""
matrix = []
for loc in self.candidate_locations:
coverage = []
for demand in self.demand_points:
covered = loc.distance_to(demand.location) <= self.coverage_radius
coverage.append(covered)
matrix.append(coverage)
return matrix
def objective(self, solution: List[int]) -> float:
"""
计算覆盖的需求量 (负值,因为算法最小化)
Args:
solution: 选中的候选位置索引列表
Returns:
负的覆盖需求量
"""
covered = [False] * len(self.demand_points)
for facility_idx in solution:
if 0 <= facility_idx < len(self.coverage_matrix):
for j, is_covered in enumerate(self.coverage_matrix[facility_idx]):
if is_covered:
covered[j] = True
total_demand = sum(
self.demand_points[j].demand
for j, c in enumerate(covered) if c
)
return -total_demand # 负值用于最小化
def get_initial_solution(self) -> List[int]:
"""获取初始解"""
n = len(self.candidate_locations)
if n <= self.num_facilities:
return list(range(n))
return random.sample(range(n), self.num_facilities)
class MaxCoverageGAOptimizer(GeneticAlgorithm):
"""针对最大覆盖问题的遗传算法优化器"""
def _crossover_operators(self, parent1: List[int], parent2: List[int]) -> Tuple[List[int], List[int]]:
"""单点交叉"""
if not parent1 or not parent2:
return parent1, parent2
size = min(len(parent1), len(parent2))
if size < 2:
return parent1, parent2
point = random.randint(1, size - 1)
child1 = parent1[:point] + [x for x in parent2[point:] if x not in parent1[:point]]
child2 = parent2[:point] + [x for x in parent1[point:] if x not in parent2[:point]]
# 补足长度
all_indices = set(range(len(self.problem.candidate_locations)))
while len(child1) < len(parent1):
available = list(all_indices - set(child1))
if available:
child1.append(random.choice(available))
else:
break
while len(child2) < len(parent2):
available = list(all_indices - set(child2))
if available:
child2.append(random.choice(available))
else:
break
return child1[:len(parent1)], child2[:len(parent2)]
def _mutate(self, individual: List[int]) -> List[int]:
"""变异操作"""
if not individual:
return individual
mutated = individual.copy()
n = len(self.problem.candidate_locations)
# 随机替换一个基因
idx = random.randint(0, len(mutated) - 1)
available = [i for i in range(n) if i not in mutated]
if available:
mutated[idx] = random.choice(available)
return mutated
# ============================================================================
# 路径优化 (TSP)
# ============================================================================
class TSPProblem(OptimizationProblem):
"""
旅行商问题 (TSP)
寻找访问所有城市的最短路径。
"""
def __init__(self, cities: List[Point]):
"""
初始化TSP问题
Args:
cities: 城市位置列表
"""
super().__init__("旅行商问题")
self.cities = cities
self.n = len(cities)
def objective(self, solution: List[int]) -> float:
"""
计算路径总长度
Args:
solution: 城市访问顺序列表
Returns:
路径总长度
"""
if not solution or len(solution) != self.n:
return float('inf')
total = 0.0
for i in range(len(solution)):
from_idx = solution[i]
to_idx = solution[(i + 1) % len(solution)]
total += self.cities[from_idx].distance_to(self.cities[to_idx])
return total
def get_initial_solution(self) -> List[int]:
"""获取初始解 (随机顺序)"""
solution = list(range(self.n))
random.shuffle(solution)
return solution
class TSPSAOptimizer(SimulatedAnnealing):
"""针对TSP的模拟退火优化器"""
def _generate_neighbor(self, solution: List[int]) -> List[int]:
"""生成邻居解 (2-opt交换)"""
if len(solution) < 2:
return solution
neighbor = solution.copy()
# 随机选择两个位置并交换
i, j = random.sample(range(len(neighbor)), 2)
neighbor[i], neighbor[j] = neighbor[j], neighbor[i]
return neighbor
# ============================================================================
# 主程序
# ========================================================================
def main():
"""主程序 - 演示空间优化的使用"""
print("="*70)
print("空间优化示例演示")
print("="*70)
random.seed(42)
# ========================================================================
# 1. P-中值问题 (选址优化)
# ========================================================================
print("\n[部分 1] P-中值问题 - 商场选址优化")
print("-" * 50)
# 生成需求点
demand_points = []
for i in range(20):
demand_points.append(DemandPoint(
id=f"D{i}",
location=Point(random.uniform(0, 100), random.uniform(0, 100)),
demand=random.uniform(50, 200)
))
print(f"\n需求点数量: {len(demand_points)}")
print(f"总需求量: {sum(d.demand for d in demand_points):.1f}")
# 候选位置
candidate_locations = [
Point(20, 20), Point(50, 20), Point(80, 20),
Point(20, 50), Point(50, 50), Point(80, 50),
Point(20, 80), Point(50, 80), Point(80, 80)
]
print(f"\n候选位置数量: {len(candidate_locations)}")
print("候选位置:", [str(loc) for loc in candidate_locations])
# 创建P-中值问题 (选择3个位置)
p_median = PMedianProblem(demand_points, candidate_locations, p=3)
# 使用模拟退火求解
sa_optimizer = LocationSAOptimizer(p_median)
sa_result = sa_optimizer.optimize(
initial_temp=100,
cooling_rate=0.95,
min_temp=0.1,
max_iterations=1000
)
print(f"\n模拟退火结果:")
print(f" 目标函数值: {sa_result.objective_value:.2f}")
print(f" 迭代次数: {sa_result.iterations}")
print(f" 选中的位置: {[str(candidate_locations[i]) for i in sa_result.solution]}")
# 贪心算法对比
greedy_optimizer = GreedyOptimizer(p_median)
# 简单的贪心: 逐步添加最优位置
best_solution = None
best_value = float('inf')
for _ in range(100):
solution = p_median.get_initial_solution()
value = p_median.objective(solution)
if value < best_value:
best_value = value
best_solution = solution
print(f"\n随机搜索对比:")
print(f" 目标函数值: {best_value:.2f}")
print(f" 选中的位置: {[str(candidate_locations[i]) for i in best_solution]}")
# ========================================================================
# 2. 最大覆盖问题
# ========================================================================
print("\n\n[部分 2] 最大覆盖问题 - 5G基站选址")
print("-" * 50)
# 创建更大规模的需求点
coverage_demands = []
for i in range(50):
coverage_demands.append(DemandPoint(
id=f"C{i}",
location=Point(random.uniform(0, 100), random.uniform(0, 100)),
demand=random.uniform(10, 100)
))
coverage_radius = 25
num_bases = 5
max_coverage = MaxCoverageProblem(
coverage_demands,
candidate_locations,
num_bases,
coverage_radius
)
# 使用遗传算法求解
ga_optimizer = MaxCoverageGAOptimizer(max_coverage)
ga_result = ga_optimizer.optimize(
population_size=50,
generations=100,
mutation_rate=0.1
)
covered_demand = -ga_result.objective_value
total_demand = sum(d.demand for d in coverage_demands)
coverage_ratio = covered_demand / total_demand * 100
print(f"\n遗传算法结果:")
print(f" 覆盖需求量: {covered_demand:.1f} / {total_demand:.1f}")
print(f" 覆盖率: {coverage_ratio:.1f}%")
print(f" 迭代次数: {ga_result.iterations}")
print(f" 选中的位置: {[str(candidate_locations[i]) for i in ga_result.solution]}")
# ========================================================================
# 3. 旅行商问题 (TSP)
# ========================================================================
print("\n\n[部分 3] 旅行商问题 - 配送路线优化")
print("-" * 50)
# 生成城市
cities = []
for i in range(15):
cities.append(Point(random.uniform(0, 100), random.uniform(0, 100)))
print(f"\n城市数量: {len(cities)}")
print(f"城市位置: {[str(city) for city in cities[:5]]}...")
tsp = TSPProblem(cities)
tsp_optimizer = TSPSAOptimizer(tsp)
tsp_result = tsp_optimizer.optimize(
initial_temp=1000,
cooling_rate=0.99,
min_temp=0.01,
max_iterations=5000
)
print(f"\n模拟退火结果:")
print(f" 最短路径长度: {tsp_result.objective_value:.2f}")
print(f" 访问顺序: {[cities[i].__repr__() for i in tsp_result.solution[:5]]}...")
# 对比随机解
random_solution = list(range(len(cities)))
random.shuffle(random_solution)
random_length = tsp.objective(random_solution)
print(f"\n随机解对比:")
print(f" 路径长度: {random_length:.2f}")
print(f" 改进: {(1 - tsp_result.objective_value / random_length) * 100:.1f}%")
# ========================================================================
# 4. 多目标优化讨论
# ========================================================================
print("\n\n[部分 4] 多目标优化说明")
print("-" * 50)
print("""
在实际应用中,空间优化往往涉及多个目标:
1. 成本最小化
- 设施建设成本
- 运营成本
- 运输成本
2. 服务最大化
- 覆盖范围
- 服务质量
- 响应时间
3. 公平性
- 服务均等化
- 负载均衡
4. 环境影响
- 最小化污染
- 保护生态
处理方法:
- 加权求和法 (将多目标转为单目标)
- 帕累托优化 (寻找非劣解集)
- 约束法 (将部分目标转为约束)
- 目标规划 (设定目标满意水平)
""")
# ========================================================================
# 5. 收敛过程可视化 (文本)
# ========================================================================
print("\n[部分 5] 优化过程")
print("-" * 50)
if len(ga_result.convergence_history) > 0:
print("\n遗传算法收敛过程:")
steps = min(10, len(ga_result.convergence_history))
step_size = len(ga_result.convergence_history) // steps
for i in range(0, len(ga_result.convergence_history), step_size):
iteration = i
value = -ga_result.convergence_history[i] # 转回正值
print(f" 迭代 {iteration:4d}: 覆盖需求量 = {value:.1f}")
print("\n" + "="*70)
print("演示完成!")
print("="*70)
if __name__ == "__main__":
main()
@@ -0,0 +1,957 @@
"""
空间推理示例 (Spatial Reasoning Example)
=======================================
本示例展示空间智能系统中的空间推理方法。
空间推理是从已知空间事实推导新知识的过程。
核心概念:
1. 定性推理 - 使用定性术语描述空间关系
2. 定量推理 - 使用精确数值计算
3. 空间逻辑 - 形式化的空间推理规则
4. 路径规划 - 寻找最优路径
5. 可见性分析 - 判断视线可见性
应用场景:
- 导航与路径规划
- 空间查询与分析
- 地理推理系统
- 机器人导航
作者: CC4SI 项目组
"""
import math
import heapq
from typing import List, Dict, Tuple, Optional, Set, Any
from dataclasses import dataclass, field
from enum import Enum
from abc import ABC, abstractmethod
import random
# 导入空间表征示例中的基础类
import sys
import os
sys.path.append(os.path.dirname(__file__))
try:
from spatial_representation import Point, LineString, Polygon, Envelope
except ImportError:
# 如果导入失败,定义简化版本
@dataclass
class Point:
x: float
y: float
def distance_to(self, other):
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
def __repr__(self):
return f"({self.x:.2f}, {self.y:.2f})"
# ============================================================================
# 定性空间推理
# ============================================================================
class CardinalDirection(Enum):
"""基本方向"""
NORTH = "N"
SOUTH = "S"
EAST = "E"
WEST = "W"
NORTHEAST = "NE"
NORTHWEST = "NW"
SOUTHEAST = "SE"
SOUTHWEST = "SW"
@classmethod
def from_angle(cls, angle: float) -> 'CardinalDirection':
"""
从角度获取方向
Args:
angle: 角度 (度, 0=东, 90=北)
Returns:
方向枚举
"""
# 归一化到0-360
angle = angle % 360
if angle >= 337.5 or angle < 22.5:
return cls.EAST
elif 22.5 <= angle < 67.5:
return cls.NORTHEAST
elif 67.5 <= angle < 112.5:
return cls.NORTH
elif 112.5 <= angle < 157.5:
return cls.NORTHWEST
elif 157.5 <= angle < 202.5:
return cls.WEST
elif 202.5 <= angle < 247.5:
return cls.SOUTHWEST
elif 247.5 <= angle < 292.5:
return cls.SOUTH
else: # 292.5 <= angle < 337.5
return cls.SOUTHEAST
@dataclass
class QualitativeRelation:
"""定性空间关系"""
relation_type: str # "direction", "distance", "topology"
value: str # 如 "north", "near", "inside"
confidence: float = 1.0
def __repr__(self) -> str:
return f"{self.relation_type}={self.value} (conf={self.confidence:.2f})"
class QualitativeReasoner:
"""
定性空间推理器
使用定性术语进行空间推理。
"""
def __init__(self):
self.facts: List[Tuple[str, str, QualitativeRelation]] = []
def add_fact(self, entity1: str, entity2: str,
relation: QualitativeRelation) -> None:
"""添加空间事实"""
self.facts.append((entity1, entity2, relation))
def infer_direction(self, from_point: Point, to_point: Point) -> QualitativeRelation:
"""
推断两点之间的方向关系
Args:
from_point: 起始点
to_point: 目标点
Returns:
方向关系
"""
dx = to_point.x - from_point.x
dy = to_point.y - from_point.y
# 计算角度 (从东开始逆时针)
angle = math.degrees(math.atan2(dy, dx))
direction = CardinalDirection.from_angle(angle)
return QualitativeRelation(
relation_type="direction",
value=direction.value,
confidence=1.0
)
def infer_distance_category(self, p1: Point, p2: Point,
thresholds: Dict[str, float] = None) -> QualitativeRelation:
"""
推断距离类别
Args:
p1: 第一个点
p2: 第二个点
thresholds: 距离阈值字典
Returns:
距离类别关系
"""
if thresholds is None:
thresholds = {
"very_close": 100,
"close": 500,
"moderate": 1000,
"far": 5000
}
dist = p1.distance_to(p2)
if dist < thresholds.get("very_close", 100):
category = "very_close"
elif dist < thresholds.get("close", 500):
category = "close"
elif dist < thresholds.get("moderate", 1000):
category = "moderate"
else:
category = "far"
return QualitativeRelation(
relation_type="distance",
value=category,
confidence=1.0
)
def compose_relations(self, rel1: QualitativeRelation,
rel2: QualitativeRelation) -> QualitativeRelation:
"""
组合两个关系
例如: A在B的北边,B在C的东边 -> A在C的东北边
"""
if rel1.relation_type == "direction" and rel2.relation_type == "direction":
# 方向组合
return self._compose_directions(rel1.value, rel2.value)
return QualitativeRelation(
relation_type="unknown",
value="unknown",
confidence=0.5
)
def _compose_directions(self, dir1: str, dir2: str) -> QualitativeRelation:
"""组合两个方向"""
direction_map = {
"N": (0, 1), "S": (0, -1), "E": (1, 0), "W": (-1, 0),
"NE": (1, 1), "NW": (-1, 1), "SE": (1, -1), "SW": (-1, -1)
}
if dir1 in direction_map and dir2 in direction_map:
v1 = direction_map[dir1]
v2 = direction_map[dir2]
# 向量相加
result = (v1[0] + v2[0], v1[1] + v2[1])
# 找到最接近的方向
best_dir = "unknown"
best_dot = -1
for name, vec in direction_map.items():
dot = result[0] * vec[0] + result[1] * vec[1]
if dot > best_dot:
best_dot = dot
best_dir = name
return QualitativeRelation(
relation_type="direction",
value=best_dir,
confidence=0.8
)
return QualitativeRelation("direction", "unknown", 0.3)
# ============================================================================
# 路径规划
# ============================================================================
@dataclass
class Node:
"""图节点"""
id: str
point: Point
neighbors: List[str] = field(default_factory=list)
def __hash__(self):
return hash(self.id)
@dataclass
class Edge:
"""图边"""
from_node: str
to_node: str
weight: float # 权重 (如距离)
bidirectional: bool = True
class SpatialGraph:
"""
空间图
用于路径规划的空间网络结构。
"""
def __init__(self):
self.nodes: Dict[str, Node] = {}
self.edges: List[Edge] = []
def add_node(self, id: str, point: Point) -> Node:
"""添加节点"""
node = Node(id=id, point=point)
self.nodes[id] = node
return node
def add_edge(self, from_id: str, to_id: str, weight: float = None,
bidirectional: bool = True) -> None:
"""添加边"""
if from_id not in self.nodes or to_id not in self.nodes:
raise ValueError("节点不存在")
# 如果未指定权重,使用欧氏距离
if weight is None:
weight = self.nodes[from_id].point.distance_to(self.nodes[to_id].point)
edge = Edge(from_id, to_id, weight, bidirectional)
self.edges.append(edge)
# 更新邻接关系
self.nodes[from_id].neighbors.append(to_id)
if bidirectional:
self.nodes[to_id].neighbors.append(from_id)
def get_edge_weight(self, from_id: str, to_id: str) -> float:
"""获取边的权重"""
for edge in self.edges:
if edge.from_node == from_id and edge.to_node == to_id:
return edge.weight
if edge.bidirectional and edge.from_node == to_id and edge.to_node == from_id:
return edge.weight
return float('inf')
def shortest_path(self, start_id: str, end_id: str) -> Optional[List[str]]:
"""
使用Dijkstra算法计算最短路径
Args:
start_id: 起始节点ID
end_id: 目标节点ID
Returns:
节点ID列表,表示路径
"""
if start_id not in self.nodes or end_id not in self.nodes:
return None
# 优先队列: (距离, 节点ID)
pq = [(0, start_id)]
# 距离字典
distances = {node_id: float('inf') for node_id in self.nodes}
distances[start_id] = 0
# 前驱节点
previous = {start_id: None}
# 已访问
visited = set()
while pq:
current_dist, current_id = heapq.heappop(pq)
if current_id in visited:
continue
visited.add(current_id)
if current_id == end_id:
break
# 检查所有邻居
for neighbor_id in self.nodes[current_id].neighbors:
if neighbor_id in visited:
continue
edge_weight = self.get_edge_weight(current_id, neighbor_id)
new_dist = current_dist + edge_weight
if new_dist < distances[neighbor_id]:
distances[neighbor_id] = new_dist
previous[neighbor_id] = current_id
heapq.heappush(pq, (new_dist, neighbor_id))
# 重建路径
if distances[end_id] == float('inf'):
return None
path = []
current = end_id
while current is not None:
path.append(current)
current = previous.get(current)
path.reverse()
return path
def shortest_path_distance(self, start_id: str, end_id: str) -> float:
"""获取最短路径距离"""
path = self.shortest_path(start_id, end_id)
if not path:
return float('inf')
total = 0.0
for i in range(len(path) - 1):
total += self.get_edge_weight(path[i], path[i + 1])
return total
# ============================================================================
# A*路径规划
# ============================================================================
class AStarPlanner:
"""
A*路径规划器
使用启发式搜索的高效路径规划算法。
"""
def __init__(self, graph: SpatialGraph):
self.graph = graph
def heuristic(self, node_id: str, goal_id: str) -> float:
"""
启发式函数 (使用欧氏距离)
Args:
node_id: 当前节点
goal_id: 目标节点
Returns:
启发式估计值
"""
if node_id not in self.graph.nodes or goal_id not in self.graph.nodes:
return 0.0
return self.graph.nodes[node_id].point.distance_to(
self.graph.nodes[goal_id].point
)
def plan(self, start_id: str, goal_id: str) -> Optional[Tuple[List[str], float]]:
"""
规划路径
Args:
start_id: 起始节点ID
goal_id: 目标节点ID
Returns:
(路径节点列表, 总距离) 或 None
"""
if start_id not in self.graph.nodes or goal_id not in self.graph.nodes:
return None
# 开集和闭集
open_set = {start_id}
closed_set = set()
# g值: 从起点到当前节点的实际距离
g_score = {node_id: float('inf') for node_id in self.graph.nodes}
g_score[start_id] = 0
# f值: g值 + 启发式值
f_score = {node_id: float('inf') for node_id in self.graph.nodes}
f_score[start_id] = self.heuristic(start_id, goal_id)
# 前驱节点
came_from = {}
while open_set:
# 获取f值最小的节点
current = min(open_set, key=lambda x: f_score[x])
if current == goal_id:
# 重建路径
path = [current]
total_distance = g_score[current]
while current in came_from:
current = came_from[current]
path.append(current)
path.reverse()
return (path, total_distance)
open_set.remove(current)
closed_set.add(current)
# 检查邻居
for neighbor in self.graph.nodes[current].neighbors:
if neighbor in closed_set:
continue
# 计算 tentative_g_score
edge_weight = self.graph.get_edge_weight(current, neighbor)
tentative_g = g_score[current] + edge_weight
if neighbor not in open_set:
open_set.add(neighbor)
elif tentative_g >= g_score[neighbor]:
continue
# 更新
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self.heuristic(neighbor, goal_id)
return None # 没有找到路径
# ============================================================================
# 可见性分析
# ============================================================================
class VisibilityAnalyzer:
"""
可见性分析器
判断点之间的可见性,考虑障碍物。
"""
def __init__(self, obstacles: List[Polygon] = None):
"""
初始化可见性分析器
Args:
obstacles: 障碍物多边形列表
"""
self.obstacles = obstacles or []
def add_obstacle(self, obstacle: Polygon) -> None:
"""添加障碍物"""
self.obstacles.append(obstacle)
def is_visible(self, p1: Point, p2: Point,
tolerance: float = 1e-6) -> bool:
"""
判断两点之间是否可见
Args:
p1: 第一个点
p2: 第二个点
tolerance: 容差
Returns:
是否可见
"""
# 检视线是否与任何障碍物相交
for obstacle in self.obstacles:
if self._line_intersects_polygon(p1, p2, obstacle):
return False
return True
def _line_intersects_polygon(self, p1: Point, p2: Point,
polygon: Polygon) -> bool:
"""判断线段是否与多边形相交"""
# 首先检查包围盒
line_min_x = min(p1.x, p2.x)
line_max_x = max(p1.x, p2.x)
line_min_y = min(p1.y, p2.y)
line_max_y = max(p1.y, p2.y)
poly_min_x = min(p.x for p in polygon.exterior)
poly_max_x = max(p.x for p in polygon.exterior)
poly_min_y = min(p.y for p in polygon.exterior)
poly_max_y = max(p.y for p in polygon.exterior)
# 包围盒不相交
if line_max_x < poly_min_x or line_min_x > poly_max_x or \
line_max_y < poly_min_y or line_min_y > poly_max_y:
return False
# 检查线段是否与多边形的任何边相交
n = len(polygon.exterior)
for i in range(n):
v1 = polygon.exterior[i]
v2 = polygon.exterior[(i + 1) % n]
if self._segments_intersect(p1, p2, v1, v2):
return True
return False
def _segments_intersect(self, p1: Point, p2: Point,
p3: Point, p4: Point) -> bool:
"""判断两条线段是否相交"""
def orientation(a, b, c):
val = (b.y - a.y) * (c.x - b.x) - (b.x - a.x) * (c.y - b.y)
if abs(val) < 1e-10:
return 0 # 共线
return 1 if val > 0 else 2 # 顺时针/逆时针
def on_segment(a, b, c):
return min(a.x, c.x) <= b.x <= max(a.x, c.x) and \
min(a.y, c.y) <= b.y <= max(a.y, c.y)
o1 = orientation(p1, p2, p3)
o2 = orientation(p1, p2, p4)
o3 = orientation(p3, p4, p1)
o4 = orientation(p3, p4, p2)
# 一般情况
if o1 != o2 and o3 != o4:
return True
# 特殊情况
if o1 == 0 and on_segment(p1, p3, p2):
return True
if o2 == 0 and on_segment(p1, p4, p2):
return True
if o3 == 0 and on_segment(p3, p1, p4):
return True
if o4 == 0 and on_segment(p3, p2, p4):
return True
return False
def viewshed(self, observer: Point, radius: float,
num_rays: int = 360) -> List[Tuple[Point, bool]]:
"""
计算视域 (可视范围)
Args:
observer: 观察点
radius: 视距
num_rays: 射线数量
Returns:
(点, 可见性) 列表
"""
results = []
for i in range(num_rays):
angle = 2 * math.pi * i / num_rays
target = Point(
observer.x + radius * math.cos(angle),
observer.y + radius * math.sin(angle)
)
visible = self.is_visible(observer, target)
results.append((target, visible))
return results
# ============================================================================
# 空间推理引擎
# ============================================================================
class SpatialReasoningEngine:
"""
空间推理引擎
集成多种空间推理功能的综合引擎。
"""
def __init__(self):
self.qualitative_reasoner = QualitativeReasoner()
self.graph = SpatialGraph()
self.visibility_analyzer = VisibilityAnalyzer()
self.astar_planner = None
def build_road_network(self, points: List[Tuple[str, Point]],
connections: List[Tuple[str, str]]) -> None:
"""
构建道路网络
Args:
points: (节点ID, 点) 列表
connections: (节点1, 节点2) 连接列表
"""
for id, point in points:
self.graph.add_node(id, point)
for id1, id2 in connections:
self.graph.add_edge(id1, id2)
self.astar_planner = AStarPlanner(self.graph)
def navigate(self, start: str, goal: str) -> Optional[Dict[str, Any]]:
"""
导航规划
Args:
start: 起始点ID
goal: 目标点ID
Returns:
导航结果字典
"""
if not self.astar_planner:
return None
result = self.astar_planner.plan(start, goal)
if result:
path, distance = result
# 计算方向指示
directions = []
for i in range(len(path) - 1):
from_node = self.graph.nodes[path[i]]
to_node = self.graph.nodes[path[i + 1]]
relation = self.qualitative_reasoner.infer_direction(
from_node.point, to_node.point
)
directions.append({
"from": path[i],
"to": path[i + 1],
"direction": relation.value,
"distance": from_node.point.distance_to(to_node.point)
})
return {
"path": path,
"total_distance": distance,
"num_steps": len(path) - 1,
"directions": directions
}
return None
def query_relation(self, entity1: str, entity2: str,
point1: Point, point2: Point) -> Dict[str, Any]:
"""
查询两个实体之间的空间关系
Args:
entity1: 实体1名称
entity2: 实体2名称
point1: 实体1位置
point2: 实体2位置
Returns:
关系字典
"""
direction = self.qualitative_reasoner.infer_direction(point1, point2)
distance_cat = self.qualitative_reasoner.infer_distance_category(point1, point2)
actual_distance = point1.distance_to(point2)
return {
"entity1": entity1,
"entity2": entity2,
"direction": direction.value,
"distance_category": distance_cat.value,
"actual_distance": actual_distance,
"bearing": math.degrees(math.atan2(
point2.y - point1.y,
point2.x - point1.x
))
}
# ============================================================================
# 主程序
# ========================================================================
def main():
"""主程序 - 演示空间推理的使用"""
print("="*70)
print("空间推理示例演示")
print("="*70)
# ========================================================================
# 1. 定性空间推理
# ========================================================================
print("\n[部分 1] 定性空间推理")
print("-" * 50)
reasoner = QualitativeReasoner()
# 创建几个地标点
landmarks = {
"西湖": Point(120.148, 30.259),
"钱塘江": Point(120.250, 30.200),
"滨江": Point(120.350, 30.220),
"萧山": Point(120.400, 30.150)
}
print("\n地标位置:")
for name, point in landmarks.items():
print(f" {name}: {point}")
# 推断方向关系
print("\n方向关系:")
for i, (name1, point1) in enumerate(list(landmarks.items())[:-1]):
name2 = list(landmarks.keys())[i + 1]
point2 = landmarks[name2]
relation = reasoner.infer_direction(point1, point2)
dist_relation = reasoner.infer_distance_category(point1, point2)
print(f" {name1} -> {name2}:")
print(f" 方向: {relation.value}")
print(f" 距离类别: {dist_relation.value}")
# ========================================================================
# 2. 路径规划
# ========================================================================
print("\n\n[部分 2] 路径规划")
print("-" * 50)
# 创建道路网络
network_points = [
("A", Point(0, 0)),
("B", Point(50, 30)),
("C", Point(100, 50)),
("D", Point(30, 80)),
("E", Point(80, 100)),
("F", Point(120, 120)),
("G", Point(150, 60))
]
connections = [
("A", "B"), ("B", "C"), ("A", "D"),
("B", "D"), ("D", "E"), ("C", "E"),
("E", "F"), ("C", "G"), ("G", "F")
]
graph = SpatialGraph()
for id, point in network_points:
graph.add_node(id, point)
for id1, id2 in connections:
graph.add_edge(id1, id2)
print("\n道路网络:")
print(f" 节点数: {len(graph.nodes)}")
print(f" 边数: {len(graph.edges)}")
# Dijkstra最短路径
print("\nDijkstra最短路径 (A -> F):")
dijkstra_path = graph.shortest_path("A", "F")
if dijkstra_path:
print(f" 路径: {' -> '.join(dijkstra_path)}")
distance = graph.shortest_path_distance("A", "F")
print(f" 总距离: {distance:.2f}")
# A*路径规划
print("\nA*路径规划 (A -> F):")
astar = AStarPlanner(graph)
astar_result = astar.plan("A", "F")
if astar_result:
path, dist = astar_result
print(f" 路径: {' -> '.join(path)}")
print(f" 总距离: {dist:.2f}")
# ========================================================================
# 3. 可见性分析
# ========================================================================
print("\n\n[部分 3] 可见性分析")
print("-" * 50)
# 创建障碍物
obstacle1 = Polygon(exterior=[
Point(60, 40),
Point(80, 40),
Point(80, 70),
Point(60, 70),
Point(60, 40)
])
obstacle2 = Polygon(exterior=[
Point(100, 80),
Point(130, 80),
Point(130, 110),
Point(100, 110),
Point(100, 80)
])
visibility = VisibilityAnalyzer([obstacle1, obstacle2])
print("\n障碍物:")
print(f" 障碍物1: {obstacle1}")
print(f" 障碍物2: {obstacle2}")
# 测试可见性
observer = Point(30, 50)
test_points = [
("目标A", Point(90, 50)), # 被障碍物1遮挡
("目标B", Point(120, 120)), # 被障碍物2遮挡
("目标C", Point(150, 30)), # 可见
("目标D", Point(50, 90)) # 可见
]
print(f"\n从观察点 {observer} 观察:")
for name, target in test_points:
visible = visibility.is_visible(observer, target)
status = "可见" if visible else "不可见"
print(f" {name} {target}: {status}")
# ========================================================================
# 4. 综合推理引擎
# ========================================================================
print("\n\n[部分 4] 综合空间推理引擎")
print("-" * 50)
engine = SpatialReasoningEngine()
# 构建城市路网
city_points = [
("火车站", Point(100, 100)),
("市政府", Point(150, 120)),
("西湖", Point(200, 100)),
("钱江新城", Point(180, 180)),
("滨江", Point(120, 200)),
("萧山机场", Point(250, 250))
]
city_connections = [
("火车站", "市政府"),
("火车站", "滨江"),
("市政府", "西湖"),
("市政府", "钱江新城"),
("滨江", "钱江新城"),
("钱江新城", "萧山机场"),
("西湖", "萧山机场")
]
engine.build_road_network(city_points, city_connections)
print("\n城市路网:")
for name, point in city_points:
print(f" {name}: {point}")
# 导航示例
print("\n导航示例: 从 火车站 到 萧山机场")
nav_result = engine.navigate("火车站", "萧山机场")
if nav_result:
print(f"\n路径规划结果:")
print(f" 路径: {' -> '.join(nav_result['path'])}")
print(f" 总距离: {nav_result['total_distance']:.2f}")
print(f" 步数: {nav_result['num_steps']}")
print(f"\n详细指引:")
for i, step in enumerate(nav_result['directions'], 1):
dir_map = {
"N": "向北", "S": "向南", "E": "向东", "W": "向西",
"NE": "向东北", "NW": "向西北", "SE": "向东南", "SW": "向西南"
}
direction_cn = dir_map.get(step['direction'], step['direction'])
print(f" {i}. 从 {step['from']} {direction_cn} 前往 {step['to']} "
f"(距离: {step['distance']:.1f})")
# 空间关系查询
print("\n空间关系查询:")
relation = engine.query_relation(
"火车站", "西湖",
city_points[0][1], city_points[2][1]
)
print(f" {relation['entity1']} 相对于 {relation['entity2']}:")
print(f" 方向: {relation['direction']}")
print(f" 距离: {relation['actual_distance']:.2f}")
print(f" 方位角: {relation['bearing']:.1f}°")
# ========================================================================
# 5. 多路径比较
# ========================================================================
print("\n\n[部分 5] 多路径比较")
print("-" * 50)
destinations = ["市政府", "西湖", "钱江新城", "滨江", "萧山机场"]
start = "火车站"
print(f"\n{start} 到各目的地的距离:")
results = []
for dest in destinations:
if dest == start:
continue
path_info = engine.navigate(start, dest)
if path_info:
results.append((dest, path_info['total_distance'], path_info['path']))
results.sort(key=lambda x: x[1])
for i, (dest, dist, path) in enumerate(results, 1):
print(f" {i}. {dest:8s}: {dist:6.1f} (路径: {' -> '.join(path)})")
print("\n" + "="*70)
print("演示完成!")
print("="*70)
if __name__ == "__main__":
main()
@@ -0,0 +1,982 @@
"""
空间表征示例 (Spatial Representation Example)
============================================
本示例展示空间智能系统中的空间表征方法。
空间表征是对地理空间现象的抽象和建模,是空间推理的基础。
核心概念:
1. 空间对象模型 - 点、线、面等几何对象
2. 空间关系模型 - 拓扑、距离、方向关系
3. 空间场模型 - 连续表面的表示
4. 空间索引结构 - 加速空间查询
5. 多尺度表征 - 不同详细程度的表示
应用场景:
- 地理信息系统 (GIS)
- 空间数据库
- 空间推理引擎
- 地理可视化
作者: CC4SI 项目组
"""
import math
import json
from typing import List, Dict, Tuple, Optional, Any, Set
from dataclasses import dataclass, field
from enum import Enum
from abc import ABC, abstractmethod
import random
# ============================================================================
# 几何类型与空间对象
# ============================================================================
class GeometryType(Enum):
"""几何类型枚举"""
POINT = "Point"
LINESTRING = "LineString"
POLYGON = "Polygon"
MULTIPOINT = "MultiPoint"
MULTILINESTRING = "MultiLineString"
MULTIPOLYGON = "MultiPolygon"
GEOMETRYCOLLECTION = "GeometryCollection"
class SpatialReference(Enum):
"""空间参考系统"""
WGS84 = "EPSG:4326" # 经纬度
WEB_MERCATOR = "EPSG:3857" # Web墨卡托
CGCS2000 = "EPSG:4490" # 中国大地坐标系统
@dataclass
class Point:
"""点几何对象"""
x: float
y: float
srid: int = 4326 # 空间参考ID
def __iter__(self):
"""支持解包"""
return iter((self.x, self.y))
def __iter__(self):
return iter((self.x, self.y))
def to_tuple(self) -> Tuple[float, float]:
"""转换为元组"""
return (self.x, self.y)
def to_geojson(self) -> Dict:
"""转换为GeoJSON"""
return {
"type": "Point",
"coordinates": [self.x, self.y]
}
def distance_to(self, other: 'Point') -> float:
"""计算到另一个点的欧氏距离"""
return math.sqrt((self.x - other.x)**2 + (self.y - other.y)**2)
def __repr__(self) -> str:
return f"Point({self.x:.4f}, {self.y:.4f})"
@dataclass
class LineString:
"""线几何对象"""
coordinates: List[Point]
srid: int = 4326
@property
def length(self) -> float:
"""计算线的长度"""
if len(self.coordinates) < 2:
return 0.0
total = 0.0
for i in range(len(self.coordinates) - 1):
total += self.coordinates[i].distance_to(self.coordinates[i + 1])
return total
def to_geojson(self) -> Dict:
"""转换为GeoJSON"""
return {
"type": "LineString",
"coordinates": [[p.x, p.y] for p in self.coordinates]
}
def get_point_at(self, ratio: float) -> Point:
"""
获取线上指定比例位置的点
Args:
ratio: 0到1之间的比例值
Returns:
该位置的点
"""
if ratio <= 0:
return self.coordinates[0]
if ratio >= 1:
return self.coordinates[-1]
target_length = self.length * ratio
accumulated = 0.0
for i in range(len(self.coordinates) - 1):
segment_length = self.coordinates[i].distance_to(self.coordinates[i + 1])
if accumulated + segment_length >= target_length:
# 在此段上
segment_ratio = (target_length - accumulated) / segment_length
p1 = self.coordinates[i]
p2 = self.coordinates[i + 1]
return Point(
p1.x + (p2.x - p1.x) * segment_ratio,
p1.y + (p2.y - p1.y) * segment_ratio
)
accumulated += segment_length
return self.coordinates[-1]
def __repr__(self) -> str:
return f"LineString({len(self.coordinates)} points)"
@dataclass
class Polygon:
"""面几何对象"""
exterior: List[Point] # 外环
interiors: List[List[Point]] = field(default_factory=list) # 内环(空洞)
srid: int = 4326
@property
def area(self) -> float:
"""使用鞋带公式计算多边形面积"""
return self._ring_area(self.exterior) - sum(
self._ring_area(interior) for interior in self.interiors
)
def _ring_area(self, ring: List[Point]) -> float:
"""计算环的面积(绝对值)"""
if len(ring) < 3:
return 0.0
area = 0.0
for i in range(len(ring)):
j = (i + 1) % len(ring)
area += ring[i].x * ring[j].y
area -= ring[j].x * ring[i].y
return abs(area) / 2
@property
def centroid(self) -> Point:
"""计算多边形质心"""
if len(self.exterior) < 3:
return self.exterior[0] if self.exterior else Point(0, 0)
# 简化:使用外环的顶点平均值
avg_x = sum(p.x for p in self.exterior) / len(self.exterior)
avg_y = sum(p.y for p in self.exterior) / len(self.exterior)
return Point(avg_x, avg_y)
def contains_point(self, point: Point) -> bool:
"""
判断点是否在多边形内(射线法)
Args:
point: 待判断的点
Returns:
是否在多边形内
"""
return self._point_in_ring(point, self.exterior) and \
all(not self._point_in_ring(point, interior)
for interior in self.interiors)
def _point_in_ring(self, point: Point, ring: List[Point]) -> bool:
"""射线法判断点是否在环内"""
if len(ring) < 3:
return False
x, y = point.x, point.y
inside = False
for i in range(len(ring)):
j = (i + 1) % len(ring)
xi, yi = ring[i].x, ring[i].y
xj, yj = ring[j].x, ring[j].y
# 检查射线与边的交点
if ((yi > y) != (yj > y)) and \
(x < (xj - xi) * (y - yi) / (yj - yi + 1e-10) + xi):
inside = not inside
return inside
def to_geojson(self) -> Dict:
"""转换为GeoJSON"""
coords = [[[p.x, p.y] for p in self.exterior]]
coords.extend([[[p.x, p.y] for p in interior] for interior in self.interiors])
return {
"type": "Polygon",
"coordinates": coords
}
def __repr__(self) -> str:
holes = len(self.interiors)
return f"Polygon({len(self.exterior)} vertices{f', {holes} holes' if holes > 0 else ''})"
@dataclass
class Envelope:
"""包围盒"""
min_x: float
max_x: float
min_y: float
max_y: float
@property
def width(self) -> float:
return self.max_x - self.min_x
@property
def height(self) -> float:
return self.max_y - self.min_y
@property
def area(self) -> float:
return self.width * self.height
@property
def center(self) -> Point:
return Point(
(self.min_x + self.max_x) / 2,
(self.min_y + self.max_y) / 2
)
def contains(self, point: Point) -> bool:
"""判断点是否在包围盒内"""
return (self.min_x <= point.x <= self.max_x and
self.min_y <= point.y <= self.max_y)
def intersects(self, other: 'Envelope') -> bool:
"""判断是否与另一个包围盒相交"""
return not (self.max_x < other.min_x or self.min_x > other.max_x or
self.max_y < other.min_y or self.min_y > other.max_y)
def union(self, other: 'Envelope') -> 'Envelope':
"""计算与另一个包围盒的并集"""
return Envelope(
min_x=min(self.min_x, other.min_x),
max_x=max(self.max_x, other.max_x),
min_y=min(self.min_y, other.min_y),
max_y=max(self.max_y, other.max_y)
)
def __repr__(self) -> str:
return f"Envelope([{self.min_x:.2f}, {self.min_y:.2f}] -> [{self.max_x:.2f}, {self.max_y:.2f}])"
# ============================================================================
# 空间特征对象
# ============================================================================
@dataclass
class SpatialFeature:
"""
空间特征
包含几何和属性信息的完整空间对象。
"""
id: str
geometry: Any # Point, LineString, Polygon等
properties: Dict[str, Any] = field(default_factory=dict)
def to_geojson(self) -> Dict:
"""转换为GeoJSON Feature"""
geom_data = None
if isinstance(self.geometry, Point):
geom_data = self.geometry.to_geojson()
elif isinstance(self.geometry, LineString):
geom_data = self.geometry.to_geojson()
elif isinstance(self.geometry, Polygon):
geom_data = self.geometry.to_geojson()
return {
"type": "Feature",
"id": self.id,
"geometry": geom_data,
"properties": self.properties
}
def to_geojson_collection(self) -> Dict:
"""转换为GeoJSON FeatureCollection"""
return {
"type": "FeatureCollection",
"features": [self.to_geojson()]
}
def envelope(self) -> Envelope:
"""计算包围盒"""
if isinstance(self.geometry, Point):
return Envelope(
self.geometry.x, self.geometry.x,
self.geometry.y, self.geometry.y
)
elif isinstance(self.geometry, (LineString, list)):
coords = self.geometry.coordinates if isinstance(self.geometry, LineString) else self.geometry
xs = [p.x for p in coords]
ys = [p.y for p in coords]
return Envelope(min(xs), max(xs), min(ys), max(ys))
elif isinstance(self.geometry, Polygon):
xs = [p.x for p in self.geometry.exterior]
ys = [p.y for p in self.geometry.exterior]
return Envelope(min(xs), max(xs), min(ys), max(ys))
else:
raise ValueError(f"Unsupported geometry type: {type(self.geometry)}")
def __repr__(self) -> str:
return f"SpatialFeature(id={self.id}, geom={type(self.geometry).__name__})"
# ============================================================================
# 空间关系
# ============================================================================
class SpatialRelation(Enum):
"""空间关系类型"""
EQUALS = "equals"
DISJOINT = "disjoint"
INTERSECTS = "intersects"
TOUCHES = "touches"
CROSSES = "crosses"
WITHIN = "within"
CONTAINS = "contains"
OVERLAPS = "overlaps"
@dataclass
class TopologyRelation:
"""
拓扑关系描述
基于DE-9IM (Dimensionally Extended nine-Intersection Model)模型。
"""
relation: SpatialRelation
confidence: float = 1.0
def __repr__(self) -> str:
return f"TopologyRelation({self.relation.value}, conf={self.confidence:.2f})"
def calculate_relation(geom1: Any, geom2: Any) -> TopologyRelation:
"""
计算两个几何对象的空间关系
Args:
geom1: 第一个几何对象
geom2: 第二个几何对象
Returns:
拓扑关系
"""
# 点-点关系
if isinstance(geom1, Point) and isinstance(geom2, Point):
if geom1.x == geom2.x and geom1.y == geom2.y:
return TopologyRelation(SpatialRelation.EQUALS)
return TopologyRelation(SpatialRelation.DISJOINT)
# 点-多边形关系
if isinstance(geom1, Point) and isinstance(geom2, Polygon):
if geom2.contains_point(geom1):
return TopologyRelation(SpatialRelation.WITHIN)
# 检查是否在边界上
for i in range(len(geom2.exterior)):
p1, p2 = geom2.exterior[i], geom2.exterior[(i + 1) % len(geom2.exterior)]
if point_on_segment(geom1, p1, p2):
return TopologyRelation(SpatialRelation.TOUCHES)
return TopologyRelation(SpatialRelation.DISJOINT)
# 多边形-点关系
if isinstance(geom1, Polygon) and isinstance(geom2, Point):
relation = calculate_relation(geom2, geom1)
if relation.relation == SpatialRelation.WITHIN:
return TopologyRelation(SpatialRelation.CONTAINS)
return relation
# 多边形-多边形关系(简化版:只检查相交)
if isinstance(geom1, Polygon) and isinstance(geom2, Polygon):
env1 = envelope_of_polygon(geom1)
env2 = envelope_of_polygon(geom2)
if not env1.intersects(env2):
return TopologyRelation(SpatialRelation.DISJOINT)
# 简化:检查是否有交点
if geom1.contains_point(geom2.exterior[0]):
return TopologyRelation(SpatialRelation.CONTAINS)
if geom2.contains_point(geom1.exterior[0]):
return TopologyRelation(SpatialRelation.WITHIN)
return TopologyRelation(SpatialRelation.INTERSECTS)
return TopologyRelation(SpatialRelation.DISJOINT)
def point_on_segment(point: Point, seg_start: Point, seg_end: Point,
tolerance: float = 1e-6) -> bool:
"""判断点是否在线段上"""
# 检查点是否在线段的包围盒内
if not (min(seg_start.x, seg_end.x) - tolerance <= point.x <=
max(seg_start.x, seg_end.x) + tolerance and
min(seg_start.y, seg_end.y) - tolerance <= point.y <=
max(seg_start.y, seg_end.y) + tolerance):
return False
# 检查三点共线
cross = (seg_end.x - seg_start.x) * (point.y - seg_start.y) - \
(seg_end.y - seg_start.y) * (point.x - seg_start.x)
return abs(cross) < tolerance
def envelope_of_polygon(polygon: Polygon) -> Envelope:
"""计算多边形的包围盒"""
xs = [p.x for p in polygon.exterior]
ys = [p.y for p in polygon.exterior]
return Envelope(min(xs), max(xs), min(ys), max(ys))
# ============================================================================
# 空间索引
# ============================================================================
class RTreeNode:
"""R树节点"""
def __init__(self, is_leaf: bool = False):
self.is_leaf = is_leaf
self.envelope: Optional[Envelope] = None
self.children: List['RTreeNode'] = []
self.features: List[SpatialFeature] = []
def update_envelope(self):
"""更新节点的包围盒"""
if self.is_leaf and self.features:
envelopes = [f.envelope() for f in self.features]
self.envelope = envelopes[0]
for env in envelopes[1:]:
self.envelope = self.envelope.union(env)
elif not self.is_leaf and self.children:
self.envelope = self.children[0].envelope
for child in self.children[1:]:
if child.envelope:
self.envelope = self.envelope.union(child.envelope)
class RTree:
"""
R树空间索引
用于加速空间查询的树状索引结构。
"""
def __init__(self, max_children: int = 4):
"""
初始化R树
Args:
max_children: 每个节点的最大子节点数
"""
self.max_children = max_children
self.root = RTreeNode(is_leaf=True)
self.size = 0
def insert(self, feature: SpatialFeature) -> None:
"""插入空间特征"""
self._insert(self.root, feature)
self.size += 1
def _insert(self, node: RTreeNode, feature: SpatialFeature) -> None:
"""递归插入"""
feature_env = feature.envelope()
if node.is_leaf:
node.features.append(feature)
node.update_envelope()
# 如果超过容量,分裂节点
if len(node.features) > self.max_children:
self._split(node)
else:
# 选择最佳子节点
best_child = self._choose_best_child(node, feature_env)
self._insert(best_child, feature)
node.update_envelope()
def _choose_best_child(self, node: RTreeNode, env: Envelope) -> RTreeNode:
"""选择插入代价最小的子节点"""
best = None
best_increase = float('inf')
for child in node.children:
if child.envelope is None:
continue
union_env = child.envelope.union(env)
increase = union_env.area - child.envelope.area
if increase < best_increase:
best_increase = increase
best = child
return best or node.children[0]
def _split(self, node: RTreeNode) -> None:
"""分裂节点 (简化版)"""
if node.is_leaf:
# 简单分裂:将特征分成两组
mid = len(node.features) // 2
group1 = node.features[:mid]
group2 = node.features[mid:]
node.features = group1
new_leaf = RTreeNode(is_leaf=True)
new_leaf.features = group2
new_leaf.update_envelope()
# 更新父节点
if node == self.root and not node.children:
# 根节点分裂
new_root = RTreeNode(is_leaf=False)
new_root.children = [node, new_leaf]
new_root.update_envelope()
self.root = new_root
else:
# 简化:不处理非根节点的分裂
pass
def query(self, envelope: Envelope) -> List[SpatialFeature]:
"""查询与包围盒相交的所有特征"""
results = []
self._query(self.root, envelope, results)
return results
def _query(self, node: RTreeNode, envelope: Envelope,
results: List[SpatialFeature]) -> None:
"""递归查询"""
if node.envelope and not node.envelope.intersects(envelope):
return
if node.is_leaf:
for feature in node.features:
if feature.envelope().intersects(envelope):
results.append(feature)
else:
for child in node.children:
self._query(child, envelope, results)
def nearest_neighbor(self, point: Point, k: int = 1) -> List[Tuple[SpatialFeature, float]]:
"""
K近邻查询
Args:
point: 查询点
k: 返回的最近邻数量
Returns:
(特征, 距离) 列表
"""
candidates = []
self._collect_candidates(self.root, candidates)
# 计算距离并排序
distances = []
for feature in candidates:
if isinstance(feature.geometry, Point):
dist = feature.geometry.distance_to(point)
distances.append((feature, dist))
else:
# 非点几何:使用包围盒中心距离
env = feature.envelope()
center = env.center
dist = math.sqrt((center.x - point.x)**2 + (center.y - point.y)**2)
distances.append((feature, dist))
distances.sort(key=lambda x: x[1])
return distances[:k]
def _collect_candidates(self, node: RTreeNode, results: List[SpatialFeature]) -> None:
"""收集所有候选特征"""
if node.is_leaf:
results.extend(node.features)
else:
for child in node.children:
self._collect_candidates(child, results)
# ============================================================================
# 空间场模型
# ============================================================================
class GridField:
"""
栅格场模型
使用规则网格表示连续空间现象。
"""
def __init__(self, bounds: Envelope, rows: int, cols: int,
nodata: float = -9999):
"""
初始化栅格场
Args:
bounds: 空间范围
rows: 行数
cols: 列数
nodata: 无数据值
"""
self.bounds = bounds
self.rows = rows
self.cols = cols
self.nodata = nodata
self.data = [[nodata for _ in range(cols)] for _ in range(rows)]
@property
def cell_width(self) -> float:
"""获取单元格宽度"""
return self.bounds.width / self.cols
@property
def cell_height(self) -> float:
"""获取单元格高度"""
return self.bounds.height / self.rows
def get_cell_index(self, point: Point) -> Optional[Tuple[int, int]]:
"""
获取点对应的栅格索引
Args:
point: 空间点
Returns:
(行索引, 列索引) 或 None
"""
if not self.bounds.contains(point):
return None
col = int((point.x - self.bounds.min_x) / self.cell_width)
row = int((self.bounds.max_y - point.y) / self.cell_height)
col = max(0, min(col, self.cols - 1))
row = max(0, min(row, self.rows - 1))
return (row, col)
def set_value(self, row: int, col: int, value: float) -> None:
"""设置栅格值"""
if 0 <= row < self.rows and 0 <= col < self.cols:
self.data[row][col] = value
def get_value(self, row: int, col: int) -> float:
"""获取栅格值"""
if 0 <= row < self.rows and 0 <= col < self.cols:
return self.data[row][col]
return self.nodata
def get_value_at_point(self, point: Point) -> float:
"""获取点位置的值(最近邻)"""
idx = self.get_cell_index(point)
if idx:
return self.get_value(idx[0], idx[1])
return self.nodata
def interpolate_at_point(self, point: Point) -> float:
"""双线性插值获取点位置的值"""
idx = self.get_cell_index(point)
if not idx:
return self.nodata
row, col = idx
# 获取四个角点的值
values = []
for r in range(row, min(row + 2, self.rows)):
for c in range(col, min(col + 2, self.cols)):
values.append((r, c, self.data[r][c]))
if len(values) < 4 or any(v[2] == self.nodata for v in values):
return self.get_value(row, col) # 回退到最近邻
# 双线性插值
# 简化实现
return self.get_value(row, col)
def get_statistics(self) -> Dict[str, float]:
"""获取统计信息"""
values = [v for row in self.data for v in row if v != self.nodata]
if not values:
return {"count": 0, "min": self.nodata, "max": self.nodata,
"mean": self.nodata, "std": 0}
import statistics
return {
"count": len(values),
"min": min(values),
"max": max(values),
"mean": statistics.mean(values),
"std": statistics.stdev(values) if len(values) > 1 else 0
}
def to_geojson(self) -> Dict:
"""转换为GeoJSON(简化:输出为点集)"""
features = []
for r in range(self.rows):
for c in range(self.cols):
val = self.data[r][c]
if val != self.nodata:
# 计算点坐标
x = self.bounds.min_x + (c + 0.5) * self.cell_width
y = self.bounds.max_y - (r + 0.5) * self.cell_height
features.append({
"type": "Feature",
"geometry": {"type": "Point", "coordinates": [x, y]},
"properties": {"value": val}
})
return {
"type": "FeatureCollection",
"features": features
}
# ============================================================================
# 主程序
# ============================================================================
def main():
"""主程序 - 演示空间表征的使用"""
print("="*70)
print("空间表征示例演示")
print("="*70)
# ========================================================================
# 1. 基础几何对象
# ========================================================================
print("\n[部分 1] 基础几何对象")
print("-" * 50)
# 创建点
point1 = Point(120.5, 30.2)
point2 = Point(121.0, 30.5)
print(f"\n点对象:")
print(f" point1 = {point1}")
print(f" point2 = {point2}")
print(f" 距离 = {point1.distance_to(point2):.4f}")
# 创建线
linestring = LineString([
Point(120.0, 30.0),
Point(120.5, 30.2),
Point(121.0, 30.5),
Point(121.5, 30.3)
])
print(f"\n线对象:")
print(f" {linestring}")
print(f" 长度 = {linestring.length:.4f}")
print(f" 中点 = {linestring.get_point_at(0.5)}")
# 创建多边形
polygon = Polygon(exterior=[
Point(120.0, 30.0),
Point(121.0, 30.0),
Point(121.0, 31.0),
Point(120.0, 31.0),
Point(120.0, 30.0)
])
print(f"\n多边形对象:")
print(f" {polygon}")
print(f" 面积 = {polygon.area:.4f}")
print(f" 质心 = {polygon.centroid}")
# 点包含测试
test_inside = Point(120.5, 30.5)
test_outside = Point(121.5, 30.5)
print(f" {test_inside} 在多边形内: {polygon.contains_point(test_inside)}")
print(f" {test_outside} 在多边形内: {polygon.contains_point(test_outside)}")
# ========================================================================
# 2. 空间特征
# ========================================================================
print("\n\n[部分 2] 空间特征")
print("-" * 50)
feature1 = SpatialFeature(
id="poi_001",
geometry=Point(120.5, 30.2),
properties={
"name": "杭州西湖",
"type": "scenic_spot",
"rating": 4.8
}
)
feature2 = SpatialFeature(
id="zone_001",
geometry=polygon,
properties={
"name": "开发区A",
"type": "industrial_zone",
"area_ha": 10000
}
)
print(f"\n特征1: {feature1}")
print(f" 属性: {feature1.properties}")
print(f" 包围盒: {feature1.envelope()}")
print(f"\n特征2: {feature2}")
print(f" 属性: {feature2.properties}")
print(f" 包围盒: {feature2.envelope()}")
# GeoJSON输出
print(f"\nGeoJSON输出:")
print(json.dumps(feature1.to_geojson(), ensure_ascii=False, indent=2))
# ========================================================================
# 3. 空间关系
# ========================================================================
print("\n\n[部分 3] 空间关系")
print("-" * 50)
# 点与多边形的关系
point_a = Point(120.5, 30.5) # 在多边形内
point_b = Point(121.5, 30.5) # 在多边形外
point_c = Point(120.0, 30.5) # 在边界上
print(f"\n点-多边形关系测试:")
print(f" {point_a} 与多边形: {calculate_relation(point_a, polygon)}")
print(f" {point_b} 与多边形: {calculate_relation(point_b, polygon)}")
print(f" {point_c} 与多边形: {calculate_relation(point_c, polygon)}")
# 多边形-多边形关系
poly2 = Polygon(exterior=[
Point(120.5, 29.5),
Point(121.5, 29.5),
Point(121.5, 30.5),
Point(120.5, 30.5),
Point(120.5, 29.5)
])
print(f"\n多边形-多边形关系:")
print(f" poly1 与 poly2: {calculate_relation(polygon, poly2)}")
# ========================================================================
# 4. 空间索引
# ========================================================================
print("\n\n[部分 4] R树空间索引")
print("-" * 50)
# 创建R树
rtree = RTree(max_children=4)
# 插入一些特征
features = []
for i in range(20):
x = random.uniform(119, 122)
y = random.uniform(29, 32)
feature = SpatialFeature(
id=f"feature_{i:03d}",
geometry=Point(x, y),
properties={"value": random.uniform(0, 100)}
)
features.append(feature)
rtree.insert(feature)
print(f"\n已插入 {rtree.size} 个特征到R树")
# 范围查询
query_env = Envelope(120, 121, 30, 31)
results = rtree.query(query_env)
print(f"\n范围查询 {query_env}:")
print(f" 找到 {len(results)} 个特征")
for f in results[:5]:
print(f" - {f.id}: {f.geometry}")
# K近邻查询
query_point = Point(120.5, 30.5)
neighbors = rtree.nearest_neighbor(query_point, k=5)
print(f"\nK近邻查询 (中心点: {query_point}):")
for i, (f, dist) in enumerate(neighbors, 1):
print(f" {i}. {f.id}: 距离 = {dist:.4f}")
# ========================================================================
# 5. 栅格场模型
# ========================================================================
print("\n\n[部分 5] 栅格场模型")
print("-" * 50)
# 创建栅格场
grid = GridField(
bounds=Envelope(119, 122, 29, 32),
rows=30,
cols=30,
nodata=-9999
)
# 填充一些模拟数据 (温度场)
for r in range(grid.rows):
for c in range(grid.cols):
# 计算坐标
x = grid.bounds.min_x + (c + 0.5) * grid.cell_width
y = grid.bounds.max_y - (r + 0.5) * grid.cell_height
# 模拟温度场 (简单的径向基函数)
center_x, center_y = 120.5, 30.5
dist = math.sqrt((x - center_x)**2 + (y - center_y)**2)
temp = 25 - dist * 2 # 中心25度,向外递减
grid.set_value(r, c, round(temp, 2))
print(f"\n栅格场信息:")
print(f" 范围: {grid.bounds}")
print(f" 尺寸: {grid.rows} x {grid.cols}")
print(f" 单元格大小: {grid.cell_width:.4f} x {grid.cell_height:.4f}")
stats = grid.get_statistics()
print(f" 统计: {stats}")
# 点查询
sample_point = Point(120.5, 30.5)
value = grid.get_value_at_point(sample_point)
print(f"\n点位置 {sample_point} 的值: {value}")
# ========================================================================
# 6. GeoJSON导出
# ========================================================================
print("\n\n[部分 6] GeoJSON导出")
print("-" * 50)
# 创建特征集合
feature_collection = {
"type": "FeatureCollection",
"features": [
feature1.to_geojson(),
feature2.to_geojson()
]
}
print("\n特征集合:")
print(json.dumps(feature_collection, ensure_ascii=False, indent=2))
print("\n" + "="*70)
print("演示完成!")
print("="*70)
if __name__ == "__main__":
main()
File diff suppressed because it is too large Load Diff