sphere-cli 0.1.40 → 0.1.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sphere.js +258 -23
- package/package.json +5 -2
- package/scripts/certify.py +131 -0
- package/scripts/evaluate.py +70 -0
- package/sphere_cli/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/__init__.cpython-314.pyc +0 -0
- package/sphere_cli/__pycache__/_algo._givens_inplace_nb-84.py314.1.nbc +0 -0
- package/sphere_cli/__pycache__/_algo._givens_inplace_nb-84.py314.nbi +0 -0
- package/sphere_cli/__pycache__/_algo.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_core.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_evaluate.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_evaluate.cpython-314.pyc +0 -0
- package/sphere_cli/__pycache__/_generate.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_generate.cpython-314.pyc +0 -0
- package/sphere_cli/__pycache__/cli.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/cli.cpython-314.pyc +0 -0
- package/sphere_cli/_algo.cpython-314-darwin.so +0 -0
- package/sphere_cli/_certify.cpython-314-darwin.so +0 -0
- package/sphere_cli/_core.cpython-314-darwin.so +0 -0
- package/sphere_cli/_evaluate.cpython-314-darwin.so +0 -0
- package/sphere_cli/_generate.cpython-314-darwin.so +0 -0
- package/sphere_cli/cli.cpython-314-darwin.so +0 -0
package/bin/sphere.js
CHANGED
|
@@ -4,15 +4,12 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* sphere — CLI entry point.
|
|
6
6
|
*
|
|
7
|
-
* This wrapper handles argument parsing, progress display, and license checks,
|
|
8
|
-
* then delegates generation to sphere-node.jsc (V8 bytecode, bytenode-loaded).
|
|
9
|
-
*
|
|
10
7
|
* Commands:
|
|
11
8
|
* sphere generate <input.csv> -o <output.csv> [options]
|
|
12
|
-
* sphere
|
|
9
|
+
* sphere evaluate <real.csv> <synth.csv> [options]
|
|
10
|
+
* sphere certify <real.csv> <synth.csv> -o <cert.html> [options]
|
|
13
11
|
* sphere demo
|
|
14
|
-
* sphere
|
|
15
|
-
* sphere certify — not available in this version (requires Python sidecar)
|
|
12
|
+
* sphere license activate|status|clear
|
|
16
13
|
*/
|
|
17
14
|
|
|
18
15
|
const { spawn } = require('child_process');
|
|
@@ -23,6 +20,8 @@ const os = require('os');
|
|
|
23
20
|
|
|
24
21
|
const PKG_DIR = path.join(__dirname, '..');
|
|
25
22
|
const SPHERE_NODE_JS = path.join(PKG_DIR, 'sphere-node.js');
|
|
23
|
+
const EVALUATE_PY = path.join(PKG_DIR, 'scripts', 'evaluate.py');
|
|
24
|
+
const CERTIFY_PY = path.join(PKG_DIR, 'scripts', 'certify.py');
|
|
26
25
|
const VERSION = require(path.join(PKG_DIR, 'package.json')).version;
|
|
27
26
|
|
|
28
27
|
// ── License ───────────────────────────────────────────────────────────────────
|
|
@@ -150,6 +149,132 @@ function clearLine() {
|
|
|
150
149
|
process.stderr.write(`\r${' '.repeat(BAR_WIDTH + 60)}\r`);
|
|
151
150
|
}
|
|
152
151
|
|
|
152
|
+
// ── Score bar (for evaluate output) ──────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
function scoreBar(score, width = 20) {
|
|
155
|
+
const filled = Math.round(width * score / 100);
|
|
156
|
+
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function printEvalResults(result) {
|
|
160
|
+
const fid = result.fidelity;
|
|
161
|
+
const priv = result.privacy;
|
|
162
|
+
const sep = '─'.repeat(36);
|
|
163
|
+
|
|
164
|
+
process.stdout.write('\n');
|
|
165
|
+
process.stdout.write(' Fidelity\n');
|
|
166
|
+
process.stdout.write(` ${sep}\n`);
|
|
167
|
+
for (const [label, key] of [
|
|
168
|
+
['Mean', 'meanScore'],
|
|
169
|
+
['Variance', 'varScore'],
|
|
170
|
+
['Correlation', 'corScore'],
|
|
171
|
+
['KS', 'ksScore'],
|
|
172
|
+
]) {
|
|
173
|
+
const v = fid[key];
|
|
174
|
+
process.stdout.write(` ${label.padEnd(14)} ${v.toFixed(1).padStart(5)} ${scoreBar(v)}\n`);
|
|
175
|
+
}
|
|
176
|
+
process.stdout.write(` ${sep}\n`);
|
|
177
|
+
const fc = fid.composite;
|
|
178
|
+
process.stdout.write(` ${'Composite'.padEnd(14)} ${fc.toFixed(1).padStart(5)} ${scoreBar(fc)}\n`);
|
|
179
|
+
|
|
180
|
+
if (priv) {
|
|
181
|
+
process.stdout.write('\n');
|
|
182
|
+
process.stdout.write(' Privacy\n');
|
|
183
|
+
process.stdout.write(` ${sep}\n`);
|
|
184
|
+
for (const [label, key] of [
|
|
185
|
+
['Singling Out', 'singlingOut'],
|
|
186
|
+
['Linkability', 'linkability'],
|
|
187
|
+
['Inference', 'inference'],
|
|
188
|
+
]) {
|
|
189
|
+
const v = priv[key].score;
|
|
190
|
+
process.stdout.write(` ${label.padEnd(14)} ${v.toFixed(1).padStart(5)} ${scoreBar(v)}\n`);
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write(` ${sep}\n`);
|
|
193
|
+
const pc = priv.composite;
|
|
194
|
+
process.stdout.write(` ${'Composite'.padEnd(14)} ${pc.toFixed(1).padStart(5)} ${scoreBar(pc)}\n`);
|
|
195
|
+
} else if (result.privacy === null) {
|
|
196
|
+
process.stdout.write('\n Privacy: skipped (--skip-privacy)\n');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
process.stdout.write('\n');
|
|
200
|
+
const excluded = result.idColsExcluded || [];
|
|
201
|
+
const exclNote = excluded.length ? ` (${excluded.length} ID col${excluded.length > 1 ? 's' : ''} excluded)` : '';
|
|
202
|
+
process.stdout.write(
|
|
203
|
+
` ${(result.nReal || 0).toLocaleString()} real rows \xd7 ${result.pOrig || '?'} cols` +
|
|
204
|
+
` vs ${(result.nSynth || 0).toLocaleString()} synthetic rows${exclNote}\n`,
|
|
205
|
+
);
|
|
206
|
+
process.stdout.write('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Python runner (shared by evaluate + certify) ──────────────────────────────
|
|
210
|
+
|
|
211
|
+
function findPython() {
|
|
212
|
+
// Try python3 first, then python
|
|
213
|
+
for (const bin of ['python3', 'python']) {
|
|
214
|
+
try {
|
|
215
|
+
const { execFileSync } = require('child_process');
|
|
216
|
+
const out = execFileSync(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
217
|
+
if (out.includes('Python 3')) return bin;
|
|
218
|
+
} catch { /* try next */ }
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function runPythonScript(script, scriptArgs, { json: jsonMode = false } = {}) {
|
|
224
|
+
return new Promise((resolve) => {
|
|
225
|
+
const python = findPython();
|
|
226
|
+
if (!python) {
|
|
227
|
+
process.stderr.write(
|
|
228
|
+
'Error: Python 3 not found. Evaluation requires Python 3.10+.\n' +
|
|
229
|
+
' Install Python from https://python.org and retry.\n',
|
|
230
|
+
);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const proc = spawn(python, [script, ...scriptArgs], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
235
|
+
|
|
236
|
+
let resultLine = '';
|
|
237
|
+
let errorLine = '';
|
|
238
|
+
|
|
239
|
+
proc.stdout.on('data', (d) => { resultLine += d.toString(); });
|
|
240
|
+
|
|
241
|
+
proc.stderr.on('data', (d) => {
|
|
242
|
+
for (const line of d.toString().split('\n')) {
|
|
243
|
+
const s = line.trim();
|
|
244
|
+
if (!s) continue;
|
|
245
|
+
try {
|
|
246
|
+
const msg = JSON.parse(s);
|
|
247
|
+
if (typeof msg.progress === 'number' && !jsonMode) {
|
|
248
|
+
progressBar(msg.progress, msg.msg || '');
|
|
249
|
+
} else if (msg.error) {
|
|
250
|
+
errorLine = msg.error;
|
|
251
|
+
}
|
|
252
|
+
} catch { /* ignore non-JSON lines */ }
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
proc.on('close', (code) => {
|
|
257
|
+
if (!jsonMode) clearLine();
|
|
258
|
+
if (code !== 0) {
|
|
259
|
+
process.stderr.write(`Error: ${errorLine || 'evaluation process exited with code ' + code}\n`);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
let result;
|
|
263
|
+
try { result = JSON.parse(resultLine.trim()); }
|
|
264
|
+
catch {
|
|
265
|
+
process.stderr.write('Error: could not parse evaluation output\n');
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
resolve(result);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
proc.on('error', (err) => {
|
|
272
|
+
process.stderr.write(`Error: failed to start Python: ${err.message}\n`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
153
278
|
// ── Argument parsing helpers ──────────────────────────────────────────────────
|
|
154
279
|
|
|
155
280
|
function parseArgs(argv) {
|
|
@@ -197,7 +322,6 @@ async function cmdGenerate(pos, flags) {
|
|
|
197
322
|
process.stderr.write(`Generating synthetic data from ${path.basename(input)} …\n`);
|
|
198
323
|
}
|
|
199
324
|
|
|
200
|
-
// Build args for sphere-node.js
|
|
201
325
|
const nodeArgs = [SPHERE_NODE_JS, input, output];
|
|
202
326
|
if (flags.k) nodeArgs.push('--k', String(flags.k));
|
|
203
327
|
if (flags.theta) nodeArgs.push('--theta', String(flags.theta));
|
|
@@ -245,7 +369,7 @@ async function cmdGenerate(pos, flags) {
|
|
|
245
369
|
|
|
246
370
|
if (flags.json) {
|
|
247
371
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
248
|
-
resolve();
|
|
372
|
+
resolve(result);
|
|
249
373
|
return;
|
|
250
374
|
}
|
|
251
375
|
|
|
@@ -257,7 +381,7 @@ async function cmdGenerate(pos, flags) {
|
|
|
257
381
|
if (result.idColDetected) {
|
|
258
382
|
process.stdout.write(` ID columns excluded: ${result.idColName}\n`);
|
|
259
383
|
}
|
|
260
|
-
resolve();
|
|
384
|
+
resolve(result);
|
|
261
385
|
});
|
|
262
386
|
|
|
263
387
|
proc.on('error', (err) => {
|
|
@@ -267,6 +391,90 @@ async function cmdGenerate(pos, flags) {
|
|
|
267
391
|
});
|
|
268
392
|
}
|
|
269
393
|
|
|
394
|
+
// ── sphere evaluate ───────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
async function cmdEvaluate(pos, flags) {
|
|
397
|
+
await checkLicense();
|
|
398
|
+
|
|
399
|
+
const real = pos[0];
|
|
400
|
+
const synth = pos[1];
|
|
401
|
+
|
|
402
|
+
if (!real) { process.stderr.write('Error: missing real CSV\n'); process.exit(1); }
|
|
403
|
+
if (!synth) { process.stderr.write('Error: missing synth CSV\n'); process.exit(1); }
|
|
404
|
+
if (!fs.existsSync(real)) { process.stderr.write(`Error: file not found: ${real}\n`); process.exit(1); }
|
|
405
|
+
if (!fs.existsSync(synth)) { process.stderr.write(`Error: file not found: ${synth}\n`); process.exit(1); }
|
|
406
|
+
|
|
407
|
+
if (!flags.json) {
|
|
408
|
+
process.stderr.write(`Evaluating ${path.basename(real)} vs ${path.basename(synth)} …\n`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const scriptArgs = [real, synth];
|
|
412
|
+
if (flags['skip-privacy']) scriptArgs.push('--skip-privacy');
|
|
413
|
+
if (flags['n-attacks']) scriptArgs.push('--n-attacks', String(flags['n-attacks']));
|
|
414
|
+
if (flags['n-secrets']) scriptArgs.push('--n-secrets', String(flags['n-secrets']));
|
|
415
|
+
if (flags['n-atk-cap']) scriptArgs.push('--n-atk-cap', String(flags['n-atk-cap']));
|
|
416
|
+
if (flags['n-neighbors']) scriptArgs.push('--n-neighbors', String(flags['n-neighbors']));
|
|
417
|
+
if (flags['n-aux-cols']) scriptArgs.push('--n-aux-cols', String(flags['n-aux-cols']));
|
|
418
|
+
if (flags.seed) scriptArgs.push('--seed', String(flags.seed));
|
|
419
|
+
|
|
420
|
+
const result = await runPythonScript(EVALUATE_PY, scriptArgs, { json: !!flags.json });
|
|
421
|
+
|
|
422
|
+
if (flags.json) {
|
|
423
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
424
|
+
return result;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const evalS = ((result.runMs || 0) / 1000).toFixed(1);
|
|
428
|
+
process.stdout.write(`✓ Evaluation complete (${evalS} s)\n`);
|
|
429
|
+
printEvalResults(result);
|
|
430
|
+
return result;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── sphere certify ────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
async function cmdCertify(pos, flags) {
|
|
436
|
+
await checkLicense();
|
|
437
|
+
|
|
438
|
+
const real = pos[0];
|
|
439
|
+
const synth = pos[1];
|
|
440
|
+
const output = flags.output;
|
|
441
|
+
|
|
442
|
+
if (!real) { process.stderr.write('Error: missing real CSV\n'); process.exit(1); }
|
|
443
|
+
if (!synth) { process.stderr.write('Error: missing synth CSV\n'); process.exit(1); }
|
|
444
|
+
if (!output) { process.stderr.write('Error: missing -o / --output path\n'); process.exit(1); }
|
|
445
|
+
if (!fs.existsSync(real)) { process.stderr.write(`Error: file not found: ${real}\n`); process.exit(1); }
|
|
446
|
+
if (!fs.existsSync(synth)) { process.stderr.write(`Error: file not found: ${synth}\n`); process.exit(1); }
|
|
447
|
+
|
|
448
|
+
if (!flags.json) {
|
|
449
|
+
process.stderr.write(`Certifying ${path.basename(real)} vs ${path.basename(synth)} …\n`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const scriptArgs = [real, synth, '-o', output];
|
|
453
|
+
if (flags['skip-privacy']) scriptArgs.push('--skip-privacy');
|
|
454
|
+
if (flags['n-attacks']) scriptArgs.push('--n-attacks', String(flags['n-attacks']));
|
|
455
|
+
if (flags['n-secrets']) scriptArgs.push('--n-secrets', String(flags['n-secrets']));
|
|
456
|
+
if (flags['n-atk-cap']) scriptArgs.push('--n-atk-cap', String(flags['n-atk-cap']));
|
|
457
|
+
if (flags['n-neighbors']) scriptArgs.push('--n-neighbors', String(flags['n-neighbors']));
|
|
458
|
+
if (flags['n-aux-cols']) scriptArgs.push('--n-aux-cols', String(flags['n-aux-cols']));
|
|
459
|
+
if (flags.seed) scriptArgs.push('--seed', String(flags.seed));
|
|
460
|
+
if (flags.k) scriptArgs.push('--k', String(flags.k));
|
|
461
|
+
if (flags.theta) scriptArgs.push('--theta', String(flags.theta));
|
|
462
|
+
if (flags.delta) scriptArgs.push('--delta', String(flags.delta));
|
|
463
|
+
if (flags['mix-prob']) scriptArgs.push('--mix-prob', String(flags['mix-prob']));
|
|
464
|
+
if (flags['seed-gen']) scriptArgs.push('--seed-gen', String(flags['seed-gen']));
|
|
465
|
+
if (flags['generated-at']) scriptArgs.push('--generated-at', flags['generated-at']);
|
|
466
|
+
|
|
467
|
+
const result = await runPythonScript(CERTIFY_PY, scriptArgs, { json: !!flags.json });
|
|
468
|
+
|
|
469
|
+
if (flags.json) {
|
|
470
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const elapsedS = ((result.elapsedMs || 0) / 1000).toFixed(1);
|
|
475
|
+
process.stdout.write(`✓ ${output} (${elapsedS} s)\n`);
|
|
476
|
+
}
|
|
477
|
+
|
|
270
478
|
// ── sphere demo ───────────────────────────────────────────────────────────────
|
|
271
479
|
|
|
272
480
|
async function cmdDemo() {
|
|
@@ -280,17 +488,35 @@ async function cmdDemo() {
|
|
|
280
488
|
const tmpOut = path.join(os.tmpdir(), `sphere-demo-${randomUUID()}.csv`);
|
|
281
489
|
|
|
282
490
|
process.stdout.write('SPHERE demo — built-in NHANES dataset (4,899 rows \xd7 18 cols)\n');
|
|
283
|
-
process.stdout.write('─'.repeat(52) + '\n
|
|
491
|
+
process.stdout.write('─'.repeat(52) + '\n');
|
|
284
492
|
|
|
285
493
|
try {
|
|
494
|
+
// ── Generate ──────────────────────────────────────────────────────────────
|
|
495
|
+
process.stdout.write('\n');
|
|
286
496
|
await cmdGenerate([exampleCsv], { output: tmpOut });
|
|
497
|
+
|
|
498
|
+
// ── Evaluate ──────────────────────────────────────────────────────────────
|
|
499
|
+
process.stdout.write('\n');
|
|
500
|
+
process.stderr.write(`Evaluating ${path.basename(exampleCsv)} vs synthetic …\n`);
|
|
501
|
+
|
|
502
|
+
const scriptArgs = [exampleCsv, tmpOut, '--n-attacks', '200', '--n-atk-cap', '1000'];
|
|
503
|
+
const result = await runPythonScript(EVALUATE_PY, scriptArgs);
|
|
504
|
+
|
|
505
|
+
const evalS = ((result.runMs || 0) / 1000).toFixed(1);
|
|
506
|
+
process.stdout.write(`✓ Evaluation complete (${evalS} s)\n`);
|
|
507
|
+
printEvalResults(result);
|
|
508
|
+
|
|
509
|
+
} catch (err) {
|
|
510
|
+
process.stderr.write(`\nError during demo: ${err.message}\n`);
|
|
287
511
|
} finally {
|
|
288
512
|
try { fs.unlinkSync(tmpOut); } catch {}
|
|
289
513
|
try { fs.unlinkSync(tmpOut + '.sphere.json'); } catch {}
|
|
290
514
|
}
|
|
291
515
|
|
|
292
|
-
process.stdout.write('
|
|
293
|
-
process.stdout.write(' sphere generate your_data.csv -o synthetic.csv\n
|
|
516
|
+
process.stdout.write('Try it on your own data:\n');
|
|
517
|
+
process.stdout.write(' sphere generate your_data.csv -o synthetic.csv\n');
|
|
518
|
+
process.stdout.write(' sphere evaluate your_data.csv synthetic.csv\n');
|
|
519
|
+
process.stdout.write(' sphere certify your_data.csv synthetic.csv -o report.html\n\n');
|
|
294
520
|
}
|
|
295
521
|
|
|
296
522
|
// ── sphere license ────────────────────────────────────────────────────────────
|
|
@@ -299,7 +525,6 @@ async function cmdLicense(sub, pos, flags) {
|
|
|
299
525
|
if (sub === 'activate') {
|
|
300
526
|
let key = (pos[0] || flags.key || '').trim();
|
|
301
527
|
if (!key) {
|
|
302
|
-
// Simple stdin prompt (no readline dependency)
|
|
303
528
|
process.stdout.write('SPHERE license key (sphere_…): ');
|
|
304
529
|
key = await new Promise((resolve) => {
|
|
305
530
|
let buf = '';
|
|
@@ -395,9 +620,11 @@ async function main() {
|
|
|
395
620
|
|
|
396
621
|
if (!cmd || flags.help || flags.h) {
|
|
397
622
|
process.stdout.write(
|
|
398
|
-
`SPHERE CLI v${VERSION} — synthetic data generation\n\n` +
|
|
623
|
+
`SPHERE CLI v${VERSION} — synthetic data generation, evaluation & certification\n\n` +
|
|
399
624
|
'Usage:\n' +
|
|
400
|
-
' sphere generate <input.csv> -o <output.csv>
|
|
625
|
+
' sphere generate <input.csv> -o <output.csv> [options]\n' +
|
|
626
|
+
' sphere evaluate <real.csv> <synth.csv> [options]\n' +
|
|
627
|
+
' sphere certify <real.csv> <synth.csv> -o <cert.html> [options]\n' +
|
|
401
628
|
' sphere license activate|status|clear\n' +
|
|
402
629
|
' sphere demo\n\n' +
|
|
403
630
|
'Generate options:\n' +
|
|
@@ -406,6 +633,12 @@ async function main() {
|
|
|
406
633
|
' --delta <float> Per-pair angle jitter (default: 5° ≈ 0.087)\n' +
|
|
407
634
|
' --mix-prob <float> Privacy/utility trade-off (default: 0.75)\n' +
|
|
408
635
|
' --seed <int> Integer seed for reproducible output\n' +
|
|
636
|
+
' --json Machine-readable JSON output\n\n' +
|
|
637
|
+
'Evaluate / Certify options:\n' +
|
|
638
|
+
' --skip-privacy Fidelity only — skip privacy attacks (faster)\n' +
|
|
639
|
+
' --n-attacks <int> Attack draws per metric (default: 500)\n' +
|
|
640
|
+
' --n-atk-cap <int> Row subsample cap for attacks (default: 2000)\n' +
|
|
641
|
+
' --seed <int> RNG seed for reproducible scores\n' +
|
|
409
642
|
' --json Machine-readable JSON output\n',
|
|
410
643
|
);
|
|
411
644
|
process.exit(0);
|
|
@@ -416,6 +649,16 @@ async function main() {
|
|
|
416
649
|
return;
|
|
417
650
|
}
|
|
418
651
|
|
|
652
|
+
if (cmd === 'evaluate') {
|
|
653
|
+
await cmdEvaluate(pos.slice(1), flags);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (cmd === 'certify') {
|
|
658
|
+
await cmdCertify(pos.slice(1), flags);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
419
662
|
if (cmd === 'demo') {
|
|
420
663
|
await cmdDemo();
|
|
421
664
|
return;
|
|
@@ -426,14 +669,6 @@ async function main() {
|
|
|
426
669
|
return;
|
|
427
670
|
}
|
|
428
671
|
|
|
429
|
-
if (cmd === 'evaluate' || cmd === 'certify') {
|
|
430
|
-
process.stderr.write(
|
|
431
|
-
`'sphere ${cmd}' requires the Python evaluation engine, which is not included\n` +
|
|
432
|
-
'in this version. Coming soon.\n',
|
|
433
|
-
);
|
|
434
|
-
process.exit(1);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
672
|
process.stderr.write(`Unknown command: ${cmd}\nRun 'sphere --help' for usage.\n`);
|
|
438
673
|
process.exit(1);
|
|
439
674
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sphere-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
4
4
|
"description": "SPHERE CLI — synthetic data generation, evaluation, and certification",
|
|
5
5
|
"keywords": ["synthetic-data", "privacy", "cli", "data-science"],
|
|
6
6
|
"homepage": "https://github.com/statzihuai/sphere-cli",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|
|
13
13
|
"bin": {
|
|
14
|
-
"sphere": "
|
|
14
|
+
"sphere": "bin/sphere.js"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"postinstall": "node scripts/postinstall.js"
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"bin/",
|
|
24
24
|
"examples/",
|
|
25
25
|
"scripts/postinstall.js",
|
|
26
|
+
"scripts/evaluate.py",
|
|
27
|
+
"scripts/certify.py",
|
|
28
|
+
"sphere_cli/",
|
|
26
29
|
"sphere-node.js"
|
|
27
30
|
],
|
|
28
31
|
"engines": {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Thin subprocess wrapper — evaluate and produce an HTML certificate.
|
|
3
|
+
|
|
4
|
+
Called by sphere.js as:
|
|
5
|
+
python3 <this-file> real.csv synth.csv -o cert.html [options]
|
|
6
|
+
|
|
7
|
+
Streams {"progress": f, "msg": "..."} JSON lines to stderr.
|
|
8
|
+
Writes {"ok": true, "output": "cert.html", "elapsedMs": N} to stdout on success,
|
|
9
|
+
or {"error": "..."} on failure.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_PKG = Path(__file__).resolve().parent.parent
|
|
20
|
+
sys.path.insert(0, str(_PKG))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> None:
|
|
24
|
+
p = argparse.ArgumentParser()
|
|
25
|
+
p.add_argument('real')
|
|
26
|
+
p.add_argument('synth')
|
|
27
|
+
p.add_argument('-o', '--output', required=True)
|
|
28
|
+
p.add_argument('--skip-privacy', action='store_true')
|
|
29
|
+
p.add_argument('--n-attacks', type=int, default=500)
|
|
30
|
+
p.add_argument('--n-secrets', type=int, default=5)
|
|
31
|
+
p.add_argument('--n-atk-cap', type=int, default=2000)
|
|
32
|
+
p.add_argument('--n-neighbors', type=int, default=1)
|
|
33
|
+
p.add_argument('--n-aux-cols', type=int, default=20)
|
|
34
|
+
p.add_argument('--seed', type=int, default=None)
|
|
35
|
+
# Generation provenance (optional — marks certificate as SPHERE-generated)
|
|
36
|
+
p.add_argument('--k', type=int, default=None)
|
|
37
|
+
p.add_argument('--theta', type=float, default=None)
|
|
38
|
+
p.add_argument('--delta', type=float, default=None)
|
|
39
|
+
p.add_argument('--mix-prob', type=float, default=None)
|
|
40
|
+
p.add_argument('--seed-gen', type=int, default=None)
|
|
41
|
+
p.add_argument('--generated-at', type=str, default=None)
|
|
42
|
+
args = p.parse_args()
|
|
43
|
+
|
|
44
|
+
def on_progress(frac: float, msg: str) -> None:
|
|
45
|
+
sys.stderr.write(json.dumps({'progress': round(frac, 4), 'msg': msg}) + '\n')
|
|
46
|
+
sys.stderr.flush()
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
from sphere_cli._evaluate import evaluate # type: ignore
|
|
50
|
+
from sphere_cli._certify import build_certificate_html # type: ignore
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
sys.stderr.write(json.dumps({'error': f'Evaluation engine not available: {e}'}) + '\n')
|
|
53
|
+
sys.exit(1)
|
|
54
|
+
|
|
55
|
+
t0 = time.perf_counter()
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
result = evaluate(
|
|
59
|
+
real_path = Path(args.real),
|
|
60
|
+
synth_path = Path(args.synth),
|
|
61
|
+
n_attacks = args.n_attacks,
|
|
62
|
+
n_secrets = args.n_secrets,
|
|
63
|
+
n_atk_cap = args.n_atk_cap,
|
|
64
|
+
n_neighbors = args.n_neighbors,
|
|
65
|
+
n_aux_cols = args.n_aux_cols,
|
|
66
|
+
seed = args.seed,
|
|
67
|
+
skip_privacy = args.skip_privacy,
|
|
68
|
+
on_progress = on_progress,
|
|
69
|
+
)
|
|
70
|
+
except ValueError as e:
|
|
71
|
+
sys.stderr.write(json.dumps({'error': str(e)}) + '\n')
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
import traceback
|
|
75
|
+
sys.stderr.write(json.dumps({'error': f'Unexpected error: {e}\n{traceback.format_exc()}'}) + '\n')
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
result['elapsedMs'] = int((time.perf_counter() - t0) * 1000)
|
|
79
|
+
|
|
80
|
+
# Auto-load .sphere.json sidecar written by sphere generate
|
|
81
|
+
synth_path = Path(args.synth)
|
|
82
|
+
meta: dict = {}
|
|
83
|
+
meta_path = synth_path.with_suffix('.sphere.json')
|
|
84
|
+
if meta_path.exists():
|
|
85
|
+
try:
|
|
86
|
+
meta = json.loads(meta_path.read_text(encoding='utf-8'))
|
|
87
|
+
except Exception:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
# CLI flags win; sidecar fills gaps; defaults last
|
|
91
|
+
_theta = args.theta if args.theta is not None else meta.get('theta')
|
|
92
|
+
_delta = args.delta if args.delta is not None else meta.get('delta')
|
|
93
|
+
_mix_prob = args.mix_prob if args.mix_prob is not None else meta.get('mix_prob')
|
|
94
|
+
_k = args.k if args.k is not None else meta.get('k')
|
|
95
|
+
_seed_gen = args.seed_gen if args.seed_gen is not None else meta.get('seed')
|
|
96
|
+
_gen_at = args.generated_at if args.generated_at is not None else meta.get('generated_at')
|
|
97
|
+
|
|
98
|
+
import math
|
|
99
|
+
gen_params = None
|
|
100
|
+
if any(x is not None for x in [_theta, _delta, _mix_prob, _k, _seed_gen, _gen_at]):
|
|
101
|
+
gen_params = {
|
|
102
|
+
'theta': _theta if _theta is not None else math.pi / 6,
|
|
103
|
+
'delta': _delta if _delta is not None else 5 * math.pi / 180,
|
|
104
|
+
'mix_prob': _mix_prob if _mix_prob is not None else 0.75,
|
|
105
|
+
'k': _k if _k is not None else 2,
|
|
106
|
+
'seed': _seed_gen,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
html = build_certificate_html(
|
|
111
|
+
result = result,
|
|
112
|
+
real_path = Path(args.real),
|
|
113
|
+
synth_path = synth_path,
|
|
114
|
+
generation_params = gen_params,
|
|
115
|
+
generated_at = _gen_at,
|
|
116
|
+
)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
sys.stderr.write(json.dumps({'error': f'Certificate build failed: {e}'}) + '\n')
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
out_path = Path(args.output)
|
|
122
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
out_path.write_text(html, encoding='utf-8')
|
|
124
|
+
|
|
125
|
+
elapsed_ms = int((time.perf_counter() - t0) * 1000)
|
|
126
|
+
sys.stdout.write(json.dumps({'ok': True, 'output': str(out_path), 'elapsedMs': elapsed_ms}) + '\n')
|
|
127
|
+
sys.exit(0)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
if __name__ == '__main__':
|
|
131
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Thin subprocess wrapper — evaluate a real/synthetic CSV pair.
|
|
3
|
+
|
|
4
|
+
Called by sphere.js as:
|
|
5
|
+
python3 <this-file> real.csv synth.csv [options]
|
|
6
|
+
|
|
7
|
+
Streams {"progress": f, "msg": "..."} JSON lines to stderr.
|
|
8
|
+
Writes the final result dict as a single JSON line to stdout.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# Make sphere_cli importable from the npm package root (one level up)
|
|
18
|
+
_PKG = Path(__file__).resolve().parent.parent
|
|
19
|
+
sys.path.insert(0, str(_PKG))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
p = argparse.ArgumentParser()
|
|
24
|
+
p.add_argument('real')
|
|
25
|
+
p.add_argument('synth')
|
|
26
|
+
p.add_argument('--skip-privacy', action='store_true')
|
|
27
|
+
p.add_argument('--n-attacks', type=int, default=500)
|
|
28
|
+
p.add_argument('--n-secrets', type=int, default=5)
|
|
29
|
+
p.add_argument('--n-atk-cap', type=int, default=2000)
|
|
30
|
+
p.add_argument('--n-neighbors', type=int, default=1)
|
|
31
|
+
p.add_argument('--n-aux-cols', type=int, default=20)
|
|
32
|
+
p.add_argument('--seed', type=int, default=None)
|
|
33
|
+
args = p.parse_args()
|
|
34
|
+
|
|
35
|
+
def on_progress(frac: float, msg: str) -> None:
|
|
36
|
+
sys.stderr.write(json.dumps({'progress': round(frac, 4), 'msg': msg}) + '\n')
|
|
37
|
+
sys.stderr.flush()
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
from sphere_cli._evaluate import evaluate # type: ignore
|
|
41
|
+
except ImportError as e:
|
|
42
|
+
sys.stderr.write(json.dumps({'error': f'Evaluation engine not available: {e}'}) + '\n')
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
result = evaluate(
|
|
47
|
+
real_path = Path(args.real),
|
|
48
|
+
synth_path = Path(args.synth),
|
|
49
|
+
n_attacks = args.n_attacks,
|
|
50
|
+
n_secrets = args.n_secrets,
|
|
51
|
+
n_atk_cap = args.n_atk_cap,
|
|
52
|
+
n_neighbors = args.n_neighbors,
|
|
53
|
+
n_aux_cols = args.n_aux_cols,
|
|
54
|
+
seed = args.seed,
|
|
55
|
+
skip_privacy = args.skip_privacy,
|
|
56
|
+
on_progress = on_progress,
|
|
57
|
+
)
|
|
58
|
+
sys.stdout.write(json.dumps(result) + '\n')
|
|
59
|
+
sys.exit(0)
|
|
60
|
+
except ValueError as e:
|
|
61
|
+
sys.stderr.write(json.dumps({'error': str(e)}) + '\n')
|
|
62
|
+
sys.exit(1)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
import traceback
|
|
65
|
+
sys.stderr.write(json.dumps({'error': f'Unexpected error: {e}\n{traceback.format_exc()}'}) + '\n')
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == '__main__':
|
|
70
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|