Claude Media
Claude Codeでクロソイドの線形計算を書き、道路構造令の値で検算する

Claude Codeでクロソイドの線形計算を書き、道路構造令の値で検算する

IP座標とパラメータAから、クロソイド・円曲線の主要点と中間点を出すスクリプトをClaude Codeに書かせ、独立した数値積分とテストで検算します。

道路の平面線形は、直線・クロソイド(緩和曲線)・円曲線・クロソイド・直線の順につながります。この並びの主要点と中間点の座標を出す計算は、式そのものは古典的ですが、符号と向きを一箇所間違えるだけで座標が数百メートルずれます。Claude Codeに書かせるときは、コードの見た目ではなく「ずれたら必ず落ちるテスト」を先に置くのが要点です。

この記事では、IP(交点)の座標と曲線半径R、クロソイドパラメータAから、TS・SC・CS・STの座標と任意の追加距離の中心線座標を出すPythonスクリプトを題材にします。標準ライブラリだけで動き、検算は独立した数値積分と幾何学的な閉合条件で行います。設計値が道路構造令の下限を満たすかは、令の表をそのまま引いて確かめます。

何を計算するか — 用語と式を先に固定する

左右対称で、前後の緩和曲線の長さが等しい基本形を扱います。記号は次のとおりです。

記号意味この記事の例
IP意味2本の直線が交わる点この記事の例(1000, 0)
I意味交角(進行方向の変化量)この記事の例約39.81度
R意味円曲線の半径この記事の例300 m
A意味クロソイドパラメータ。A²=R×Lsこの記事の例√30000 ≒ 173.2
Ls意味クロソイド(緩和区間)の長さこの記事の例100 m
τ意味クロソイド終点の接線角。Ls/(2R)この記事の例0.16667 rad
TS / SC / CS / ST意味直線→クロソイド / クロソイド→円 / 円→クロソイド / クロソイド→直線この記事の例—

クロソイド上の距離Lの点は、TSを原点、TSでの接線方向をx軸とした局所座標で次の級数になります。

  • x = L − L⁵/(40A⁴) + L⁹/(3456A⁸) − L¹³/(599040A¹²)
  • y = L³/(6A²) − L⁷/(336A⁶) + L¹¹/(42240A¹⁰) − L¹⁵/(9676800A¹⁴)

円曲線の接線長TLと円曲線長Lcは、終点座標(xs, ys)から次の順に出します。

  1. 移程量 ΔR = ys − R(1 − cosτ)
  2. 接線のずれXm = xs − R sinτ
  3. TL = (R + ΔR) tan(I/2) + Xm
  4. Lc = R(I − 2τ)

ここまでが公式の部分で、Claudeに「クロソイドの計算を書いて」と頼めば、たいていこの形のコードが返ってきます。問題は次の節です。

手順1 — 検算の物差しを先に決める

実際に書かせる前に、コードが出す値を何と突き合わせるかを決めます。線形計算書(設計者が作る既存の計算結果)がある場合は、その値がいちばん強い物差しです。手元に無いときでも、次の4種類は計算書なしで置けます。

検算何を確かめるか外れたときに疑うところ
級数と数値積分の一致何を確かめるかクロソイドの式の係数・符号外れたときに疑うところ級数の項、A²=R×Lsの取り違え
STが「IPからTL」の位置にある何を確かめるかTLの式、交角の符号外れたときに疑うところ左折・右折の符号処理
SCとCSが半径Rの円の上にある何を確かめるか円の中心の求め方外れたときに疑うところ法線方向の向き
円曲線を歩いてCSに着く何を確かめるか曲線長Lcと回転方向外れたときに疑うところ時計回り・反時計回りの取り違え

数値積分は、級数を使わずに接線角L²/(2A²) をシンプソン則で積分してx, yを出します。式の係数を写し間違えても、積分側は別の経路なので同じ間違いを共有しません。

手順2 — CLAUDE.mdに検算のルールを書く

Claude Codeはプロジェクトルートの CLAUDE.md をセッションの開始時に読みます。コーディング規約や、レビューのチェックリストを書く場所です。線形計算では、座標系と単位の約束をここに置くと、セッションをまたいでも同じ規約で書かれます。

# 線形計算のルール
- 座標は x=東, y=北、角度の内部表現はラジアン、長さはメートル。
- 測量座標(X=北, Y=東)との変換は入出力の境界だけで行う。
- 左折・右折を符号 sgn(+1/-1)で一本化し、右折は左折の鏡像として扱う。
- 変更のたびに `python3 -m unittest` を実行し、全件通るまで完了としない。
- 級数と数値積分の差が 1e-6 m を超えたら、式ではなく係数の写し間違いを疑う。
- 道路構造令の値(第15条・第18条)は DESIGN_LIMITS に集約し、コードの途中に数字を直書きしない。

テストの実行を「完了条件」として書くのがポイントです。Claude Codeの公式の概要ページにも、claude "write tests for the auth module, run them, and fix any failures" のように、テストを書かせて実行させ、失敗を直させる使い方が載っています。線形計算では、書かせるテストの中身を人が決めておくことで、この反復が検算として働きます。CLAUDE.mdが長すぎて途中から読まれない問題はCLAUDE.md is too long警告の意味と直し方で扱っています。

手順3 — スクリプトを書かせる

Claude Codeへの依頼文は、入力・出力・禁止事項を具体的にします。

alignment.py を作って。入力は IP0→IP1→IP2 の座標、半径 R、パラメータ A。
標準ライブラリだけで、TS・SC・CS・ST の座標、TL、Lc、Ls と、
TS からの追加距離 s の中心線座標を返す関数を書いて。
test_alignment.py に、級数と Simpson 積分の一致、ST が IP から TL の位置にあること、
SC/CS が半径 R の円上にあること、右折が左折の鏡像になることのテストを書いて。
まずテストだけ書いて、実装はその後に。

「テストを先に」と頼むのは、実装に合わせて期待値を作る事故を避けるためです。出てきた実装は次のようになります(この記事の例題を動かしたものです)。

import math
from dataclasses import dataclass
 
# 道路構造令 第15条(曲線半径)・第18条(緩和区間長)の表
# 設計速度 km/h -> (曲線半径 m 以上, 緩和区間長 m 以上)
DESIGN_LIMITS = {120: (710, 100), 100: (460, 85), 80: (280, 70), 60: (150, 50),
                 50: (100, 40), 40: (60, 35), 30: (30, 25), 20: (15, 20)}
 
 
def spiral_local(length, a):
    """TS を原点、接線方向を x 軸とする局所座標(級数 4 項)。"""
    l, a2 = length, a * a
    x = l - l**5/(40*a2**2) + l**9/(3456*a2**4) - l**13/(599040*a2**6)
    y = l**3/(6*a2) - l**7/(336*a2**3) + l**11/(42240*a2**5) - l**15/(9676800*a2**7)
    return x, y
 
 
def _place(origin, heading, lx, ly):
    c, s = math.cos(heading), math.sin(heading)
    return (origin[0] + lx*c - ly*s, origin[1] + lx*s + ly*c)
 
 
@dataclass
class Alignment:
    ts: tuple; sc: tuple; cs: tuple; st: tuple
    tl: float; ls: float; lc: float; delta: float
    d1: float; d2: float; sgn: int; radius: float; a: float
    center: tuple
 
    @property
    def total_length(self):
        return 2*self.ls + self.lc
 
    def point(self, s):
        """TS からの追加距離 s の中心線座標。"""
        ls, lc, r = self.ls, self.lc, self.radius
        if s <= ls:
            lx, ly = spiral_local(s, self.a)
            return _place(self.ts, self.d1, lx, self.sgn*ly)
        if s <= ls + lc:
            ang0 = math.atan2(self.sc[1]-self.center[1], self.sc[0]-self.center[0])
            ang = ang0 + self.sgn*(s - ls)/r
            return (self.center[0] + r*math.cos(ang), self.center[1] + r*math.sin(ang))
        lx, ly = spiral_local(self.total_length - s, self.a)
        # ST から逆向きに歩くと旋回方向が反転する
        return _place(self.st, self.d2 + math.pi, lx, -self.sgn*ly)
 
 
def solve(ip0, ip1, ip2, radius, a):
    ls = a*a/radius
    d1 = math.atan2(ip1[1]-ip0[1], ip1[0]-ip0[0])
    d2 = math.atan2(ip2[1]-ip1[1], ip2[0]-ip1[0])
    delta = (d2 - d1 + math.pi) % (2*math.pi) - math.pi
    sgn = 1 if delta > 0 else -1
    i = abs(delta)
    tau = ls/(2*radius)
    xs, ys = spiral_local(ls, a)
    dr = ys - radius*(1 - math.cos(tau))
    xm = xs - radius*math.sin(tau)
    tl = (radius + dr)*math.tan(i/2) + xm
    lc = radius*(i - 2*tau)
    if lc < 0:
        raise ValueError("交角に対して R と A が大きすぎる(円曲線長が負)")
    ts = (ip1[0] - tl*math.cos(d1), ip1[1] - tl*math.sin(d1))
    st = (ip1[0] + tl*math.cos(d2), ip1[1] + tl*math.sin(d2))
    sc = _place(ts, d1, xs, sgn*ys)
    cs = _place(st, d2 + math.pi, xs, -sgn*ys)
    h = d1 + sgn*tau
    center = (sc[0] - sgn*radius*math.sin(h), sc[1] + sgn*radius*math.cos(h))
    return Alignment(ts, sc, cs, st, tl, ls, lc, delta, d1, d2, sgn, radius, a, center)
 
 
def check_design_speed(radius, ls, speed):
    rmin, lmin = DESIGN_LIMITS[speed]
    return radius >= rmin and ls >= lmin

手順4 — テストが最初の間違いを捕まえる

この記事の作業で、最初に書いた実装は円曲線の区間を時計回りに歩いていました。左折の線形なのに、円周上の角度を減らす向きに進めていたためです。書いたテスト7件のうち2件(test_arc_endpoints_continuous と test_cs_matches_circle_walk)が落ち、出力は次のとおりでした。

AssertionError: 832.8297414353683 != 1042.043242878848 within 6 places (209.2135014434798 difference)

円曲線の終点に着くはずの座標が約209 m離れています。他の5件は通っていました。TSやSTの座標は、円周を歩く処理を通らないためです。見た目にはもっともらしい座標列が出るので、目視では気づけません。回転方向の符号を直した後は、7件すべてが通りました。

python3 -m unittest -v
test_arc_endpoints_continuous ... ok
test_cs_matches_circle_walk ... ok
test_design_speed_60 ... ok
test_right_turn_is_mirror ... ok
test_sc_cs_on_circle ... ok
test_series_matches_integration ... ok
test_tangent_points_lie_on_tangent_lines ... ok
 
Ran 7 tests in 0.003s
OK

Claudeに「落ちたテストを直して」と戻すときは、失敗メッセージをそのまま渡すと、どの検算が破れたかが伝わります。ただし「テストを通すためにテストの許容値を緩める」直し方が出ることがあるので、テストファイルは変更しないように依頼文に一行足しておくと安全です。

手順5 — 例題の主要点を出す

IP0=(0, 0)、IP1=(1000, 0)、IP2=(1600, 500)、R=300 m、A²=30000(Ls=100 m)の左折です。出力は次のとおりです。

量値
交角I値39.8056度
接線長TL値159.071 m
円曲線長Lc値108.421 m
全長(2Ls+Lc)値308.421 m
TS値(840.929, 0.000)
SC値(940.652, 5.545)
CS値(1042.043, 42.253)
ST値(1122.202, 101.835)
円の中心値(890.883, 301.388)

SCの局所座標はxs=99.7226、ys=5.5445です。手で検算すると、ysの第1項Ls²/(6R)=10000/1800=5.5556から第2項の約0.011を引いて5.5445になります。接線角τ=100/600=0.16667 radも、表の値と合います。

TSからの追加距離sで中間点を引くと、次のようになります。

追加距離s区間座標
50区間クロソイド座標(890.920, 0.694)
100区間SC座標(940.652, 5.545)
150区間円曲線座標(989.041, 17.900)
200区間円曲線座標(1034.710, 38.113)
258.421区間CS座標(1083.353, 70.365)

最後に、STの直前10 mmの区間の向きを数値で出すと39.80557106度で、直線の向きd2=39.80557109度と小数第7位まで一致します。これは「円→クロソイド→直線の接線がつながっている」ことの独立した確認で、テストの外から見たもう一つの閉合条件になります。

道路構造令の値で設計値を検算する

設計値が令の下限を満たしているかは、第15条と第18条の表で確かめます。令は設計速度ごとに、曲線半径の下限と緩和区間長の下限を定めています。

設計速度(km/h)曲線半径(m以上)・原則特例で縮小できる値緩和区間長(m以上)
120曲線半径(m以上)・原則710特例で縮小できる値570緩和区間長(m以上)100
100曲線半径(m以上)・原則460特例で縮小できる値380緩和区間長(m以上)85
80曲線半径(m以上)・原則280特例で縮小できる値230緩和区間長(m以上)70
60曲線半径(m以上)・原則150特例で縮小できる値120緩和区間長(m以上)50
50曲線半径(m以上)・原則100特例で縮小できる値80緩和区間長(m以上)40
40曲線半径(m以上)・原則60特例で縮小できる値50緩和区間長(m以上)35
30曲線半径(m以上)・原則30特例で縮小できる値—緩和区間長(m以上)25
20曲線半径(m以上)・原則15特例で縮小できる値—緩和区間長(m以上)20

例題のR=300 m、Ls=100 mは、設計速度60 km/hの原則値(150 m、50 m)を満たします。R=120 mだと原則値は割りますが、第15条ただし書の「地形の状況その他の特別の理由によりやむを得ない箇所」の縮小値120 mには届きます。R=100 mは縮小値にも届きません。check_design_speed は原則値だけを見るので、R=120には False を返します。縮小値を使うかは設計者の判断で決まるため、関数には入れていません。

注意点が3つあります。第一に、令の本文に「クロソイド」の語は出てきません。令が定めるのは緩和区間の長さの下限までで、A²=R×Lsという式やパラメータAの許容範囲は、令の外にある設計の手引きの領分です。コードにAの範囲チェックを入れるなら、その手引きの値を使います。第二に、第18条第3項のとおり、片勾配のすりつけや拡幅に必要な長さが表の値を超えるときは、その長さが下限になります。表の値を満たしただけでは足りない場合があります。第三に、表の曲線半径は「車道の中心線の曲線半径」です。歩道側のオフセット線形にRを流用するときは、半径が変わります。

座標系と適用範囲の落とし穴

このスクリプトが前提にしている条件を挙げておきます。

  • 座標軸: x=東、y=北です。測量座標(X=北、Y=東)の計算書と突き合わせるときは、入出力の境界でxとyを入れ替えます。内部で混ぜると、符号の検算が効かなくなります。
  • 左右対称の基本形: 前後のクロソイドが同じAで、円曲線を挟む形だけです。卵形(クロソイド同士が直接つながる形)や、前後でAが異なる非対称形は別の式が要ります。
  • 平面直角座標系の補正: 縮尺係数や子午線収差は扱いません。国の座標系で計算する場合の補正は、このスクリプトの外です。
  • 級数の打ち切り: 4項までなので、LがAに比べて大きい形では精度が落ちる可能性があります。今回の例題はL/A=0.577です。

最後の点は、数値積分との差をテストで見ているので、範囲を超えれば落ちます。Claudeに係数を増やす修正を頼む前に、テストの許容値を確かめる順にすると、間違った直し方を避けられます。

既存の計算書があるときの組み込み方

発注者の線形計算書や、既存のCADの線形データがあるときは、主要点の座標を固定値として別ファイルに貼り、次のように比較する構成にすると、検算が回帰テストになります。

EXPECTED = {  # 線形計算書の主要点(単位 m)。値は自分の計算書のものに置き換える
    "TS": (840.929, 0.000),
    "SC": (940.652, 5.545),
}
 
def test_against_report(self):
    for name, (x, y) in EXPECTED.items():
        px, py = getattr(self.c, name.lower())
        self.assertAlmostEqual(px, x, places=3)
        self.assertAlmostEqual(py, y, places=3)

計算書の桁数に合わせて places を決めます。計算書が小数第3位(mm単位)までなら places=3 です。第3位で一致しないときは、まず計算書側の前提(丸めのタイミング、IPの座標の桁、Aの与え方)を疑います。そのうえで、同じテスト群が通る範囲で式を見直します。

まとめ

線形計算をClaude Codeに任せるときは、式を書かせる前に、閉合条件(STがIPからTLの位置にある、円曲線を歩いてCSに着く、級数と数値積分が一致する)をテストとして先に置きます。この例題では、円曲線の回転方向の符号ミスが、見た目はもっともらしい座標列のまま209 mのずれになり、7件中2件のテストだけが落ちました。TLやTSが合っていても、円周側の処理が間違っていることはあります。

令の表は、R・Lsが下限を満たすかの確認に使います。クロソイドの式やパラメータAの許容範囲は令に書かれていないので、設計の手引きの値と、手元の計算書の値を最終的な物差しにします。同じ型の「先に期待値、後に実装」の進め方は、片持ち梁をCalculiXで手計算と突き合わせる記事や、MATLABからPythonへ移植して数値の一致を確かめる記事でも使っています。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/overview
  2. laws.e-gov.go.jp/law/345CO0000000320
この記事を共有:XはてブLinkedIn