import os, json, math, gzip, sqlite3, hashlib, platform, sys, textwrap
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.signal import find_peaks

ROOT = Path('/mnt/data/IMEB_Exo_Synthetic_Dataset_v1.0.0')
for sub in ['data/raw','data/processed','metadata','scripts','docs']:
    (ROOT/sub).mkdir(parents=True, exist_ok=True)

SEED = 7026226
rng = np.random.default_rng(SEED)
FS = 50
DT = 1/FS
CYCLE_DURATION = 6.0
SAMPLES = int(FS*CYCLE_DURATION)
N_MODELS = 24
N_CYCLES = 8
THRESHOLD_N = 45.0  # operational only, not clinical

# ---------------- metadata ----------------
heights = np.clip(rng.normal(1.72, 0.085, N_MODELS), 1.55, 1.92)
masses = np.clip(rng.normal(74, 13.5, N_MODELS) + (heights-1.72)*35, 50, 105)
shoulder_width = np.clip(0.39 + (heights-1.65)*0.12 + rng.normal(0,0.015,N_MODELS),0.35,0.50)
upper_arm_length = np.clip(0.29 + (heights-1.65)*0.10 + rng.normal(0,0.01,N_MODELS),0.26,0.36)
scale_factor = 0.55*(heights/1.72) + 0.45*(masses/74)
models = pd.DataFrame({
    'virtual_model_id':[f'VM{i:02d}' for i in range(1,N_MODELS+1)],
    'height_m':np.round(heights,4),
    'mass_kg':np.round(masses,3),
    'shoulder_width_m':np.round(shoulder_width,4),
    'upper_arm_length_m':np.round(upper_arm_length,4),
    'anthropometric_scale':np.round(scale_factor,5),
    'synthetic_record':True
})
models.to_csv(ROOT/'metadata/virtual_models.csv', index=False)

scenarios = pd.DataFrame([
    ['S1','overhead_assembly','low',0.5,320,6.0,50],
    ['S2','overhead_assembly','moderate',1.5,220,6.0,50],
    ['S3','overhead_assembly','high',3.0,140,6.0,50],
], columns=['scenario_id','task','load_level','external_load_kg','projected_cycles_per_shift','cycle_duration_s','sampling_rate_hz'])
scenarios.to_csv(ROOT/'metadata/task_scenarios.csv', index=False)

interfaces = pd.DataFrame([
    ['I1','shoulder_strap','shoulder strap',0.0055,12.0,2.5,8.0,0.13,0.15],
    ['I2','upper_arm_cuff','upper-arm cuff',0.0070,18.0,4.1,14.0,0.18,0.85],
    ['I3','thoracic_pad','thoracic pad',0.0140,10.0,2.0,6.0,0.10,2.10],
    ['I4','waist_belt','waist belt',0.0200,15.0,3.0,10.0,0.15,2.75],
], columns=['interface_id','interface_code','interface_label','effective_contact_area_m2','base_normal_N','assist_gain_N_per_Nm','dynamic_gain_N','tangential_ratio','phase_offset_rad'])
interfaces.to_csv(ROOT/'metadata/interface_definitions.csv', index=False)

device = {
    'device_type':'passive upper-limb exoskeleton surrogate',
    'maximum_assist_torque_Nm':13.0,
    'engagement_angle_deg':50.0,
    'full_assist_angle_deg':105.0,
    'assist_transfer_efficiency':0.82,
    'interface_force_operational_threshold_N':THRESHOLD_N,
    'threshold_interpretation':'Operational synthetic threshold for persistence/peak-count calculations; not a clinical or injury threshold.',
    'sampling_rate_hz':FS,
    'cycle_duration_s':CYCLE_DURATION,
    'cycles_per_scenario_condition':N_CYCLES,
    'random_seed':SEED,
}
with open(ROOT/'metadata/device_and_generation_parameters.json','w',encoding='utf-8') as f:
    json.dump(device,f,indent=2,ensure_ascii=False)

# ---------------- signal generation ----------------
time = np.arange(SAMPLES)/FS
u = time/CYCLE_DURATION
phase = 2*np.pi*u
envelope = 0.5*(1-np.cos(phase))

kin_rows=[]
int_rows=[]

for m_idx, m in models.iterrows():
    vm=m['virtual_model_id']; mass=float(m['mass_kg']); scale=float(m['anthropometric_scale'])
    for _,sc in scenarios.iterrows():
        sid=sc['scenario_id']; load=float(sc['external_load_kg'])
        for cycle in range(1,N_CYCLES+1):
            # cycle-level deterministic variability
            cv = 1.0 + rng.normal(0,0.018)
            phase_jitter = rng.normal(0,0.025)
            ph = phase + phase_jitter
            env = 0.5*(1-np.cos(ph))
            elev = 40 + 70*env + 2.0*np.sin(2*ph+0.2) + rng.normal(0,0.35,SAMPLES)
            trunk = 7.5 + 2.5*np.sin(ph+0.4) + 0.65*load + rng.normal(0,0.18,SAMPLES)
            baseline_moment = (18.5 + 6.7*load + 0.085*(mass-70)) * (0.32 + 0.68*env) * cv
            baseline_moment += 0.8*np.sin(2*ph+0.3) + rng.normal(0,0.22,SAMPLES)
            activation_base = np.clip(0.10 + 0.50*env + 0.035*load + 0.0015*(mass-70),0.03,0.92)

            for condition in ['no_exo','passive_exo']:
                if condition=='passive_exo':
                    assist_fraction=np.clip((elev-device['engagement_angle_deg'])/(device['full_assist_angle_deg']-device['engagement_angle_deg']),0,1)
                    assist_torque = device['maximum_assist_torque_Nm']*assist_fraction*(0.95+0.02*load)
                    shoulder_moment = np.maximum(0, baseline_moment - device['assist_transfer_efficiency']*assist_torque)
                    deltoid = np.clip(activation_base*(1-0.23*assist_fraction)+rng.normal(0,0.008,SAMPLES),0,1)
                    trapezius = np.clip((0.09+0.38*env+0.025*load)*(1-0.14*assist_fraction)+rng.normal(0,0.007,SAMPLES),0,1)
                    shoulder_reaction = 145 + 28*load + 1.65*shoulder_moment + 0.35*(mass-70) + rng.normal(0,1.2,SAMPLES)
                else:
                    assist_torque=np.zeros(SAMPLES)
                    shoulder_moment=baseline_moment
                    deltoid=np.clip(activation_base+rng.normal(0,0.008,SAMPLES),0,1)
                    trapezius=np.clip(0.09+0.38*env+0.025*load+rng.normal(0,0.007,SAMPLES),0,1)
                    shoulder_reaction=145 + 28*load + 1.65*shoulder_moment + 0.35*(mass-70) + rng.normal(0,1.2,SAMPLES)

                # write kinetics row per sample
                for k in range(SAMPLES):
                    kin_rows.append((vm,sid,condition,cycle,k,float(time[k]),float(u[k]),float(elev[k]),float(trunk[k]),float(assist_torque[k]),float(shoulder_moment[k]),float(shoulder_reaction[k]),float(deltoid[k]),float(trapezius[k])))

                if condition=='passive_exo':
                    for _,it in interfaces.iterrows():
                        code=it['interface_code']; area=float(it['effective_contact_area_m2'])
                        base=float(it['base_normal_N']); ag=float(it['assist_gain_N_per_Nm']); dg=float(it['dynamic_gain_N']); tr=float(it['tangential_ratio']); off=float(it['phase_offset_rad'])
                        local_noise=rng.normal(0,0.65,SAMPLES)
                        normal=(base + ag*assist_torque + dg*env + 2.5*np.sin(2*ph+off) + local_noise)
                        normal*=scale*(1+0.025*load)*cv
                        normal=np.clip(normal,0.5,None)
                        tang=tr*normal*np.sin(ph+off) + rng.normal(0,0.30,SAMPLES)
                        pressure=normal/area/1000.0
                        for k in range(SAMPLES):
                            int_rows.append((vm,sid,cycle,k,float(time[k]),float(u[k]),it['interface_id'],code,float(normal[k]),float(tang[k]),float(pressure[k]),area,True))

kin_cols=['virtual_model_id','scenario_id','condition','cycle_id','sample_id','time_s','cycle_fraction','shoulder_elevation_deg','trunk_flexion_deg','assist_torque_Nm','shoulder_joint_moment_Nm','shoulder_joint_reaction_N','deltoid_activation_norm','trapezius_activation_norm']
int_cols=['virtual_model_id','scenario_id','cycle_id','sample_id','time_s','cycle_fraction','interface_id','interface_code','normal_force_N','tangential_force_N','mean_pressure_est_kPa','effective_contact_area_m2','synthetic_record']
kin=pd.DataFrame.from_records(kin_rows,columns=kin_cols)
ints=pd.DataFrame.from_records(int_rows,columns=int_cols)

# optimize precision before writing
for c in kin.columns:
    if kin[c].dtype.kind=='f': kin[c]=kin[c].round(6)
for c in ints.columns:
    if ints[c].dtype.kind=='f': ints[c]=ints[c].round(6)

kin.to_csv(ROOT/'data/raw/kinematics_kinetics_timeseries.csv.gz', index=False, compression='gzip')
ints.to_csv(ROOT/'data/raw/interface_timeseries.csv.gz', index=False, compression='gzip')

# ---------------- derived metrics ----------------
metric_rows=[]
for (vm,sid,cycle,icode), g in ints.groupby(['virtual_model_id','scenario_id','cycle_id','interface_code'], sort=False):
    f=g['normal_force_N'].to_numpy(float)
    ft=np.abs(g['tangential_force_N'].to_numpy(float))
    p=g['mean_pressure_est_kPa'].to_numpy(float)
    t=g['time_s'].to_numpy(float)
    # smoothing for loading rate/peaks (5-point moving average)
    kernel=np.ones(5)/5
    fsmooth=np.convolve(f,kernel,mode='same')
    loading=np.gradient(fsmooth,DT)
    peaks,_=find_peaks(fsmooth,height=THRESHOLD_N,distance=int(0.55*FS),prominence=2.0)
    r=float(np.mean(f>=THRESHOLD_N))
    cyc_per_shift=int(scenarios.loc[scenarios.scenario_id==sid,'projected_cycles_per_shift'].iloc[0])
    impulse=float(np.trapz(f,t))
    metric_rows.append({
        'virtual_model_id':vm,'scenario_id':sid,'cycle_id':cycle,'interface_code':icode,
        'fmax_normal_N':float(np.max(f)),'f95_normal_N':float(np.percentile(f,95)),'fmean_normal_N':float(np.mean(f)),
        'frms_normal_N':float(np.sqrt(np.mean(f*f))),'impulse_normal_Ns':impulse,
        'tangential_abs_p95_N':float(np.percentile(ft,95)),'tangential_abs_max_N':float(np.max(ft)),
        'max_loading_rate_N_s':float(np.max(np.abs(loading))),
        'time_above_45N_pct':100*r,'high_load_peak_count':int(len(peaks)),
        'pressure_p95_kPa':float(np.percentile(p,95)),'pressure_mean_kPa':float(np.mean(p)),
        'projected_cycles_per_shift':cyc_per_shift,'projected_shift_impulse_Ns':impulse*cyc_per_shift,
        'operational_threshold_N':THRESHOLD_N
    })
cycle_metrics=pd.DataFrame(metric_rows)
for c in cycle_metrics.columns:
    if cycle_metrics[c].dtype.kind=='f': cycle_metrics[c]=cycle_metrics[c].round(6)
cycle_metrics.to_csv(ROOT/'data/processed/interface_cycle_metrics.csv',index=False)

criteria=['f95_normal_N','frms_normal_N','impulse_normal_Ns','tangential_abs_p95_N','max_loading_rate_N_s','time_above_45N_pct']
agg=cycle_metrics.groupby(['virtual_model_id','scenario_id','interface_code'],as_index=False).agg({
    'f95_normal_N':'mean','frms_normal_N':'mean','impulse_normal_Ns':'mean','tangential_abs_p95_N':'mean',
    'max_loading_rate_N_s':'mean','time_above_45N_pct':'mean','high_load_peak_count':'mean',
    'pressure_p95_kPa':'mean','pressure_mean_kPa':'mean','projected_shift_impulse_Ns':'mean'
})

# CRITIC on 288 alternatives
X=agg[criteria].to_numpy(float)
mins=X.min(axis=0); maxs=X.max(axis=0)
Z=(X-mins)/(maxs-mins)
sigma=Z.std(axis=0,ddof=1)
R=np.corrcoef(Z,rowvar=False)
C=sigma*np.sum(1-R,axis=1)
weights=C/C.sum()
V=Z*weights
A=V.max(axis=0); M=V.min(axis=0)
DA=np.sqrt(((V-A)**2).sum(axis=1)); DM=np.sqrt(((V-M)**2).sum(axis=1))
IMEB=DM/(DA+DM)
classes=np.select([IMEB<0.2,IMEB<0.4,IMEB<0.6,IMEB<0.8],[1,2,3,4],default=5)
agg['imeb_exo']=IMEB
agg['ccb_exo_class']=classes.astype(int)
agg['ccb_exo_level']=pd.Categorical(classes, categories=[1,2,3,4,5]).rename_categories({1:'Very low',2:'Low',3:'Moderate',4:'High',5:'Very high / critical'}).astype(str)
for c in agg.columns:
    if agg[c].dtype.kind=='f': agg[c]=agg[c].round(6)
agg.to_csv(ROOT/'data/processed/imeb_scores_by_model_scenario_interface.csv',index=False)

wdf=pd.DataFrame({'criterion':criteria,'critic_weight':weights,'normalized_min':mins,'normalized_max':maxs,'information_C':C,'std_normalized':sigma})
wdf=wdf.round(8)
wdf.to_csv(ROOT/'data/processed/critic_weights.csv',index=False)

# summaries
summary_interface=agg.groupby('interface_code').agg(
    n_alternatives=('imeb_exo','size'),
    imeb_mean=('imeb_exo','mean'), imeb_sd=('imeb_exo','std'), imeb_median=('imeb_exo','median'), imeb_p95=('imeb_exo',lambda x:np.percentile(x,95)),
    class_median=('ccb_exo_class','median'), class5_share=('ccb_exo_class',lambda x:np.mean(x==5)),
    f95_mean_N=('f95_normal_N','mean'), frms_mean_N=('frms_normal_N','mean'), impulse_mean_Ns=('impulse_normal_Ns','mean'),
    pressure_p95_mean_kPa=('pressure_p95_kPa','mean'), shift_impulse_mean_Ns=('projected_shift_impulse_Ns','mean')
).reset_index()
summary_interface['overall_rank']=summary_interface['imeb_mean'].rank(ascending=False,method='min').astype(int)
summary_interface=summary_interface.sort_values('overall_rank')
summary_interface=summary_interface.round(6)
summary_interface.to_csv(ROOT/'data/processed/interface_summary.csv',index=False)

summary_task=agg.groupby(['scenario_id','interface_code']).agg(n=('imeb_exo','size'),imeb_mean=('imeb_exo','mean'),imeb_sd=('imeb_exo','std'),class_median=('ccb_exo_class','median'),f95_mean_N=('f95_normal_N','mean'),impulse_mean_Ns=('impulse_normal_Ns','mean')).reset_index().round(6)
summary_task.to_csv(ROOT/'data/processed/scenario_interface_summary.csv',index=False)

# representative timeseries excerpt (VM12, S2, cycle 4) every 0.5 s
rep=ints[(ints.virtual_model_id=='VM12')&(ints.scenario_id=='S2')&(ints.cycle_id==4)].copy()
rep=rep[np.isclose((rep.time_s*2)%1,0,atol=1e-9)]
rep.to_csv(ROOT/'data/processed/representative_timeseries_excerpt.csv',index=False)

# ---------------- SQLite ----------------
db=ROOT/'imeb_exo_synthetic.sqlite'
if db.exists(): db.unlink()
con=sqlite3.connect(db)
models.to_sql('virtual_models',con,index=False)
scenarios.to_sql('task_scenarios',con,index=False)
interfaces.to_sql('interface_definitions',con,index=False)
kin.to_sql('kinematics_kinetics_timeseries',con,index=False,chunksize=50000)
ints.to_sql('interface_timeseries',con,index=False,chunksize=50000)
cycle_metrics.to_sql('interface_cycle_metrics',con,index=False,chunksize=20000)
agg.to_sql('imeb_scores',con,index=False)
wdf.to_sql('critic_weights',con,index=False)
summary_interface.to_sql('interface_summary',con,index=False)
summary_task.to_sql('scenario_interface_summary',con,index=False)
con.execute('CREATE INDEX idx_kin_vm_scenario_condition_cycle ON kinematics_kinetics_timeseries(virtual_model_id, scenario_id, condition, cycle_id)')
con.execute('CREATE INDEX idx_int_vm_scenario_cycle_interface ON interface_timeseries(virtual_model_id, scenario_id, cycle_id, interface_code)')
con.execute('CREATE INDEX idx_metrics_vm_scenario_interface ON interface_cycle_metrics(virtual_model_id, scenario_id, interface_code)')
con.commit(); con.close()

# ---------------- data dictionary ----------------
dd=[]
def add(file,field,unit,desc): dd.append([file,field,unit,desc])
for f,cols_desc in {
'virtual_models.csv':{
'virtual_model_id':'Deterministic identifier for a virtual anthropometric model.','height_m':'Synthetic standing height.','mass_kg':'Synthetic body mass.','shoulder_width_m':'Synthetic biacromial-style shoulder width surrogate.','upper_arm_length_m':'Synthetic upper-arm segment length.','anthropometric_scale':'Dimensionless scaling factor used by the signal generator.','synthetic_record':'Always true; confirms no human participant record.'},
'task_scenarios.csv':{'scenario_id':'Scenario identifier.','task':'Task label.','load_level':'Categorical synthetic load level.','external_load_kg':'External handled mass used by the surrogate generator.','projected_cycles_per_shift':'Declared operational repetition count used only for exposure projection.','cycle_duration_s':'Cycle duration.','sampling_rate_hz':'Sampling frequency.'},
'kinematics_kinetics_timeseries.csv.gz':{'virtual_model_id':'Virtual model identifier.','scenario_id':'Task scenario identifier.','condition':'no_exo or passive_exo.','cycle_id':'Repetition identifier.','sample_id':'Zero-based sample number.','time_s':'Time within cycle.','cycle_fraction':'Normalized cycle position 0-1.','shoulder_elevation_deg':'Synthetic shoulder elevation angle.','trunk_flexion_deg':'Synthetic trunk flexion angle.','assist_torque_Nm':'Synthetic passive-device assistance torque; zero in no_exo.','shoulder_joint_moment_Nm':'Synthetic shoulder joint moment surrogate.','shoulder_joint_reaction_N':'Synthetic shoulder reaction-force magnitude surrogate.','deltoid_activation_norm':'Synthetic normalized activation surrogate.','trapezius_activation_norm':'Synthetic normalized activation surrogate.'},
'interface_timeseries.csv.gz':{'virtual_model_id':'Virtual model identifier.','scenario_id':'Task scenario identifier.','cycle_id':'Repetition identifier.','sample_id':'Zero-based sample number.','time_s':'Time within cycle.','cycle_fraction':'Normalized cycle position.','interface_id':'Interface metadata identifier.','interface_code':'Machine-readable interface name.','normal_force_N':'Synthetic normal interface force.','tangential_force_N':'Synthetic signed tangential interface force.','mean_pressure_est_kPa':'Normal force divided by declared effective contact area; average pressure estimate only.','effective_contact_area_m2':'Declared effective contact area used for pressure estimate.','synthetic_record':'Always true.'},
'interface_cycle_metrics.csv':{'fmax_normal_N':'Maximum normal force per cycle.','f95_normal_N':'95th percentile normal force per cycle.','fmean_normal_N':'Mean normal force per cycle.','frms_normal_N':'RMS normal force per cycle.','impulse_normal_Ns':'Time integral of normal force over the cycle.','tangential_abs_p95_N':'95th percentile of absolute tangential force.','tangential_abs_max_N':'Maximum absolute tangential force.','max_loading_rate_N_s':'Maximum absolute loading rate from smoothed normal force.','time_above_45N_pct':'Percent of cycle at or above the 45 N operational synthetic threshold.','high_load_peak_count':'Number of smoothed peaks above 45 N using the documented peak rule.','pressure_p95_kPa':'95th percentile estimated mean pressure.','pressure_mean_kPa':'Mean estimated mean pressure.','projected_cycles_per_shift':'Scenario projection factor.','projected_shift_impulse_Ns':'Cycle impulse multiplied by projected cycles per shift.','operational_threshold_N':'Synthetic threshold used for persistence and peak count.'},
'imeb_scores_by_model_scenario_interface.csv':{'imeb_exo':'TOPSIS-derived IMEB-Exo score, 0-1, larger is more adverse relative to this synthetic dataset.','ccb_exo_class':'Operational class 1-5.','ccb_exo_level':'Text label for operational class.'}
}.items():
    for field,desc in cols_desc.items():
        unit=''
        if field.endswith('_m'): unit='m'
        elif field.endswith('_kg'): unit='kg'
        elif field.endswith('_s') and not field.endswith('_Ns'): unit='s'
        elif field.endswith('_deg'): unit='degree'
        elif field.endswith('_Nm'): unit='N·m'
        elif field.endswith('_N'): unit='N'
        elif field.endswith('_Ns'): unit='N·s'
        elif field.endswith('_N_s'): unit='N/s'
        elif field.endswith('_kPa'): unit='kPa'
        elif field.endswith('_pct'): unit='%'
        elif field.endswith('_hz'): unit='Hz'
        elif field.endswith('_m2'): unit='m²'
        add(f,field,unit,desc)
pd.DataFrame(dd,columns=['file','field','unit','description']).to_csv(ROOT/'metadata/data_dictionary.csv',index=False)

# ---------------- docs ----------------
readme=f"""# IMEB-Exo Synthetic Time-Series Dataset v1.0.0

## Purpose
This repository artifact is a **fully synthetic, deterministic dataset** created to exercise and document the IMEB-Exo computational workflow for passive upper-limb exoskeleton research. It does **not** contain measurements from human participants and must not be interpreted as empirical evidence of discomfort, injury, tissue tolerance, or commercial-device performance.

## Funding context
- Funder: FAEPEX / Universidade Estadual de Campinas (UNICAMP)
- Process: 70262-26
- Project: *Métodos Multicritério Para Análise de Dados Monitorados No Uso de Exoesqueletos Passivos de Membros Superiores*

## Dataset design
- 24 virtual anthropometric models
- 3 overhead-assembly load scenarios: 0.5, 1.5, and 3.0 kg
- 2 conditions: `no_exo` and `passive_exo`
- 8 cycles per model/scenario/condition
- 6 s per cycle at 50 Hz (300 samples/cycle)
- 4 modeled attachment interfaces in the passive-exoskeleton condition: shoulder strap, upper-arm cuff, thoracic pad, waist belt
- Random seed: `{SEED}`
- Operational elevated-load threshold: `{THRESHOLD_N:.1f} N` (synthetic analytical threshold only; **not clinical**)

## Core files
- `data/raw/kinematics_kinetics_timeseries.csv.gz`: OpenSim-compatible surrogate kinematic/kinetic time series.
- `data/raw/interface_timeseries.csv.gz`: long-format interface force and estimated mean-pressure time series.
- `data/processed/interface_cycle_metrics.csv`: per-cycle metrics used to construct multicriteria inputs.
- `data/processed/imeb_scores_by_model_scenario_interface.csv`: 288 region-level alternatives with IMEB-Exo scores/classes.
- `data/processed/critic_weights.csv`: CRITIC weights derived from the synthetic alternatives.
- `data/processed/interface_summary.csv`: overall interface ranking summary.
- `data/processed/scenario_interface_summary.csv`: ranking summary by load scenario.
- `imeb_exo_synthetic.sqlite`: SQLite copy of all major tables.
- `metadata/data_dictionary.csv`: variable definitions and units.
- `scripts/generate_synthetic_dataset.py`: reproducible generator.
- `scripts/compute_imeb_from_repository.py`: standalone recomputation of CRITIC/TOPSIS from processed metrics.

## Synthetic signal status
The time series mimic the *structure and units* expected from an OpenSim-centered workflow but are **not outputs of an executed OpenSim model**. They are deterministic surrogates generated from documented mathematical profiles plus seeded variability. Real OpenSim exports can replace these files without changing the data-processing architecture.

## IMEB-Exo criteria in v1.0.0
1. `f95_normal_N`
2. `frms_normal_N`
3. `impulse_normal_Ns`
4. `tangential_abs_p95_N`
5. `max_loading_rate_N_s`
6. `time_above_45N_pct`
`high_load_peak_count` is retained as a descriptive variable but is not part of the default v1.0.0 CRITIC/TOPSIS criterion set because discrete peak counts can dominate objective weighting in small or synthetic samples.

All selected criteria are treated as adverse/cost-type criteria. CRITIC weights are estimated across 288 alternatives (24 virtual models × 3 scenarios × 4 interfaces). TOPSIS is oriented so larger IMEB-Exo values indicate greater relative biomechanical criticality within this synthetic reference set.

## CCB-Exo operational classes
- Class 1: 0.00–<0.20 — Very low
- Class 2: 0.20–<0.40 — Low
- Class 3: 0.40–<0.60 — Moderate
- Class 4: 0.60–<0.80 — High
- Class 5: 0.80–1.00 — Very high / critical

These are operational classes only and are not validated discomfort or injury thresholds.

## DOI and citation
DOI: https://doi.org/10.5281/zenodo.22678689

## License
Suggested: CC BY 4.0 for data and MIT for code. Confirm institutional policy before deposit.
"""
(ROOT/'README.md').write_text(readme,encoding='utf-8')

readme_pt=readme.replace('# IMEB-Exo Synthetic Time-Series Dataset v1.0.0','# Base de Dados Sintética de Séries Temporais IMEB-Exo v1.0.0')
(ROOT/'README_PT.md').write_text(readme_pt,encoding='utf-8')

methodology=f"""# Synthetic-generation methodology

The repository uses deterministic surrogate equations to produce reproducible signals with the same dimensional structure expected from an OpenSim-centered upper-limb exoskeleton workflow.

- Cycle duration: {CYCLE_DURATION} s
- Sampling rate: {FS} Hz
- Samples per cycle: {SAMPLES}
- Random seed: {SEED}

A smooth overhead-work envelope is defined as `0.5*(1-cos(2*pi*t/T))`. Shoulder elevation, trunk flexion, baseline shoulder moment, device assistance, muscle-activation surrogates, and interface forces are generated from this envelope, scenario load, virtual-model scaling, and small seeded perturbations. Interface normal force depends on base preload, assistance torque transfer, movement envelope, and interface-specific gains. Tangential force is a signed fraction of normal force with interface-specific phase offset. Estimated mean pressure equals normal force divided by the declared effective contact area.

The 45 N threshold is used only to compute time-above-threshold and high-load peak counts in this synthetic demonstration. It has no clinical, physiological, or injury interpretation.
"""
(ROOT/'docs/SYNTHETIC_METHODS.md').write_text(methodology,encoding='utf-8')

citation="""cff-version: 1.2.0
message: "If you use this synthetic dataset, please cite the dataset DOI assigned by the repository and the associated IMEB-Exo article."
title: "IMEB-Exo Synthetic Time-Series Dataset for Passive Upper-Limb Exoskeleton Multicriteria Analysis"
version: 1.0.0
date-released: 2026-09-09
type: dataset
doi: 10.5281/zenodo.22678689
authors:
  - family-names: "[FAMILY NAME]"
    given-names: "[GIVEN NAMES]"
repository-code: "https://doi.org/10.5281/zenodo.22678689"
license: CC-BY-4.0
"""
(ROOT/'CITATION.cff').write_text(citation,encoding='utf-8')

metadata={
  'schema':'DataCite metadata preparation template',
    'doi':'10.5281/zenodo.22678689',
  'creators':[{'name':'[AUTHOR NAME]','nameType':'Personal'}],
  'titles':[{'title':'IMEB-Exo Synthetic Time-Series Dataset for Passive Upper-Limb Exoskeleton Multicriteria Analysis'}],
  'publisher':'[DOI-ISSUING REPOSITORY]',
  'publicationYear':2026,
  'types':{'resourceTypeGeneral':'Dataset','resourceType':'Synthetic biomechanical time-series dataset'},
  'version':'1.0.0',
  'language':'en',
  'descriptions':[{'descriptionType':'Abstract','description':'Deterministic synthetic time-series dataset for testing the IMEB-Exo CRITIC-TOPSIS pipeline for prioritizing passive upper-limb exoskeleton interface regions. Contains no human participant data.'}],
  'fundingReferences':[{'funderName':'FAEPEX / Universidade Estadual de Campinas - UNICAMP','awardNumber':'70262-26','awardTitle':'Métodos Multicritério Para Análise de Dados Monitorados No Uso de Exoesqueletos Passivos de Membros Superiores'}],
  'rightsList':[{'rights':'Creative Commons Attribution 4.0 International','rightsUri':'https://creativecommons.org/licenses/by/4.0/'}],
  'subjects':[{'subject':'exoskeletons'},{'subject':'OpenSim'},{'subject':'multicriteria decision analysis'},{'subject':'synthetic data'},{'subject':'biomechanical time series'}]
}
with open(ROOT/'metadata/datacite_metadata_template.json','w',encoding='utf-8') as f: json.dump(metadata,f,indent=2,ensure_ascii=False)

(ROOT/'LICENSE_DATA.txt').write_text('Suggested data license: Creative Commons Attribution 4.0 International (CC BY 4.0). https://creativecommons.org/licenses/by/4.0/\nConfirm institutional policy before DOI deposit.\n',encoding='utf-8')
(ROOT/'LICENSE_CODE.txt').write_text('Suggested code license: MIT License. Confirm institutional policy before DOI deposit.\n',encoding='utf-8')
(ROOT/'VERSION').write_text('1.0.0\n',encoding='utf-8')
(ROOT/'CHANGELOG.md').write_text('# Changelog\n\n## 1.0.0 — 2026-09-09\n- Initial deterministic synthetic dataset release.\n- Added raw time series, derived metrics, IMEB-Exo scores, metadata, SQLite database, and reproducibility scripts.\n',encoding='utf-8')

# standalone recomputation script
compute_script=r'''import pandas as pd
import numpy as np
from pathlib import Path
ROOT=Path(__file__).resolve().parents[1]
metrics=pd.read_csv(ROOT/'data/processed/interface_cycle_metrics.csv')
criteria=['f95_normal_N','frms_normal_N','impulse_normal_Ns','tangential_abs_p95_N','max_loading_rate_N_s','time_above_45N_pct']
agg=metrics.groupby(['virtual_model_id','scenario_id','interface_code'],as_index=False)[criteria].mean()
X=agg[criteria].to_numpy(float)
mins=X.min(0); maxs=X.max(0)
Z=(X-mins)/(maxs-mins)
sigma=Z.std(0,ddof=1); R=np.corrcoef(Z,rowvar=False)
C=sigma*np.sum(1-R,axis=1); w=C/C.sum()
V=Z*w; A=V.max(0); M=V.min(0)
DA=np.sqrt(((V-A)**2).sum(1)); DM=np.sqrt(((V-M)**2).sum(1))
agg['imeb_exo']=DM/(DA+DM)
agg['ccb_exo_class']=np.select([agg.imeb_exo<0.2,agg.imeb_exo<0.4,agg.imeb_exo<0.6,agg.imeb_exo<0.8],[1,2,3,4],default=5)
print('CRITIC weights')
print(pd.Series(w,index=criteria))
print('\nInterface summary')
print(agg.groupby('interface_code').imeb_exo.agg(['mean','std','median']).sort_values('mean',ascending=False))
'''
(ROOT/'scripts/compute_imeb_from_repository.py').write_text(compute_script,encoding='utf-8')
# copy full generator
import shutil
shutil.copy('/mnt/data/generate_imeb_dataset.py', ROOT/'scripts/generate_synthetic_dataset.py')

# requirements versions
import scipy
req=f"numpy=={np.__version__}\npandas=={pd.__version__}\nscipy=={scipy.__version__}\n"
(ROOT/'requirements.txt').write_text(req,encoding='utf-8')

# DOI insertion guide
(ROOT/'docs/DOI_DEPOSIT_CHECKLIST.md').write_text('''# DOI deposit checklist\n\nAssigned DOI: https://doi.org/10.5281/zenodo.22678689\n\n1. Review creator names, affiliations, ORCID identifiers, and institutional repository policy.\n2. Confirm the data/code licenses.\n3. Add the DOI to the manuscript Data Availability statement.\n4. Keep the DOI in `CITATION.cff`, the DataCite metadata, and README.\n5. If the repository creates a new version DOI, preserve both concept DOI and version DOI according to repository policy.\n6. Do not describe the synthetic series as empirical OpenSim output or human-subject data.\n''',encoding='utf-8')

# ---------------- manifest/checksums ----------------
# row counts
inventory=[]
for rel, rows, desc in [
('data/raw/kinematics_kinetics_timeseries.csv.gz',len(kin),'Global synthetic kinematic/kinetic time series'),
('data/raw/interface_timeseries.csv.gz',len(ints),'Synthetic interface time series for passive-exoskeleton condition'),
('data/processed/interface_cycle_metrics.csv',len(cycle_metrics),'Per-cycle derived interface metrics'),
('data/processed/imeb_scores_by_model_scenario_interface.csv',len(agg),'IMEB-Exo alternatives and classes'),
('data/processed/critic_weights.csv',len(wdf),'CRITIC criteria weights'),
('data/processed/interface_summary.csv',len(summary_interface),'Overall interface ranking summary'),
('data/processed/scenario_interface_summary.csv',len(summary_task),'Scenario-specific interface summary'),
('data/processed/representative_timeseries_excerpt.csv',len(rep),'Repository-size excerpt used to reproduce article example')]:
    p=ROOT/rel
    inventory.append({'path':rel,'rows':rows,'bytes':p.stat().st_size,'description':desc})
inv=pd.DataFrame(inventory)
inv.to_csv(ROOT/'metadata/file_inventory.csv',index=False)

# checksum all files except checksum itself
checks=[]
for p in sorted(ROOT.rglob('*')):
    if p.is_file() and p.name!='checksums.sha256':
        h=hashlib.sha256()
        with open(p,'rb') as f:
            for chunk in iter(lambda:f.read(1024*1024),b''): h.update(chunk)
        checks.append(f"{h.hexdigest()}  {p.relative_to(ROOT).as_posix()}")
(ROOT/'checksums.sha256').write_text('\n'.join(checks)+'\n',encoding='utf-8')

# machine-readable top-level summary
summary={
    'version':'1.0.0','random_seed':SEED,'virtual_models':N_MODELS,'scenarios':len(scenarios),'conditions':2,'cycles_per_scenario_condition':N_CYCLES,
    'sampling_rate_hz':FS,'cycle_duration_s':CYCLE_DURATION,'samples_per_cycle':SAMPLES,'interface_regions':interfaces.interface_code.tolist(),
    'row_counts':{'kinematics_kinetics_timeseries':len(kin),'interface_timeseries':len(ints),'interface_cycle_metrics':len(cycle_metrics),'imeb_alternatives':len(agg)},
    'critic_weights':dict(zip(criteria,[round(float(x),8) for x in weights])),
    'interface_ranking':summary_interface[['overall_rank','interface_code','imeb_mean','imeb_sd','class_median']].to_dict('records')
}
with open(ROOT/'metadata/dataset_summary.json','w',encoding='utf-8') as f: json.dump(summary,f,indent=2,ensure_ascii=False)

# zip
shutil.make_archive('/mnt/data/IMEB_Exo_Synthetic_Dataset_v1.0.0','zip',ROOT)

print('Generated',ROOT)
print('kin rows',len(kin),'interface rows',len(ints),'metrics',len(cycle_metrics),'alternatives',len(agg))
print('\nCRITIC weights')
print(wdf[['criterion','critic_weight']].to_string(index=False))
print('\nInterface summary')
print(summary_interface.to_string(index=False))
print('\nZIP MB', Path('/mnt/data/IMEB_Exo_Synthetic_Dataset_v1.0.0.zip').stat().st_size/1024/1024)
