6767
6868 mkdir -p _site
6969 touch _site/.nojekyll
70+ : > _site/.docs_routes.txt
7071
7172 mapfile -t DOCS < <(find . -maxdepth 4 -type d -name docs | sort)
7273
7980 echo "==> Copying $ROUTE"
8081 mkdir -p "$DEST"
8182 cp -R "$D/_build/html/"* "$DEST/"
83+ printf '%s\n' "$ROUTE" >> _site/.docs_routes.txt
8284 else
8385 echo "==> No built docs for $ROUTE"
8486 fi
@@ -92,49 +94,177 @@ jobs:
9294 from pathlib import Path
9395
9496 site = Path("_site")
95-
96- def discover_routes() -> list[str]:
97- """Discover built Sphinx sites from the assembled _site folder."""
97+ manifest = site / ".docs_routes.txt"
98+
99+ def read_routes() -> list[str]:
100+ """Read only the package routes copied by the workflow.
101+
102+ Do not discover routes with ``_site.rglob('index.html')``. Sphinx
103+ builds many internal pages such as ``modules/index.html`` and
104+ ``_sources``/``_static`` support files. Treating every nested
105+ ``index.html`` as a package creates duplicate cards such as
106+ "Modules". The manifest contains only the real package doc roots.
107+ """
108+ if not manifest.exists():
109+ return []
98110 routes: list[str] = []
99- for index in sorted(site.rglob("index.html")):
100- if index == site / "index.html":
111+ for line in manifest.read_text(encoding="utf-8").splitlines():
112+ route = line.strip().strip("/")
113+ if not route or route.startswith("_"):
101114 continue
102- rel = index.parent.relative_to(site).as_posix()
103- if rel and not rel.startswith("_"):
104- routes.append(rel)
115+ if (site / route / "index.html").exists():
116+ routes.append(route)
105117 return sorted(set(routes))
106118
107- routes = discover_routes()
119+ routes = read_routes()
120+
121+ DISPLAY = {
122+ "introduction": (
123+ "IN",
124+ "Introduction",
125+ "Introductory examples, helper utilities, and foundational control-system workflows.",
126+ ),
127+ "control_systems": (
128+ "CS",
129+ "Control Systems",
130+ "Core control-system analysis, modeling, and command-line engineering workflows.",
131+ ),
132+ "fluid_systems": (
133+ "FL",
134+ "Fluid Systems",
135+ "Fluid-system modeling utilities and package-level API documentation.",
136+ ),
137+ "frequency_response": (
138+ "FR",
139+ "Frequency Response",
140+ "Frequency-domain analysis, plotting, margins, and stability-oriented design documentation.",
141+ ),
142+ "mechanical_systems": (
143+ "MS",
144+ "Mechanical Systems",
145+ "Mechanical-system modeling utilities and package-level API documentation.",
146+ ),
147+ "pid_controllers": (
148+ "PID",
149+ "PID Controllers",
150+ "PID controller workflows, tuning utilities, and closed-loop response documentation.",
151+ ),
152+ "root_locus_analysis": (
153+ "RL",
154+ "Root Locus Analysis",
155+ "Root-locus analysis, gain studies, and classical control design documentation.",
156+ ),
157+ "state_space_analysis": (
158+ "SA",
159+ "State Space Analysis",
160+ "State-space modeling, simulation, response analysis, and reproducible CLI workflows.",
161+ ),
162+ "state_space_design": (
163+ "SD",
164+ "State Space Design",
165+ "State-space design, controller synthesis, observers, and related workflows.",
166+ ),
167+ "transient_analysis": (
168+ "TR",
169+ "Transient Analysis",
170+ "Time-domain response, transient metrics, stability tests, and simulation-oriented workflows.",
171+ ),
172+ }
173+
174+ TOP_ORDER = [
175+ "introduction",
176+ "control_systems",
177+ "transient_analysis",
178+ "frequency_response",
179+ "root_locus_analysis",
180+ "state_space_analysis",
181+ "state_space_design",
182+ "pid_controllers",
183+ "mechanical_systems",
184+ "fluid_systems",
185+ ]
186+
187+ TOOL_DISPLAY = {
188+ "bodeTool": ("B", "Bode Tool", "Bode magnitude/phase plotting and frequency-response workflows."),
189+ "nyquistTool": ("N", "Nyquist Tool", "Nyquist analysis and stability-oriented frequency-response workflows."),
190+ "routhTool": ("R", "Routh Tool", "Routh-Hurwitz tabulation, stability checks, and symbolic/numeric workflows."),
191+ "hurwitzTool": ("H", "Hurwitz Tool", "Hurwitz matrices, polynomial stability tests, and supporting utilities."),
192+ "rootLocusTool": ("RL", "Root Locus Tool", "Root-locus plots, gain studies, and classical design workflows."),
193+ "stateTool": ("ST", "State Tool", "State-space calculations, matrix workflows, and CLI utilities."),
194+ "stateRespTool": ("SR", "State Response Tool", "State-response simulation, transition matrices, and output analysis."),
195+ "stateSolnTool": ("SS", "State Solution Tool", "State-transition and solution workflows for state-space models."),
196+ "stateTransTool": ("Φ", "State Transformation Tool", "Coordinate transformations and state-space canonical-form workflows."),
197+ "pidTool": ("PID", "PID Tool", "PID controller tuning, evaluation, and closed-loop response workflows."),
198+ "transientTool": ("tr", "Transient Tool", "Step, impulse, and time-domain response metrics."),
199+ "steadyStateTool": ("ss", "Steady-State Tool", "Steady-state error, system type, and tracking-performance calculations."),
200+ "transferFunctionTool": ("G", "Transfer Function Tool", "Transfer-function manipulation, poles, zeros, and response utilities."),
201+ "secondOrderTool": ("ζ", "Second-Order Tool", "Canonical second-order response metrics and design relationships."),
202+ "blockDiagramTool": ("Σ", "Block Diagram Tool", "Block-diagram reduction and signal-flow style analysis utilities."),
203+ "compensatorTool": ("C", "Compensator Tool", "Lead, lag, and lead-lag design workflows for classical control."),
204+ "modelingTool": ("M", "Modeling Tool", "Modeling utilities and system representation workflows."),
205+ }
108206
109207 def titleize_token(token: str) -> str:
110- acronyms = {"api", "cli", "pid", "lqr", "mimo", "siso"}
208+ acronyms = {"api", "cli", "pid", "lqr", "mimo", "siso", "ode", "tf" }
111209 lower = token.lower()
112210 if lower in acronyms:
113211 return lower.upper()
114212 return token[:1].upper() + token[1:]
115213
214+ def split_camel(name: str) -> str:
215+ out = []
216+ prev = ""
217+ for ch in name:
218+ if prev and ch.isupper() and (prev.islower() or prev.isdigit()):
219+ out.append(" ")
220+ out.append(ch)
221+ prev = ch
222+ return "".join(out)
223+
224+ def group_key(route: str) -> str:
225+ return route.split("/", 1)[0]
226+
227+ def leaf_name(route: str) -> str:
228+ return route.split("/")[-1]
229+
116230 def titleize(route: str) -> str:
117- """Create a readable title from the actual route path."""
118- name = route.split("/")[-1]
119- cleaned = name.replace("_", " ").replace("-", " ")
120- return " ".join(titleize_token(part) for part in cleaned.split()) or route
231+ top = group_key(route)
232+ leaf = leaf_name(route)
233+ if route in DISPLAY:
234+ return DISPLAY[route][1]
235+ if leaf in TOOL_DISPLAY:
236+ return TOOL_DISPLAY[leaf][1]
237+ name = split_camel(leaf).replace("_", " ").replace("-", " ")
238+ return " ".join(titleize_token(part) for part in name.split()) or route
121239
122240 def group_title(group: str) -> str:
241+ if group in DISPLAY:
242+ return DISPLAY[group][1]
123243 cleaned = group.replace("_", " ").replace("-", " ")
124244 return " ".join(titleize_token(part) for part in cleaned.split()) or group
125245
126246 def icon_for(route: str, idx: int) -> str:
247+ if route in DISPLAY:
248+ return DISPLAY[route][0]
249+ leaf = leaf_name(route)
250+ if leaf in TOOL_DISPLAY:
251+ return TOOL_DISPLAY[leaf][0]
127252 title = titleize(route)
128253 words = title.split()
129- if not words:
130- return str(idx + 1)
131254 if len(words) >= 2:
132255 return (words[0][0] + words[1][0]).upper()
133- word = words[0]
134- return word[:3].upper() if len(word) <= 3 else word[:2].upper()
256+ if words:
257+ return words[0][:3].upper() if len(words[0]) <= 3 else words[0][:2].upper()
258+ return str(idx + 1)
135259
136260 def desc_for(route: str) -> str:
137- """Generate useful text without depending on stale package names."""
261+ if route in DISPLAY:
262+ return DISPLAY[route][2]
263+ leaf = leaf_name(route)
264+ if leaf in TOOL_DISPLAY:
265+ return TOOL_DISPLAY[leaf][2]
266+
267+ top = group_key(route)
138268 title = titleize(route)
139269 low = route.lower()
140270 if "root" in low and "locus" in low:
@@ -147,40 +277,22 @@ jobs:
147277 return "PID controller workflows, tuning utilities, and closed-loop response documentation."
148278 if "transient" in low:
149279 return "Time-domain response, transient metrics, and simulation-oriented workflows."
150- if "fluid" in low:
151- return "Fluid-systems modeling utilities and package-level API documentation."
152- if "mechanical" in low:
153- return "Mechanical-systems modeling utilities and package-level API documentation."
154- if "introduction" in low:
155- return "Introductory examples, helper utilities, and foundational control-system workflows."
156- if "control" in low:
157- return "Control-system analysis, design utilities, and command-line engineering workflows."
280+ if top in DISPLAY:
281+ return DISPLAY[top][2]
158282 return f"Package-level API documentation and reproducible CLI workflows for {title}."
159283
160284 def route_sort_key(route: str) -> tuple[int, str]:
161- preferred = [
162- "introduction",
163- "control_systems",
164- "transient_analysis",
165- "frequency_response",
166- "root_locus_analysis",
167- "state_space_analysis",
168- "state_space_design",
169- "pid_controllers",
170- "mechanical_systems",
171- "fluid_systems",
172- ]
173- top = route.split("/", 1)[0]
285+ top = group_key(route)
174286 try:
175- rank = preferred .index(top)
287+ rank = TOP_ORDER .index(top)
176288 except ValueError:
177- rank = len(preferred )
289+ rank = len(TOP_ORDER )
178290 return rank, route
179291
180292 routes = sorted(routes, key=route_sort_key)
181293 grouped: dict[str, list[str]] = defaultdict(list)
182294 for route in routes:
183- grouped[route.split("/", 1)[0] ].append(route)
295+ grouped[group_key(route) ].append(route)
184296
185297 package_count = len(routes)
186298 first_link = f"{routes[0]}/" if routes else "#"
0 commit comments