223 lines
13 KiB
Python
223 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate deterministic C tables and read/query functions from knowledge V1."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from validate_training_knowledge import ValidationError, validate
|
|
|
|
|
|
def c(value) -> str:
|
|
if value is None:
|
|
return "NULL"
|
|
if isinstance(value, list):
|
|
value = "\n".join(value)
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
|
|
def interp(value) -> str:
|
|
if value is None:
|
|
return "{NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL}"
|
|
keys = ("family_description", "action_ids", "pattern_ids", "primary_muscle_ids",
|
|
"secondary_muscle_ids", "stabilizer_muscle_ids", "primary_zone_id",
|
|
"secondary_zone_ids", "confidence", "evidence_type", "source_refs",
|
|
"variant_notes", "role_notes", "required_confirmation")
|
|
return "{" + ",".join(c(value.get(key)) for key in keys) + "}"
|
|
|
|
|
|
def load(path: Path, key: str):
|
|
return json.loads(path.read_text(encoding="utf-8"))[key]
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 3:
|
|
raise SystemExit("usage: generate_training_knowledge.py CATALOG_DIR OUTPUT_C")
|
|
root, output = Path(sys.argv[1]), Path(sys.argv[2])
|
|
try:
|
|
validate(root)
|
|
except ValidationError as error:
|
|
raise SystemExit(str(error)) from error
|
|
refs = load(root / "science-references-v1.json", "references")
|
|
muscles = load(root / "muscles-v1.json", "muscles")
|
|
actions = load(root / "joint-actions-v1.json", "joint_actions")
|
|
patterns = load(root / "movement-patterns-v1.json", "movement_patterns")
|
|
exercises = load(root / "exercise-knowledge-v1.json", "exercises")
|
|
equipment = load(root / "equipment-knowledge-v1.json", "equipment")
|
|
lines = ['#include "trainlog/training_knowledge.h"',
|
|
'#include "trainlog/body_zone_catalog.h"', "#include <string.h>", ""]
|
|
lines.append("static const TrainlogKnowledgeReference references[] = {")
|
|
for row in refs:
|
|
lines.append(" {%s}," % ",".join((c(row["ref_id"]), c(row["title"]),
|
|
c(row["authors_or_organization"]), str(row["year"] or 0), c(row["type"]),
|
|
c(row["url"]), c(row["doi"]), c(row["pmid"]), c(row["topics"]),
|
|
c(row["notes"]), c(row["limitations"]), c(row["accessed_on"]),
|
|
c(row.get("publication_note")))))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogKnowledgeBodyZoneAudit audits[] = {")
|
|
for row in exercises:
|
|
audit = row["body_zone_audit"]
|
|
interpretation = row["interpretation"] or row["conditional_interpretation"]
|
|
fields = (row["exercise_id"], audit["status"], audit["severity"], audit["rationale"],
|
|
audit["confidence"], audit["source_refs"], audit["existing_primary_zone_id"],
|
|
audit["existing_secondary_zone_ids"],
|
|
None if interpretation is None else interpretation["primary_zone_id"],
|
|
[] if interpretation is None else interpretation["secondary_zone_ids"],
|
|
audit["proposed_mutation"])
|
|
lines.append(" {%s}," % ",".join(c(value) for value in fields))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogKnowledgeMuscle muscles[] = {")
|
|
for row in muscles:
|
|
lines.append(" {%s}," % ",".join(c(row.get(key)) for key in (
|
|
"muscle_id", "display_name", "display_name_fr", "entity_type", "anatomical_group",
|
|
"aggregate_group_id", "member_muscle_ids", "body_zone_ids", "joint_action_ids",
|
|
"primary_actions", "primary_actions_semantics", "functional_notes", "confidence", "evidence_type", "source_refs",
|
|
"overlap_warning")))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogKnowledgeJointAction actions[] = {")
|
|
for row in actions:
|
|
lines.append(" {%s}," % ",".join(c(row.get(key)) for key in (
|
|
"action_id", "display_name_fr", "definition", "anatomical_region", "joint_complex",
|
|
"principal_plane", "plane_notes", "contributing_muscle_ids", "contributor_semantics", "confidence",
|
|
"evidence_type", "source_refs", "notes")))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogKnowledgeMovementPattern patterns[] = {")
|
|
for row in patterns:
|
|
lines.append(" {%s}," % ",".join(c(row.get(key)) for key in (
|
|
"pattern_id", "display_name_fr", "definition", "parent_pattern_id", "typical_action_ids",
|
|
"typical_body_zone_ids", "body_zone_semantics", "confidence", "evidence_type", "source_refs", "notes")))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogKnowledgeInterpretation exercise_interpretations[] = {")
|
|
for row in exercises:
|
|
lines.append(" %s," % interp(row["interpretation"]))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogKnowledgeInterpretation exercise_conditionals[] = {")
|
|
for row in exercises:
|
|
lines.append(" %s," % interp(row["conditional_interpretation"]))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogExerciseKnowledge exercises[] = {")
|
|
for index, row in enumerate(exercises):
|
|
resolved = "&exercise_interpretations[%d]" % index if row["interpretation"] else "NULL"
|
|
conditional = "&exercise_conditionals[%d]" % index if row["conditional_interpretation"] else "NULL"
|
|
fields = [c(row.get(key)) for key in ("exercise_id", "exercise_name", "resolution_status",
|
|
"confidence", "equipment_ids", "identity_evidence", "identity_evidence_type", "source_refs",
|
|
"limitations", "equipment_link_status")]
|
|
lines.append(" {%s,%s,%s}," % (",".join(fields), resolved, conditional))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogEquipmentKnowledge equipment[] = {")
|
|
for row in equipment:
|
|
fields = [c(row.get(key)) for key in ("equipment_id", "manufacturer", "model", "identification_status",
|
|
"scientific_status", "scientific_status_scope", "catalog_type", "catalog_load_semantics", "mechanics", "confidence",
|
|
"evidence_type", "source_refs", "limitations", "audit_note")]
|
|
fields.append("true" if row["requires_actual_exercise"] else "false")
|
|
lines.append(" {%s}," % ",".join(fields))
|
|
lines.append("};")
|
|
capabilities = [(row["equipment_id"], cap) for row in equipment for cap in row["capabilities"]]
|
|
lines.append("static const TrainlogKnowledgeInterpretation capability_interpretations[] = {")
|
|
for _, cap in capabilities:
|
|
lines.append(" %s," % interp(cap["interpretation"]))
|
|
lines.append("};")
|
|
lines.append("static const TrainlogEquipmentCapability capabilities[] = {")
|
|
for index, (equipment_id, cap) in enumerate(capabilities):
|
|
pointer = "&capability_interpretations[%d]" % index if cap["interpretation"] else "NULL"
|
|
lines.append(" {%s,%s,%s,%s,%s,%s}," % (c(equipment_id), c(cap["display_name"]),
|
|
c(cap["exercise_ids"]), c(cap["link_status"]), c(cap["requirements"]), pointer))
|
|
lines.append("};")
|
|
lines.append(r'''
|
|
static bool list_has(const char *list, const char *id) {
|
|
size_t length;
|
|
const char *at;
|
|
if (list == NULL || id == NULL || id[0] == '\0') return false;
|
|
length = strlen(id);
|
|
at = list;
|
|
while (*at != '\0') {
|
|
const char *end = strchr(at, '\n');
|
|
size_t item_length = end == NULL ? strlen(at) : (size_t)(end - at);
|
|
if (item_length == length && memcmp(at, id, length) == 0) return true;
|
|
if (end == NULL) break;
|
|
at = end + 1;
|
|
}
|
|
return false;
|
|
}
|
|
#define DEFINE_ACCESSORS(prefix,type,array,key) \
|
|
size_t prefix##_count(void) { return sizeof(array) / sizeof(array[0]); } \
|
|
const type *prefix##_at(size_t index) { return index < prefix##_count() ? &array[index] : NULL; } \
|
|
const type *prefix##_lookup(const char *id) { size_t i; if (id == NULL || id[0] == '\0') return NULL; \
|
|
for (i=0; i<prefix##_count(); ++i) { if (strcmp(array[i].key,id)==0) return &array[i]; } \
|
|
return NULL; }
|
|
DEFINE_ACCESSORS(trainlog_knowledge_reference, TrainlogKnowledgeReference, references, ref_id)
|
|
DEFINE_ACCESSORS(trainlog_knowledge_muscle, TrainlogKnowledgeMuscle, muscles, muscle_id)
|
|
DEFINE_ACCESSORS(trainlog_knowledge_joint_action, TrainlogKnowledgeJointAction, actions, action_id)
|
|
DEFINE_ACCESSORS(trainlog_knowledge_movement_pattern, TrainlogKnowledgeMovementPattern, patterns, pattern_id)
|
|
DEFINE_ACCESSORS(trainlog_exercise_knowledge, TrainlogExerciseKnowledge, exercises, exercise_id)
|
|
DEFINE_ACCESSORS(trainlog_knowledge_body_zone_audit, TrainlogKnowledgeBodyZoneAudit, audits, exercise_id)
|
|
DEFINE_ACCESSORS(trainlog_equipment_knowledge, TrainlogEquipmentKnowledge, equipment, equipment_id)
|
|
size_t trainlog_equipment_capability_count(void) { return sizeof(capabilities)/sizeof(capabilities[0]); }
|
|
const TrainlogEquipmentCapability *trainlog_equipment_capability_at(size_t index) {
|
|
return index < trainlog_equipment_capability_count() ? &capabilities[index] : NULL;
|
|
}
|
|
const TrainlogKnowledgeInterpretation *trainlog_exercise_knowledge_conditional(const char *exercise_id) {
|
|
const TrainlogExerciseKnowledge *record = trainlog_exercise_knowledge_lookup(exercise_id);
|
|
return record == NULL ? NULL : record->conditional_interpretation;
|
|
}
|
|
static bool zone_matches(const TrainlogKnowledgeInterpretation *value, const char *zone, bool descendants) {
|
|
if (strcmp(value->primary_zone_id, zone) == 0 || list_has(value->secondary_zone_ids, zone)) return true;
|
|
if (!descendants) return false;
|
|
if (trainlog_body_zone_catalog_is_descendant(value->primary_zone_id, zone)) return true;
|
|
{ const char *at = value->secondary_zone_ids;
|
|
while (at != NULL && *at != '\0') { const char *end = strchr(at, '\n'); char id[64];
|
|
size_t n = end == NULL ? strlen(at) : (size_t)(end-at); if (n >= sizeof(id)) return false;
|
|
memcpy(id,at,n); id[n]='\0'; if (trainlog_body_zone_catalog_is_descendant(id,zone)) return true;
|
|
if (end == NULL) break;
|
|
at=end+1; }
|
|
}
|
|
return false;
|
|
}
|
|
TrainlogStatus trainlog_exercise_knowledge_query(const TrainlogKnowledgeQuery *query,
|
|
const TrainlogExerciseKnowledge **output, size_t capacity, size_t *output_count) {
|
|
size_t i, count=0; const TrainlogKnowledgeQuery empty={0};
|
|
if (output_count == NULL || (capacity > 0 && output == NULL)) return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
*output_count=0; if (query == NULL) query=∅
|
|
if (query->muscle_role < TRAINLOG_KNOWLEDGE_ROLE_ANY || query->muscle_role > TRAINLOG_KNOWLEDGE_ROLE_STABILIZER)
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
/* INVARIANT: a specific muscle role is meaningful only with a muscle ID.
|
|
* Keep the C query contract aligned with Android's paired filters. */
|
|
if (query->muscle_id == NULL && query->muscle_role != TRAINLOG_KNOWLEDGE_ROLE_ANY)
|
|
return TRAINLOG_STATUS_INVALID_ARGUMENT;
|
|
if (query->scientific_zone_id != NULL && trainlog_body_zone_catalog_lookup(query->scientific_zone_id) == NULL)
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
if (query->movement_pattern_id != NULL && trainlog_knowledge_movement_pattern_lookup(query->movement_pattern_id) == NULL)
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
if (query->muscle_id != NULL && trainlog_knowledge_muscle_lookup(query->muscle_id) == NULL)
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
if (query->available_equipment_id != NULL && trainlog_equipment_knowledge_lookup(query->available_equipment_id) == NULL)
|
|
return TRAINLOG_STATUS_NOT_FOUND;
|
|
for (i=0; i<trainlog_exercise_knowledge_count(); ++i) { const TrainlogExerciseKnowledge *e=&exercises[i];
|
|
const TrainlogKnowledgeInterpretation *v=e->interpretation; bool muscle_ok=true;
|
|
if (v == NULL) continue;
|
|
if (query->scientific_zone_id != NULL && !zone_matches(v,query->scientific_zone_id,query->include_zone_descendants)) continue;
|
|
if (query->movement_pattern_id != NULL && !list_has(v->pattern_ids,query->movement_pattern_id)) continue;
|
|
if (query->available_equipment_id != NULL && !list_has(e->equipment_ids,query->available_equipment_id)) continue;
|
|
if (query->muscle_id != NULL) {
|
|
switch (query->muscle_role) {
|
|
case TRAINLOG_KNOWLEDGE_ROLE_PRIMARY: muscle_ok=list_has(v->primary_muscle_ids,query->muscle_id); break;
|
|
case TRAINLOG_KNOWLEDGE_ROLE_SECONDARY: muscle_ok=list_has(v->secondary_muscle_ids,query->muscle_id); break;
|
|
case TRAINLOG_KNOWLEDGE_ROLE_STABILIZER: muscle_ok=list_has(v->stabilizer_muscle_ids,query->muscle_id); break;
|
|
case TRAINLOG_KNOWLEDGE_ROLE_ANY: muscle_ok=list_has(v->primary_muscle_ids,query->muscle_id) ||
|
|
list_has(v->secondary_muscle_ids,query->muscle_id) || list_has(v->stabilizer_muscle_ids,query->muscle_id); break;
|
|
}
|
|
}
|
|
if (!muscle_ok) continue;
|
|
if (count < capacity) output[count]=e;
|
|
++count;
|
|
}
|
|
*output_count=count; return count > capacity ? TRAINLOG_STATUS_INVALID_ARGUMENT : TRAINLOG_STATUS_OK;
|
|
}
|
|
''')
|
|
output.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|