"""Read-only audit of v0.31 inputs; write v0.32 pressure/stability review.

Run with .venv-device-cad/Scripts/python.exe. This does not import old builders.
"""
import csv
import hashlib
import html
import json
import math
from pathlib import Path

HERE = Path(__file__).resolve().parent
BASE = HERE.parent / "common_support_v0_31"
G = 9.80665


def read(name):
    return json.loads((BASE / name).read_text(encoding="utf-8"))


def dump(name, data):
    (HERE / name).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")


rows = list(csv.DictReader((BASE / "parts.csv").open(encoding="utf-8-sig")))
normal = [p for p in rows if p["kind"] == "normal"]
mass = sum(float(p["mass_kg"]) for p in normal)
cg = [sum(float(p["mass_kg"]) * json.loads(p["cg_mm"])[i] for p in normal) / mass for i in range(3)]
head_mass = sum(float(p["mass_kg"]) for p in normal if p["motion"] != "F")
lift_mass = sum(float(p["mass_kg"]) for p in normal if p["motion"] in ["Z", "W"])
weight = mass * G
meshes = read("meshes.json")
normal_meshes = [m for m in meshes if m["kind"] == "normal"]


def bbox(items):
    vertices = [v for m in items for v in m["vertices"]]
    return [min(v[i] for v in vertices) for i in range(3)] + [max(v[i] for v in vertices) for i in range(3)]


assembly_box = bbox(normal_meshes)
wheel_box = bbox([m for m in normal_meshes if m["id"] == "W_rubber"])
foot_rows = [p for p in normal if p["id"].startswith("old_sole_")]
feet = [json.loads(p["cg_mm"])[:2] for p in foot_rows]
a, b = max(abs(f[0]) for f in feet), max(abs(f[1]) for f in feet)
axis_z = (wheel_box[2] + wheel_box[5]) / 2
contact_z = wheel_box[2]
assert len(feet) == 4 and a == 300 and b == 375
assert abs(mass - read("parameters.json")["normal_model_mass_reference_kg"]) < 1e-8


def static_case(n, fx, y=0, z=0):
    # X: longitudinal; Y: manually repositioned head; Z: vertical.
    # Steady rotation only; no angular acceleration, impact or cable loads.
    # All H/Z/W parts translate in Y, only Z/W parts translate vertically,
    # matching v0.31 review's pose transform.
    cgy = cg[1] + head_mass / mass * y
    h = contact_z + z
    total_r = weight - n
    moment_y = weight * cg[0] + h * fx  # worst direction is positive Fx
    moment_x = weight * cgy - n * y
    forces = [total_r / 4 + x * moment_y / (4 * a * a) + yy * moment_x / (4 * b * b) for x, yy in feet]
    # Equal vertical support stiffness approximation for individual reactions.
    # Global force/moment balance is independent of that approximation.
    balance_errors = [sum(forces) + n - weight,
                      sum(x * r for (x, _), r in zip(feet, forces)) - moment_y,
                      sum(yy * r for (_, yy), r in zip(feet, forces)) - moment_x]
    assert max(abs(v) for v in balance_errors) < 1e-7
    restoring = a * total_r - weight * abs(cg[0])
    return dict(N_N=n, Fx_N=fx, manual_Y_mm=y, lift_Z_mm=z, contact_height_mm=h,
                sum_foot_normal_N=total_r, foot_normal_equal_stiffness_N=forces,
                minimum_foot_normal_equal_stiffness_N=min(forces),
                foot_COP_xy_mm=[moment_y / total_r, moment_x / total_r],
                all_feet_compressive_under_assumed_stiffness=min(forces) > 0,
                COP_inside_foot_centres=abs(moment_y / total_r) < a and abs(moment_x / total_r) < b,
                foot_mu_required_static=abs(fx) / total_r,
                foot_mu_required_with_assumed_1_5_force_reserve=1.5 * abs(fx) / total_r,
                longitudinal_restoring_to_Fx_overturning_ratio=restoring / (h * abs(fx)),
                equilibrium_errors_N_or_Nmm=balance_errors)


central_cases = [static_case(n, n) for n in [50, 100, 150]]
design_pose_cases = [static_case(n, fx, y, z) for n in [50, 100] for fx in [-100, 100]
                     for y in [-150, 0, 150] for z in [-15, 0, 85]]
assert all(c["COP_inside_foot_centres"] and c["all_feet_compressive_under_assumed_stiffness"] for c in design_pose_cases)

area_cases = [dict(N_N=n, target_mean_contact_pressure_MPa=p,
                   required_area_mm2=n / p, required_area_cm2=n / p / 100)
              for n in [50, 100, 150] for p in [0.25, 0.5, 1.0]]
speed_cases = [dict(diameter_mm=d, tread_width_mm=w,
                    rpm_at_60_kmh=16.6666666667 * 60 / (math.pi * d / 1000),
                    wheel_torque_at_Fx100_Nm=100 * d / 2000,
                    ideal_friction_power_at_Fx100_60_kmh_W=100 * 16.6666666667)
               for d, w in [(125, 25), (150, 30), (165, 45), (200, 50)]]
# Ideal gas illustration only, fixed gas amount and volume. No inflation recipe.
patm, t_room, t_cold = 0.101325, 293.15, 243.15
thermal = dict(model="ideal gas, fixed amount and volume; actual tyre volume/cold ratings unknown",
               room_C=20, cold_C=-30,
               cold_gauge_MPa_if_room_gauge_0_5=(0.5 + patm) * t_cold / t_room - patm,
               room_gauge_MPa_if_cold_gauge_0_5=(0.5 + patm) * t_room / t_cold - patm)

sources = [
    dict(id="K671", source="Kenda official product page", size_label="6x1-1/4 / 150x30",
         listed_psi=36, listed_MPa=36 * 0.006894757293168,
         url="https://wheelchair.kendatire.com/en-us/find-a-tire/wheelchair/everyday-homecare-multi-use/k671/",
         status="listed pressure only; complete wheel/rim cold-speed rating not supplied"),
    dict(id="K909", source="Kenda official product page", part_no="109090301B0", size_label="8x2 / 200x50",
         listed_psi=50, listed_MPa=50 * 0.006894757293168,
         url="https://wheelchair.kendatire.com/en-us/find-a-tire/wheelchair/powerchair-scooter/k909/",
         status="listed pressure only; not selected for -30C or 60km/h"),
]
result = dict(version="v0.32", date="2026-10-14", scope="pneumatic requirements and reference static audit",
              baseline="v0.31 remains unchanged", new_pneumatic_CAD_released=False,
              baseline_reference=dict(normal_part_count=len(normal), total_mass_kg=mass, actual_total_mass_kg=None,
                                      cg_mm=cg, head_mass_translated_Y_kg=head_mass, moving_ZW_mass_kg=lift_mass,
                                      bounds_minXYZ_maxXYZ_mm=assembly_box,
                                      envelope_XYZ_mm=[assembly_box[i+3]-assembly_box[i] for i in range(3)],
                                      wheel_rubber_bounds_minXYZ_maxXYZ_mm=wheel_box,
                                      feet_centres_xy_mm=feet, wheel_axis_z_mm=axis_z,
                                      wheel_nominal_contact_height_mm=contact_z),
              assumptions=dict(steady_rotation=True, tyre_contact_x_mm=0,
                               foot_reaction_distribution="four equally stiff point supports at sole centres",
                               illustrative_Fx_bound_N=100, force_reserve_factor=1.5,
                               force_reserve_is_approved_standard=False,
                               foot_friction_measured=False,
                               actual_foot_pressure_distribution_measured=False,
                               dynamic_release_or_impact_checked=False),
              central_cases_Fx_equals_N=central_cases,
              current_first_stage_pose_cases=design_pose_cases,
              pose_summary=dict(case_count=len(design_pose_cases),
                                minimum_foot_normal_equal_stiffness_N=min(c["minimum_foot_normal_equal_stiffness_N"] for c in design_pose_cases),
                                min_longitudinal_restoring_to_Fx_overturning_ratio=min(c["longitudinal_restoring_to_Fx_overturning_ratio"] for c in design_pose_cases),
                                max_mu_with_assumed_1_5_reserve=max(c["foot_mu_required_with_assumed_1_5_force_reserve"] for c in design_pose_cases)),
              contact_area_targets=area_cases, size_speed_torque_examples=speed_cases,
              thermal_pressure_illustration=thermal, public_tyre_examples=sources,
              pneumatic_requirements=dict(compatible_pneumatic_wheel=True, candidate_test_temperature_gauge_pressure_MPa=0.5,
                                          selected_tyre=None, selected_rim=None,
                                          certified_maximum_tyre_pressure_MPa=None,
                                          first_stage_normal_load_target_N=[50, 100], extension_target_N=150,
                                          extension_qualified=False, cold_target_C=[-20, -30],
                                          tread_speed_60_kmh_is_aspiration=True,
                                          invented_meeting_device_width_900mm_is_requirement=False,
                                          new_machine_dimensions_mm=None),
              input_sha256={name: hashlib.sha256((BASE / name).read_bytes()).hexdigest()
                            for name in ["parts.csv", "meshes.json", "parameters.json", "geometry_reference.json"]})
dump("calculation.json", result)

central = central_cases[1]
stability_rows = "".join(f"<tr><td>{c['N_N']:.0f}</td><td>{c['Fx_N']:.0f}</td><td>{c['sum_foot_normal_N']:.1f}</td><td>{c['foot_mu_required_with_assumed_1_5_force_reserve']:.3f}</td><td>{c['longitudinal_restoring_to_Fx_overturning_ratio']:.1f}</td></tr>" for c in central_cases)
tyre_rows = "".join(f"<tr><td><a href='{html.escape(s['url'])}'>{s['id']}</a></td><td>{s['size_label']}</td><td>{s['listed_psi']} psi ≈ {s['listed_MPa']:.3f} MPa</td><td>未选为本机试验轮</td></tr>" for s in sources)
speed_rows = "".join(f"<tr><td>Ø{s['diameter_mm']} × {s['tread_width_mm']}</td><td>{s['rpm_at_60_kmh']:.0f}</td><td>{s['wheel_torque_at_Fx100_Nm']:.2f}</td></tr>" for s in speed_cases)
doc = f"""<!doctype html><html lang='zh-CN'><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>
<title>充气轮胎要求与参考结构核算 v0.32</title>
<style>body{{font:16px/1.75 'Microsoft YaHei',sans-serif;margin:0;background:#f3f5f7;color:#24313c}}main{{max-width:960px;margin:auto;padding:28px}}h1{{font-size:28px;line-height:1.35}}h2{{font-size:21px;margin-top:30px}}section{{background:white;padding:24px;margin:18px 0;border-radius:12px}}table{{border-collapse:collapse;width:100%;font-size:15px}}td,th{{text-align:left;border-bottom:1px solid #dbe1e6;padding:10px}}th{{background:#edf4f7}}a{{color:#166b87}}.lead{{font-size:19px}}.tag{{color:#53636e;font-size:14px}}code{{font-family:Consolas,monospace}}.scroll{{overflow:auto}}@media(max-width:600px){{main{{padding:12px}}section{{padding:16px}}h1{{font-size:23px}}}}</style>
<main><p class='tag'>设备线 · 2026-10-14 · v0.31 只读核查 / v0.32 需求重定草案</p>
<h1>兼容充气轮胎，但胎压上限不能由机架轮载直接换算</h1>
<section><p class='lead'>先把 0.5 MPa 作为充气轮胎的选型目标，把 50—100 N 保留为首版轮载目标。实际允许胎压须由匹配的轮胎、轮辋和气门组件及使用条件确认。</p>
<p>固定轮载时，提高胎压会改变轮胎刚度和接触面积，不会按“胎压 × 轮胎外形面积”增加机架的下压力。本轮没有选定充气轮胎，也没有发布可实际使用的最高胎压。</p></section>
<section><h2>现有图纸和设计资料到哪一步</h2>
<p>已有总装与检修 STEP、轮轴/轴承盒/电机板/承板/共同支承等零件参考模型与 SVG，部分名义二维 DXF；另有材料、紧固、装配、控制、接线、BOM 和校验资料。全套交流文件见 <a href='../common_support_v0_31/manufacturer_exchange_v0_31.zip'>v0.31 厂家交流包</a>，<a href='../common_support_v0_31/common_review.html'>v0.31 总装查看页</a>，<a href='../common_support_v0_31/制造图任务清单.csv'>制造图待办清单</a>。</p>
<p>还需补齐整体焊接制造细图、准确传感器安装面/孔图、真实轮胎轮辋接口、冷态配合与器件额定条件。现有资料可用于厂家详细设计讨论；换充气轮后，相关接口图要重新出。</p>
<p>当前参考总装外包络为 <b>{result['baseline_reference']['envelope_XYZ_mm'][0]:.0f} × {result['baseline_reference']['envelope_XYZ_mm'][1]:.0f} × {result['baseline_reference']['envelope_XYZ_mm'][2]:.0f} mm</b>（X/Y/Z），不是新设计冻结尺寸。900 mm 是底脚外包络，不是有效测量宽度；会议中用户随口提到的设备尺寸作废。</p></section>
<section><h2>三个量分别控制什么</h2><table><tr><th>量</th><th>含义</th><th>控制依据</th></tr>
<tr><td>胎压 p<sub>t</sub>，MPa</td><td>轮胎内部表压</td><td>轮胎＋轮辋匹配、气门组件，以及温度/转速条件</td></tr>
<tr><td>轮载 N，N</td><td>轮子向试样施加的法向力</td><td>机架、反力、加载控制与测力链</td></tr>
<tr><td>平均接触压强 p̄，MPa</td><td>N / 实际接触面积 A</td><td>实测轮印，含轮胎变形和冰雪下陷影响</td></tr></table>
<p>例如 100 N 轮载要得到平均 0.5 MPa，需要面积 200 mm²（2 cm²）；平均 1 MPa 则需 100 mm²（1 cm²）。这不是把胎压设成同一个数值就能实现，轮印必须实测。对雪面还需明确面积与下陷的测定方法。</p></section>
<section><h2>现有结构的静态稳定性参考核算</h2>
<p>读取 v0.31 的 {len(normal)} 个正常总装零件，模型质量 {mass:.2f} kg，重心 X={cg[0]:.1f}、Y={cg[1]:.1f} mm；实机未称重。四脚中心为 (±{a:.0f}, ±{b:.0f}) mm。本表取轮组居中、接触高度 {contact_z:.0f} mm、纵向摩擦力 Fx=N，四脚等竖向刚度；1.5 倍力余量是本轮比较假设。</p>
<div class='scroll'><table><tr><th>轮载 N</th><th>Fx，N</th><th>四脚剩余承重，N</th><th>脚底 μ 需求（1.5倍）</th><th>纵向恢复/倾覆力矩比</th></tr>{stability_rows}</table></div>
<p>100 N 行：四脚总法向力 {central['sum_foot_normal_N']:.1f} N，最小单脚反力约 {central['minimum_foot_normal_equal_stiffness_N']:.1f} N；支承合力中心 X={central['foot_COP_xy_mm'][0]:.1f} mm，位于 ±300 mm 脚中心以内。该条件下，脚底滑移更值得先验证；需要实测低温脚垫/金属地面组合能否达到 μ≈{central['foot_mu_required_with_assumed_1_5_force_reserve']:.2f}。</p>
<p>另扫描首版 N=50/100 N、Fx=±100 N、手动 Y=−150/0/150 mm、Z=−15/0/85 mm 共 {len(design_pose_cases)} 个参考姿态；四脚等刚度模型最小单脚反力 {result['pose_summary']['minimum_foot_normal_equal_stiffness_N']:.1f} N，最小纵向恢复/倾覆比 {result['pose_summary']['min_longitudinal_restoring_to_Fx_overturning_ratio']:.1f}。这些仅覆盖静态姿态；新轮组质量/轴高、实机脚底摩擦和加载瞬态需重新代入。150 N 仍为扩展，现有主测力传感器组合力矩范围也未闭合。</p>
<p><code>R = Mg − N</code>；<code>μ需求 = 1.5 |Fx| / R</code>；<code>x支承合力 = (Mg·x重心 + h·Fx) / R</code>。不能用当前模型 100 kg 的结果证明今后更轻机器同样稳定。</p></section>
<section><h2>公开小气胎参数说明了什么</h2><div class='scroll'><table><tr><th>原厂实例</th><th>原厂尺寸栏</th><th>原厂 PSI 栏换算</th><th>本项目状态</th></tr>{tyre_rows}</table></div>
<p>原厂页面仅给出这些标注，不能把任意小气胎按 0.5 MPa 使用；表中产品也没有给本项目的 −30℃与 60 km/h 完整轮组适用证明。轮胎尺寸标签不是本机装配用的实测外包络。0.5 MPa≈5 bar≈72.5 psi，须专门筛选满足该目标的匹配轮组。</p>
<p>胎压定义为试验温度下记录的实测表压。恒定容积理想气体举例：20℃的 0.5 MPa 到 −30℃约变为 {thermal['cold_gauge_MPa_if_room_gauge_0_5']:.3f} MPa；反过来，−30℃为 0.5 MPa，回到20℃约为 {thermal['room_gauge_MPa_if_cold_gauge_0_5']:.3f} MPa。实际轮胎体积与冷态额定值未知，这个计算只用于安排测压与温度条件，不能用作充气操作值。</p></section>
<section><h2>尺寸重新确定的顺序</h2>
<ol><li>以 0.5 MPa 为选型目标，先核轮胎＋轮辋匹配及冷态/转速范围，取得真实外径、胎宽、轮毂和轴孔。</li>
<li>先改轮组局部：现有 Ø125×25 实心轮位置附近有共同支承梁、轴承盒和电机；更大气胎不能直接替换，见 <a href='envelope_screening.json'>STEP 局部包络筛查</a>。</li>
<li>把地面零点和 50/80 mm 雪层表面重新纳入 Z 高度。当前 Ø125 轮在 Z=0 时底点为100 mm、参考几何位移−15…85 mm；不能把该显示范围当成已确认执行器可用行程。</li>
<li>代入新轮组质量与重心，复算脚距、所需底座质量和防滑条件，再定整机长宽高；不为满足随口提到的90 cm而扩大机架。</li></ol>
<p>60 km/h 仍是胎面线速度争取目标。尺寸变大虽降低转速，却增大同一 Fx 下的轮轴转矩：</p><div class='scroll'><table><tr><th>轮子参考包络，mm</th><th>60 km/h 转速，rpm</th><th>Fx=100 N 的轮轴转矩，Nm</th></tr>{speed_rows}</table></div>
<p>所有尺寸下理想摩擦功率都约1.67 kW；尚未加轴承/传动损失、加速和冷态降额。现有1:1传动、9.8 Nm 电机额定参考下，Ø200 轮的10 Nm已略超这一额定转矩，需重算传动比或驱动。上述表不是高速运行资格。</p></section>
<section><h2>本轮结论与下一项工作</h2><p><b>不从机架反推出最高胎压。先按兼容充气轮、0.5 MPa 选型目标、50—100 N 首版轮载推进；新轮组尺寸确定后，再修轮架和总装尺寸。</b></p>
<p>下一项具体工作是匹配轮胎/轮辋接口与局部布局，随后进行样机静止加载、轮印和低速摩擦验证；60 km/h 分阶段提升。原地旋转且试样固定，输出局部滑动抗力及其时间变化，无法据此宣称已经获得真实跑道自由滚动/制动滑移率曲线。</p>
<p><a href='calculation.json'>完整输入、算例与假设 JSON</a> · <a href='calculate.py'>可复算脚本</a> · <a href='envelope_screening.json'>局部包络结果</a></p></section></main></html>"""
(HERE / "pressure_review.html").write_text(doc, encoding="utf-8")
print(json.dumps(dict(mass_kg=mass, envelope_mm=result['baseline_reference']['envelope_XYZ_mm'],
                     N100_Fx100_central=central, pose_summary=result['pose_summary']), ensure_ascii=False, indent=2))
